TUI-пульт на Textual вместо текстового меню

Списки баз стенда и локальных дампов на одном экране, фильтр сужает их
на каждом набранном символе. Enter по базе открывает окно параметров
дампа с галочкой автоподнятия: снята — только dump, поднята — dump плюс
restore. Правки полей уезжают в копию конфига, config.json не трогается.

Ядро получило четыре хука для UI (лог, границы шагов, пароль, отмена),
run() читает вывод утилит построчно через Popen, поэтому журнал
наполняется во время работы pg_dump. Текстовый режим, CLI и --dry-run
ведут себя как раньше; без textual или вне tty пульт не запускается.

Журнал разворачивается на весь экран по F4 и сам на старте операции.
Раскладка адаптивная: narrow ниже 100 колонок, short ниже 32 строк.
Ctrl-хоткеи продублированы кириллицей — русская раскладка их не глушит.

Запуск через run.ps1 (venv, UTF-8-консоль, -Setup и -Tests).
Проверка — tests_tui.py: 46 тестов, TUI гоняется headless через Pilot.
This commit is contained in:
2026-08-20 05:21:52 +03:00
parent b30406bbf6
commit 9fdb02c72a
10 changed files with 3211 additions and 30 deletions
+311
View File
@@ -0,0 +1,311 @@
/* Пульт pg-stand-sync. Литеральных цветов здесь нет: всё через переменные темы,
иначе светлая тема рассыпается на первом же захардкоженном #hex. */
Screen {
background: $background;
color: $foreground;
}
#topbar {
height: 2;
padding: 0 1;
background: $surface;
color: $foreground;
}
#board {
layout: horizontal;
height: 1fr;
min-height: 6;
}
#pane-db {
width: 1fr;
}
#col-right {
width: 1fr;
}
.panel {
border: round $panel;
background: $surface;
padding: 0 1;
}
.panel:focus-within {
border: round $primary;
}
.panel > Input {
border: none;
height: 1;
padding: 0;
background: $surface;
}
.panel > OptionList {
height: 1fr;
background: $surface;
border: none;
padding: 0;
scrollbar-size-vertical: 1;
}
#target-card {
height: auto;
border: round $panel;
background: $surface;
padding: 0 1;
}
#pane-dump {
height: 1fr;
}
#bottom {
height: 20;
max-height: 60%;
min-height: 6;
}
#plan {
width: 36;
border: round $panel;
background: $surface;
padding: 0 1;
}
#log {
width: 1fr;
border: round $panel;
background: $surface;
padding: 0 1;
scrollbar-size-vertical: 1;
}
#status {
height: 1;
padding: 0 1;
background: $surface;
}
#op-spin {
width: 2;
color: $accent;
}
#op-title {
width: 1fr;
content-align: left middle;
}
#op-timer {
width: 10;
content-align: right middle;
color: $text-muted;
}
#op-progress {
width: 22;
}
/* Во время операции списки заперты: менять выбор посреди restore нечего */
.locked {
opacity: 0.55;
}
.panel.locked {
border: round $panel;
}
.pane-error {
border: round $error;
}
#db-error, #dump-note {
height: auto;
padding: 0 1;
color: $text-error;
display: none;
}
#db-error.shown, #dump-note.shown {
display: block;
}
/* --------------------------------------------------------------- модалки */
ModalScreen {
align: center middle;
background: $background 60%;
}
.dialog {
width: 72;
height: auto;
max-height: 80%;
border: round $primary;
background: $surface;
padding: 1 2;
opacity: 1;
transition: opacity 140ms;
}
.dialog > .title {
text-style: bold;
color: $text-primary;
padding-bottom: 1;
}
.dialog > .subject {
padding-bottom: 1;
}
.dialog Input {
margin-bottom: 1;
}
.danger {
background: $error 25%;
color: $text-error;
border-left: thick $error;
padding: 0 1;
margin-bottom: 1;
height: auto;
}
.danger.hidden {
display: none;
}
.row {
height: auto;
align-horizontal: right;
}
.row > Button {
margin-left: 2;
}
.switch-row {
height: auto;
margin-bottom: 1;
}
.switch-row > Label {
padding: 1 1 0 1;
}
#help-body {
height: auto;
}
/* Узкое окно: пульт складывается в колонку, обзорность теряется, но ничего не режется */
.narrow #board {
layout: vertical;
height: 1fr;
}
.narrow #pane-db {
width: 1fr;
height: 1fr;
min-height: 5;
}
.narrow #col-right {
width: 1fr;
height: 12;
}
.narrow.short #col-right {
height: 7;
}
.narrow #bottom {
layout: vertical;
}
.narrow #plan {
width: 1fr;
height: 35%;
min-height: 3;
}
.dialog > .form {
height: auto;
max-height: 24;
}
.dialog .section {
text-style: bold;
color: $text-primary;
padding-top: 1;
}
.dialog Collapsible {
border: none;
padding: 0;
margin-bottom: 1;
}
.dialog #target-block {
height: auto;
}
.dialog #target-block.hidden {
display: none;
}
.dialog .form Label {
color: $text-muted;
padding: 0 0 0 1;
}
.dialog .form Input {
width: 1fr;
border: tall $panel;
background: $boost;
}
.dialog .form Input:focus {
border: tall $primary;
}
.dialog Collapsible > CollapsibleTitle {
color: $text-primary;
}
.row > Button {
min-width: 14;
}
/* журнал во весь экран: на низком окне списки и лог не помещаются вместе */
.zoom-log #board {
display: none;
}
.zoom-log #bottom {
height: 1fr !important;
max-height: 100% !important;
}
/* низкое окно: фиксированные 20 строк снизу не оставляют места спискам */
.short #bottom {
height: 35%;
min-height: 5;
}
.short #plan {
width: 28;
}
/* низкое окно: карточка приёмника — справочная, место нужнее спискам */
.short #target-card {
display: none;
}
.zoom-log.narrow #plan {
height: 25%;
min-height: 3;
}