From 853d44e607617721307c7b2c504e1beb135059c8 Mon Sep 17 00:00:00 2001 From: Uber Veng Date: Thu, 21 May 2026 21:39:24 +0700 Subject: [PATCH] added readme --- README.md | 561 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 561 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..fbefb49 --- /dev/null +++ b/README.md @@ -0,0 +1,561 @@ +# 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 +``` + +Бинарь собирается статически и не требует никаких зависимостей на целевом сервере.