# TUI Client — удалённый запуск скриптов Консольный клиент с текстовым интерфейсом (TUI) на Rust. Загружает иерархическое меню с сервера и позволяет запускать bash-скрипты, скачивать файлы и выполнять HTTP-запросы прямо из терминала. Все диалоги (выбор из списка, подтверждение, ввод текста) реализованы внутри интерфейса — никаких сырых `read -p` в терминале. --- ## Навигация | Клавиша | Действие | |---|---| | `↑` / `↓` | Перемещение по списку | | `Enter` | Войти в категорию / запустить действие | | `Esc` | Выйти из категории / закрыть popup / отменить скрипт | | `q` | Выйти из приложения | | `R` | Перезагрузить меню с сервера | В попапе запущенного скрипта: | Клавиша | Действие | |---|---| | `↑` / `↓` | Прокрутка лога (если нет активного запроса) / навигация по селектору | | `PgUp` / `PgDn` | Прокрутка лога на 10 строк | | `Y` / `N` | Быстрый ответ на подтверждение | | `Esc` | Прервать скрипт и закрыть popup | --- ## Конфигурация При первом запуске создаётся файл `~/.config/tui-client/config.toml`: ```toml server_url = "http://your-server/api/menu" timeout_sec = 10 [theme] selected_bg = "blue" ``` --- ## Формат меню (JSON) Сервер должен отдавать `GET /api/menu` с `Content-Type: application/json`. ### Корневая структура ```json { "version": "1.0", "menu": [ ...пункты... ] } ``` ### Категория Содержит вложенные пункты. Отображается с иконкой `📁`. ```json { "id": "bitrix", "title": "1С Битрикс", "description": "Инструменты администрирования", "children": [ ...пункты меню... ] } ``` | Поле | Обязательное | Описание | |---|---|---| | `id` | ✓ | Уникальный идентификатор | | `title` | ✓ | Отображаемое название | | `description` | — | Текст в нижней панели при выборе | | `children` | ✓ | Вложенные категории и действия | ### Действие Отображается с иконкой `⚡`. ```json { "id": "db_manage", "title": "Управление БД", "description": "Дамп, импорт, подключение к базе", "action": { "type": "bash", "script": "#!/bin/bash\necho hello", "interaction": "structured", "confirm": false } } ``` | Поле | Обязательное | Описание | |---|---|---| | `id` | ✓ | Уникальный идентификатор | | `title` | ✓ | Отображаемое название | | `description` | — | Текст в нижней панели | | `action` | ✓ | Объект действия | #### `confirm` и `confirm_message` Если `confirm: true`, перед запуском появится диалог подтверждения. `confirm_message` переопределяет текст вопроса. ```json { "id": "drop_db", "title": "Удалить базу данных", "action": { "type": "bash", "script": "...", "interaction": "structured", "confirm": true, "confirm_message": "Внимание! База данных будет удалена без возможности восстановления. Продолжить?" } } ``` --- ## Типы действий ### `bash` — запуск скрипта ```json { "type": "bash", "script": "#!/bin/bash\n...", "interaction": "structured", "confirm": false } ``` | Параметр | Значения | Описание | |---|---|---| | `script` | строка | Полный текст bash-скрипта включая shebang | | `interaction` | `"structured"` / `"terminal"` | Режим взаимодействия (см. ниже) | | `confirm` | `true` / `false` | Запросить подтверждение перед запуском | | `confirm_message` | строка | Кастомный текст диалога подтверждения | **`interaction: "terminal"`** — скрипт запускается как дочерний процесс, вывод игнорируется клиентом. Подходит для простых неинтерактивных команд. **`interaction: "structured"`** — скрипт общается с клиентом через протокол команд (см. раздел ниже). Весь вывод отображается в прокручиваемом логе с поддержкой ANSI-цветов. Поддерживает интерактивные элементы: селекторы, поля ввода, подтверждения. ### `download` — скачивание файла ```json { "type": "download", "url": "https://example.com/file.tar.gz", "filename": "backup.tar.gz", "target_dir": "/var/backups", "confirm": false } ``` | Параметр | Описание | |---|---| | `url` | Адрес файла | | `filename` | Имя сохраняемого файла (по умолчанию — из URL) | | `target_dir` | Директория сохранения (по умолчанию — текущая) | ### `download_and_run` — скачать и выполнить ```json { "type": "download_and_run", "url": "https://example.com/installer.sh", "run_args": ["--silent", "--prefix=/opt"], "keep_file": false, "confirm": true } ``` | Параметр | Описание | |---|---| | `run_args` | Аргументы командной строки | | `keep_file` | Оставить файл после выполнения (по умолчанию `false`) | ### `http_request` — HTTP-запрос ```json { "type": "http_request", "method": "POST", "url": "https://api.example.com/deploy", "headers": { "Authorization": "Bearer token123", "Content-Type": "application/json" }, "body": "{\"env\": \"production\"}", "confirm": true } ``` --- ## Интерактивные скрипты (structured) При `interaction: "structured"` скрипт может запрашивать у пользователя данные через специальный протокол. Клиент показывает соответствующий элемент интерфейса ratatui и ждёт ответа. ### Базовые хелперы Добавьте эти функции в начало каждого интерактивного скрипта: ```bash #!/bin/bash # Отправить команду и получить ответ от пользователя ask() { printf 'CMD:%s\n' "$1" >&2 read -r _response printf '%s' "$_response" } # Отправить уведомление (не ждёт ответа) notify() { printf 'CMD:%s\n' "$1" >&2 } ``` Команды отправляются в **stderr** (`>&2`), ответы приходят в **stdin** через `read`. Это позволяет использовать `$(ask '...')` для захвата результата. --- ### Ввод текста (`input`) Показывает текстовое поле с заголовком. Поддерживает маскировку для паролей. **Команда:** ```json {"type":"input","prompt":"Введите имя пользователя:","default":"admin","secret":false} ``` | Поле | Описание | |---|---| | `prompt` | Заголовок поля ввода | | `default` | Значение по умолчанию (опционально) | | `secret` | Если `true` — символы заменяются на `•` | **Пример:** ```bash username=$(ask '{"type":"input","prompt":"Имя пользователя:","default":"admin"}') password=$(ask '{"type":"input","prompt":"Пароль:","secret":true}') echo "Логин: $username" ``` --- ### Выбор из списка (`menu`) Показывает список с навигацией `↑`/`↓` и выбором по `Enter`. Возвращает `id` выбранного пункта. **Команда:** ```json { "type": "menu", "prompt": "Выберите действие:", "options": [ {"id": "dump", "label": "Создать дамп базы"}, {"id": "import", "label": "Импортировать дамп"}, {"id": "connect","label": "Подключиться к БД"} ] } ``` | Поле | Описание | |---|---| | `prompt` | Заголовок списка | | `options` | Массив объектов `{"id": "...", "label": "..."}` | **Ответ:** строка `id` выбранного пункта. **Пример:** ```bash action=$(ask '{"type":"menu","prompt":"Выберите действие:","options":[ {"id":"dump","label":"Создать дамп"}, {"id":"import","label":"Импортировать"}, {"id":"exit","label":"Отмена"} ]}') case $action in dump) echo "Создаю дамп..." ;; import) echo "Импортирую..." ;; exit) exit 0 ;; esac ``` **Динамический список** (например, файлы на диске): ```bash files=($(ls *.sql 2>/dev/null)) options='[' for i in "${!files[@]}"; do [ $i -gt 0 ] && options+=',' escaped=$(printf '%s' "${files[$i]}" | sed 's/"/\\"/g') options+="{\"id\":\"$i\",\"label\":\"${escaped}\"}" done options+=']' idx=$(ask "{\"type\":\"menu\",\"prompt\":\"Выберите файл:\",\"options\":${options}}") selected="${files[$idx]}" echo "Выбран: $selected" ``` --- ### Подтверждение (`confirm`) Показывает вопрос с кнопками `[Y] Да` и `[N] Нет`. Нажатия `Y`/`N` обрабатываются без `Enter`. **Команда:** ```json {"type":"confirm","prompt":"Данные будут перезаписаны. Продолжить?"} ``` **Ответ:** `"y"` или `"n"`. **Пример:** ```bash answer=$(ask '{"type":"confirm","prompt":"Удалить временные файлы?"}') if [[ "$answer" =~ ^[Yy]$ ]]; then rm -rf /tmp/myapp_* echo "Файлы удалены" else echo "Отменено" fi ``` --- ### Сообщение (`message`) Показывает информационное сообщение с цветовым выделением. Скрипт продолжается после нажатия `Enter`. **Команда:** ```json {"type":"message","level":"info","text":"Операция завершена успешно!"} ``` | `level` | Цвет | Когда использовать | |---|---|---| | `"info"` | Зелёный | Успешное завершение, информация | | `"warn"` | Жёлтый | Предупреждение, нестандартная ситуация | | `"error"` | Красный | Ошибка, критическая проблема | **Пример:** ```bash if ! command -v mysqldump &> /dev/null; then ask '{"type":"message","level":"error","text":"mysqldump не найден. Установите пакет mysql-client."}' > /dev/null exit 1 fi ask '{"type":"message","level":"warn","text":"База данных занимает более 10 ГБ. Дамп может занять несколько минут."}' > /dev/null echo "Начинаю создание дампа..." ``` > `> /dev/null` — сбрасывает пустой ответ клиента, чтобы он не попал в переменную. --- ### Прогресс (`progress`) Обновляет прогресс-бар без остановки скрипта. Скрипт продолжает выполнение немедленно. **Команда:** ```json {"type":"progress","percent":45,"message":"Копирование файлов..."} ``` | Поле | Описание | |---|---| | `percent` | Число от 0 до 100 | | `message` | Подпись под прогресс-баром (опционально) | **Пример:** ```bash notify '{"type":"progress","percent":0,"message":"Начало..."}' mysqldump -h "$host" -u "$user" "$db" > backup.sql notify '{"type":"progress","percent":60,"message":"Дамп создан, сжимаю..."}' gzip backup.sql notify '{"type":"progress","percent":100,"message":"Готово!"}' echo "Файл: backup.sql.gz ($(du -h backup.sql.gz | cut -f1))" ``` --- ### Запуск внешнего приложения (`exec`) Временно **передаёт терминал** внешней программе (mysql, vim, htop и т.д.). Клиент скрывает ratatui, программа работает на полном экране, после завершения TUI восстанавливается. Возвращает код завершения. **Команда:** ```json {"type":"exec","shell":"mysql -h localhost -u root mydb"} ``` | Поле | Описание | |---|---| | `shell` | Команда, которая будет выполнена через `bash -c` | **Ответ:** код завершения программы (`"0"`, `"1"` и т.д.). **Примеры:** ```bash # Интерактивная сессия mysql exit_code=$(ask "{\"type\":\"exec\",\"shell\":\"mysql -h \\\"$host\\\" -u \\\"$user\\\" -p\\\"$pass\\\" \\\"$db\\\"\"}") echo "Сессия завершена (код: $exit_code)" # Редактирование конфига ask '{"type":"exec","shell":"vim /etc/nginx/nginx.conf"}' > /dev/null notify '{"type":"progress","percent":0,"message":"Перезагружаю nginx..."}' systemctl reload nginx # Мониторинг ресурсов ask '{"type":"exec","shell":"htop"}' > /dev/null ``` --- ## Полный пример скрипта ```bash #!/bin/bash ask() { printf 'CMD:%s\n' "$1" >&2 read -r _response printf '%s' "$_response" } notify() { printf 'CMD:%s\n' "$1" >&2 } # Запрос данных host=$(ask '{"type":"input","prompt":"Хост базы данных:","default":"localhost"}') db=$(ask '{"type":"input","prompt":"Имя базы данных:"}') user=$(ask '{"type":"input","prompt":"Пользователь:","default":"root"}') pass=$(ask '{"type":"input","prompt":"Пароль:","secret":true}') echo "Подключаюсь к $db на $host..." # Выбор действия action=$(ask "{\"type\":\"menu\",\"prompt\":\"Выберите действие:\",\"options\":[ {\"id\":\"dump\", \"label\":\"Создать дамп\"}, {\"id\":\"shell\", \"label\":\"Открыть mysql-консоль\"}, {\"id\":\"cancel\", \"label\":\"Отмена\"} ]}") case $action in dump) confirm=$(ask '{"type":"confirm","prompt":"Создать дамп базы данных?"}') [[ ! "$confirm" =~ ^[Yy]$ ]] && exit 0 backup="backup_${db}_$(date +%Y%m%d_%H%M%S).sql" notify '{"type":"progress","percent":10,"message":"Создание дампа..."}' if mysqldump -h "$host" -u "$user" -p"$pass" "$db" > "$backup"; then notify '{"type":"progress","percent":100,"message":"Готово!"}' echo "Дамп сохранён: $backup ($(du -h "$backup" | cut -f1))" else ask '{"type":"message","level":"error","text":"Ошибка при создании дампа!"}' > /dev/null exit 1 fi ;; shell) echo "Открываю mysql-консоль..." code=$(ask "{\"type\":\"exec\",\"shell\":\"mysql -h \\\"$host\\\" -u \\\"$user\\\" -p\\\"$pass\\\" \\\"$db\\\"\"}") echo "Сессия завершена с кодом $code" ;; cancel) echo "Отменено" exit 0 ;; esac ``` --- ## Структура сервера Рекомендуемая организация серверной части: ``` server/ ├── index.php # Собирает JSON из menu_structure.json + scripts/ ├── menu_structure.json # Иерархия меню (script_file вместо script) └── scripts/ ├── bitrix_db_manage.sh ├── bitrix_monitoring.sh └── wordpress_db_backup.sh ``` В `menu_structure.json` вместо поля `"script"` используется `"script_file"`: ```json { "id": "db_manage", "title": "Управление БД", "action": { "type": "bash", "script_file": "bitrix_db_manage.sh", "interaction": "structured", "confirm": false } } ``` `index.php` подставляет содержимое файла при каждом запросе — скрипты редактируются без изменения JSON. ### Добавление нового пункта меню 1. Создать скрипт `server/scripts/myapp_action.sh` 2. Добавить пункт в `menu_structure.json`: ```json { "id": "myapp_action", "title": "Название в меню", "description": "Описание для нижней панели", "action": { "type": "bash", "script_file": "myapp_action.sh", "interaction": "structured", "confirm": false } } ``` Именование файлов: `{категория}_{подкатегория}_{действие}.sh` --- ## Сборка **Linux:** ```bash cd Ratatui cargo build --release # → target/release/tui-client ``` **Кросс-компиляция с macOS → Linux (musl, статический бинарь):** ```bash rustup target add x86_64-unknown-linux-musl brew install FiloSottile/musl-cross/musl-cross CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc \ cargo build --release --target x86_64-unknown-linux-musl # → target/x86_64-unknown-linux-musl/release/tui-client ``` Бинарь собирается статически и не требует никаких зависимостей на целевом сервере.