Files
ostiary/README.md
2026-05-21 21:39:24 +07:00

562 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```
Бинарь собирается статически и не требует никаких зависимостей на целевом сервере.