486 lines
38 KiB
Markdown
486 lines
38 KiB
Markdown
# KomiTerm — SSH/Telnet клиент на Rust
|
||
|
||
Минималистичная альтернатива MobaXterm под Linux: session manager + SSH/Telnet
|
||
в терминале, без X11-сервера и RDP/VNC (сознательно вырезано ради MVP).
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
main.rs — CLI (clap): запуск сессии + subcommand `profile add/list/remove`
|
||
session.rs — конфиг сессий (~/.config/komiterm/sessions.toml), AuthMethod/PassphraseSource
|
||
profiles.rs — именованные credential-профили (~/.config/komiterm/credentials.toml)
|
||
hotkeys.rs — конфигурируемые хоткеи табов (~/.config/komiterm/hotkeys.toml)
|
||
sftp.rs — обёртка над russh_sftp::client::SftpSession (листинг/cd/download/upload)
|
||
localfs.rs — локальный файловый браузер для выбора файла при закачке (u в SFTP-режиме)
|
||
hoststats.rs — парсинг CPU/памяти/load average из /proc для строки состояния
|
||
sshconfig.rs — парсер ~/.ssh/config, включая Include (см. ниже)
|
||
resolve.rs — слияние приоритетов: session > credential-профиль > ssh_config > дефолты
|
||
transport/
|
||
mod.rs — трейт Transport (write/resize/close/open_sftp/run_command) — общий интерфейс
|
||
ssh.rs — реализация через russh, поддерживает Password/Key/Agent + open_sftp/run_command
|
||
telnet.rs — реализация telnet с ответом на DO/WILL (не просто дроп)
|
||
serial.rs — последовательный порт (COM/tty) через tokio-serial
|
||
term.rs — обёртка над alacritty_terminal (парсинг escape-последовательностей)
|
||
ui.rs — рендер сетки терминала + боковая панель (сессии / SFTP-браузер)
|
||
app.rs — главный цикл: incoming -> term_state -> render, ввод -> transport
|
||
```
|
||
|
||
## Аутентификация и профили
|
||
|
||
Способ входа можно задать двумя способами:
|
||
|
||
1. **Прямо в сессии** — поле `auth` в `sessions.toml`.
|
||
2. **Через именованный профиль** — заведи один раз в `credentials.toml`
|
||
(командой `komiterm profile add`), затем ссылайся на него по имени полем
|
||
`credential` из любой сессии. Удобно, когда один логин/ключ используется
|
||
для нескольких хостов.
|
||
|
||
Приоритет при разрешении (см. `resolve.rs`): explicit `auth` в сессии >
|
||
`credential`-профиль > подсказка из `~/.ssh/config` (IdentityFile) > запрос
|
||
пароля как последний вариант.
|
||
|
||
```bash
|
||
komiterm profile add # интерактивно: имя, логин, пароль/ключ/agent*
|
||
komiterm profile list
|
||
komiterm profile remove myprofile
|
||
```
|
||
|
||
\* ssh-agent как способ входа временно не работает (см. TODO ниже) —
|
||
профиль с ним создать можно, но подключение с таким профилем откажет.
|
||
|
||
Пароли и passphrase **никогда не сохраняются на диск** — только логин,
|
||
путь к ключу и признак "спросить passphrase при подключении". Файл
|
||
`credentials.toml` на Unix создаётся с правами 600.
|
||
|
||
## known_hosts / защита от MITM
|
||
|
||
`check_server_key` в `transport/ssh.rs` больше не доверяет серверу
|
||
безусловно — сверяется с `~/.ssh/known_hosts` через `russh::keys::check_known_hosts`:
|
||
|
||
- **Ключ совпадает с записанным** — подключаемся молча, как обычно.
|
||
- **Записи для хоста нет вообще** (первое подключение) — печатается
|
||
отпечаток ключа, и спрашивается подтверждение (Trust On First Use, как
|
||
делает обычный `ssh` при "authenticity of host can't be established").
|
||
При согласии ключ дописывается в known_hosts — своей маленькой функцией
|
||
(`transport/ssh.rs::learn_known_hosts`), а не `russh::keys::learn_known_hosts`:
|
||
в используемой версии russh (0.62.2) это имя не реэкспортируется на
|
||
верхнем уровне `russh::keys` (только `check_known_hosts`), так что строку
|
||
формируем сами через `PublicKey::to_openssh()` — формат known_hosts
|
||
простой и стабилен десятилетиями, велосипед тут минимальный.
|
||
- **Запись есть, но ключ другой** — это подозрение на MITM или
|
||
переустановленный сервер. Здесь **нет** интерактивного "продолжить всё
|
||
равно": подключение отклоняется безусловно, с инструкцией вручную
|
||
почистить строку в `~/.ssh/known_hosts`, если пользователь уверен, что
|
||
смена ключа легитимна. Слабое место так просто не обойти повторным нажатием Enter.
|
||
|
||
Из-за этого пришлось поднять версию `russh` с `0.44` до текущей (`>=0.50`,
|
||
реально резолвится в `0.62.x`) — модуль работы с ключами в старой версии
|
||
жил отдельным крейтом `russh-keys` и не имел этих функций в удобном виде;
|
||
в текущей версии `russh::keys` включает и агента, и известные хосты, и
|
||
работу с ключами в одном месте. Заодно поменялся API аутентификации по
|
||
ключу (`authenticate_publickey` теперь принимает `PrivateKeyWithHashAlg`
|
||
вместо голого `Arc<PrivateKey>`) — учтено в текущем коде.
|
||
|
||
## ~/.ssh/config
|
||
|
||
Если у сессии `use_ssh_config = true` (по умолчанию), поле `host` сначала
|
||
ищется как алиас в `~/.ssh/config` — так же, как если бы ты написал
|
||
`ssh myalias`. Оттуда подтягиваются `HostName`, `User`, `Port`,
|
||
`IdentityFile`, если сессия их не переопределяет явно.
|
||
|
||
Путь один и тот же на всех трёх платформах (`<домашняя_директория>/.ssh/config`),
|
||
поэтому специального кода под Windows не потребовалось — OpenSSH для Windows
|
||
использует ту же схему.
|
||
|
||
Поддержана директива `Include` (в т.ч. с `*`-wildcard в имени файла и путями
|
||
относительно директории текущего конфига — `Include config.d/*` работает).
|
||
Совпадения `*`-паттерна обходятся в отсортированном порядке, как это делает
|
||
сам OpenSSH, иначе порядок Include стал бы недетерминированным. Есть защита
|
||
от циклов (`a.conf` включает `b.conf` включает `a.conf`) через отслеживание
|
||
уже разобранных файлов и предел глубины. Из не поддержанного: `Match`-блоки
|
||
и полноценные glob-паттерны (`?`, `[abc]`, отрицания через `!`) — см. TODO
|
||
в конце `sshconfig.rs`.
|
||
|
||
## Табы (мультиплексирование сессий)
|
||
|
||
```bash
|
||
komiterm myserver staging-box old-switch # три таба сразу при запуске
|
||
komiterm # один таб, сессия через менеджер
|
||
```
|
||
|
||
Хоткеи читаются из `~/.config/komiterm/hotkeys.toml` (создаётся при первом
|
||
запуске с показанными ниже значениями по умолчанию; `Alt+1..Alt+9` в конфиг
|
||
не вынесены — см. комментарий в `hotkeys.rs`, почему):
|
||
|
||
| Комбинация | Действие |
|
||
|------------------------|----------------------------------------|
|
||
| `Ctrl+PageUp/PageDown` | предыдущий / следующий таб |
|
||
| `Alt+1`..`Alt+9` | прыжок на таб N |
|
||
| `F1` | фокус на боковую панель / обратно на терминал |
|
||
| `F2` | новый таб (выбор сессии из списка) |
|
||
| `Ctrl+F2` | закрыть активный таб |
|
||
| `F3` | свернуть/развернуть боковую панель |
|
||
| `F4` | SFTP-браузер для активного таба (только SSH) |
|
||
| `F10` | выйти из KomiTerm целиком |
|
||
|
||
Все табы держат живое соединение и вычитывают входящие данные каждый кадр
|
||
(фон не "замирает", даже если ты сейчас смотришь в другой таб) — рисуется
|
||
только активный. Открытие нового таба (`F2`) и начальный выбор сессии (если
|
||
она не передана аргументом) показывают список сессий прямо поверх текущего
|
||
экрана через `ratatui::widgets::List` — без выхода из raw-режима и без
|
||
"грязного" экрана, как было в первой версии.
|
||
|
||
Если сохранённых сессий вообще нет (первый запуск), список не ошибка, а
|
||
пустой экран с подсказкой — нажми `n`, чтобы создать сессию прямо тут же,
|
||
не выходя из TUI и не редактируя `sessions.toml` руками. `n` работает и
|
||
при непустом списке — добавить ещё одну сессию можно в любой момент, когда
|
||
показан этот список (при `F2` или при начальном выборе). Мастер — короткий
|
||
пошаговый опрос (протокол → имя → поля по протоколу, включая способ входа
|
||
для SSH), каждое поле — Enter продолжить, Esc отменяет всё создание сразу.
|
||
Готовая сессия дописывается в `sessions.toml` и сразу открывается табом.
|
||
|
||
**Как закрыть/выйти из одной сессии** (не из всего komiterm): `Ctrl+F2`
|
||
закрывает активный таб явно. Выйти изнутри сессии обычным способом
|
||
(`exit` или `Ctrl+D` в шелле) тоже работает — таб закрывается сам, как
|
||
только замечает, что канал от удалённой стороны закрылся (проверяется
|
||
каждый кадр). Если все табы закрылись — komiterm завершается сам, выходить
|
||
из него отдельно уже не нужно.
|
||
|
||
## Боковая панель сессий
|
||
|
||
Слева — как в MobaXterm — постоянная панель со списком всех сохранённых
|
||
сессий (`ui::render_sidebar`), фиксированной ширины (`SIDEBAR_WIDTH = 24`
|
||
колонки). Открытые сейчас как табы сессии помечены точкой, активная —
|
||
подсвечена инверсией. Сворачивается/разворачивается по `F3`, освобождая
|
||
эти 24 колонки под содержимое терминала.
|
||
|
||
Важный нюанс: при сворачивании/разворачивании панели меняется реальная
|
||
ширина видимой области терминала, поэтому `F3` не просто перерисовывает
|
||
UI — он ресайзит PTY на удалённой стороне (`transport.resize`) для всех
|
||
табов, как при обычном ресайзе окна. Без этого full-screen приложения на
|
||
той стороне (vim, htop, less) не узнали бы о появившемся/пропавшем месте.
|
||
|
||
**Панель интерактивна**, но не одновременно с терминалом — есть явный
|
||
фокус, переключаемый `F1` (если панель была скрыта, `F1` заодно её
|
||
показывает). В фокусе рамка панели становится жёлтой — это единственный
|
||
визуальный признак того, куда сейчас уйдёт следующая клавиша. Пока панель
|
||
в фокусе, **любая** клавиша принадлежит ей, а не терминалу (даже
|
||
нераспознанная — просто игнорируется), чтобы не было пограничных случаев
|
||
вида "стрелка ушла и туда, и туда":
|
||
|
||
- `↑`/`↓` (или `j`/`k`) — выбор сессии в списке
|
||
- `Enter` — переключиться на уже открытый таб этой сессии, либо открыть
|
||
новый (без модального picker'а — сессия уже выбрана курсором)
|
||
- `Esc` — вернуть фокус терминалу (панель остаётся видна)
|
||
|
||
`F4` (SFTP) при открытии сразу переводит фокус на панель — незачем
|
||
открывать браузер файлов и не иметь возможности сразу же по нему
|
||
перемещаться. `Esc` внутри SFTP-режима возвращает и режим панели к списку
|
||
сессий, и фокус терминалу — одним действием.
|
||
|
||
## SFTP-браузер
|
||
|
||
`F4` открывает в той же боковой панели файловый браузер для **активного**
|
||
таба (переиспользует уже открытое SSH-соединение — отдельный channel с
|
||
subsystem `sftp`, без нового пароля/ключа). Работает только для SSH,
|
||
для Telnet-сессий покажет заглушку "SFTP недоступен".
|
||
|
||
Пока панель в режиме SFTP, стрелки/`j`/`k`/Enter/Esc уходят в неё, а не в
|
||
шелл — так и должно быть, иначе непонятно, куда идёт навигация:
|
||
|
||
- `↑`/`↓` (или `j`/`k`) — выбор записи в списке
|
||
- `Enter` на папке — зайти внутрь; `..` наверху списка — выйти на уровень выше
|
||
- `Enter` на файле — скачать в `~/Downloads/komiterm/<имя_сессии>/`
|
||
- `u` — открыть локальный файловый браузер и закачать выбранный файл в
|
||
текущую директорию (см. ниже)
|
||
- `Esc` — вернуть панель к списку сессий (сама SFTP-сессия при этом не
|
||
закрывается — переключение обратно на `F4` покажет тот же листинг без
|
||
повторного похода на сервер)
|
||
- Повторный `F4` — то же самое, что `Esc`
|
||
|
||
### Закачка файлов (`u`)
|
||
|
||
`u` внутри SFTP-режима открывает `localfs::LocalBrowser` — тот же список
|
||
файлов, но по локальному диску, начиная с домашней директории (обычный
|
||
`std::fs`, никакого SSH). Навигация та же (`↑`/`↓`/`j`/`k`, `..` наверху),
|
||
`Enter` на файле закачивает его в текущую директорию SFTP-браузера
|
||
активного таба под тем же именем и возвращает в SFTP-режим с уже
|
||
обновлённым листингом (закачанный файл сразу виден в списке). `Esc` —
|
||
отмена, назад в SFTP-режим без закачки.
|
||
|
||
`sftp.rs` — тонкая обёртка над `russh_sftp::client::SftpSession`: листинг
|
||
директории (папки впереди файлов, дальше по алфавиту), `cd` через
|
||
`canonicalize` (чтобы `..` и симлинки резолвил сам сервер, а не наша
|
||
склейка строк), download/upload. Download/upload грузят файл целиком в
|
||
память, для реально больших файлов нужно будет стримить чанками — см.
|
||
TODO в конце `sftp.rs`.
|
||
|
||
API `russh_sftp` сверен с реальными примерами из репозитория
|
||
(`AspectUnk/russh-sftp/examples/client.rs`, тот же паттерн — в
|
||
`russh/examples/sftp_client.rs`), а не написан по памяти.
|
||
|
||
## Строка состояния хоста
|
||
|
||
Внизу экрана (под содержимым терминала, отдельной строкой) — CPU/память/
|
||
load average активного таба, обновляется раз в `STATS_INTERVAL` (5 секунд
|
||
по умолчанию, константа в `app.rs`).
|
||
|
||
Данные берутся не парсингом вывода интерактивного шелла (это было бы
|
||
ненадёжно — зависит от промпта, алиасов, запущенной программы), а отдельным
|
||
**exec-каналом** той же SSH-сессии — `Transport::run_command()`, новый
|
||
метод трейта, третий по счёту наряду с `open_sftp` (тоже дефолт `Ok(None)`
|
||
для Telnet). Команда гоняет `cat /proc/loadavg`, первые строки
|
||
`/proc/meminfo` и первую строку `/proc/stat`, разделяя секции меткой `@@@`
|
||
для надёжного парсинга (`hoststats::parse`). Соответственно работает
|
||
только на Linux-хостах и только по SSH — для Telnet и не-Linux строка
|
||
состояния просто показывает имя сессии без метрик, без ошибок в интерфейсе.
|
||
|
||
CPU% — не то же самое, что load average: это доля "не-idle" времени между
|
||
двумя последовательными снятиями `/proc/stat` (как считает `top`/`htop`),
|
||
поэтому появляется только со второго опроса — на первом кадре после
|
||
подключения будет прочерк, пока не накопится дельта.
|
||
|
||
Честный компромисс: опрос выполняется прямо в основном цикле и блокирует
|
||
отрисовку/ввод на время round-trip команды (обычно доли секунды, но на
|
||
дальних/медленных соединениях может быть заметно) — раз в 5 секунд это
|
||
редкость, но не бесплатно. Опрашивается только активный таб — у фоновых
|
||
вкладок статистика не обновляется, пока на них не переключишься.
|
||
|
||
## Serial / COM-подключения
|
||
|
||
Для консольных портов сетевого/embedded-оборудования — как в MobaXterm, но
|
||
без модема и прочего "телефонного" наследия PuTTY. Транспорт (`serial.rs`)
|
||
использует `tokio-serial` — тот же паттерн read/write-задач, что и у telnet,
|
||
просто без протокола поверх (сырые байты в обе стороны, никакого IAC).
|
||
|
||
```toml
|
||
[[session]]
|
||
name = "switch-console"
|
||
protocol = "serial"
|
||
port = "/dev/ttyUSB0" # Linux/macOS; на Windows — "COM3", "COM15" и т.п.
|
||
baud_rate = 115200 # выбор скорости — обязательное поле, дефолта нет
|
||
# Остальное — опционально, дефолт 8N1 без flow control (подходит для
|
||
# подавляющего большинства консольных портов):
|
||
# data_bits = "eight" # five | six | seven | eight
|
||
# parity = "none" # none | odd | even
|
||
# stop_bits = "one" # one | two
|
||
# flow_control = "none" # none | software | hardware
|
||
```
|
||
|
||
При подключении к serial-сессии в терминал печатается небольшая справка
|
||
(один раз, при открытии таба) — те самые нюансы, которые не видны нигде
|
||
в интерфейсе:
|
||
|
||
```
|
||
--- Serial-подключение: /dev/ttyUSB0 @ 115200 бод ---
|
||
- Порт занят другим приложением? Один порт — одно соединение одновременно.
|
||
- Linux: Permission denied при открытии? Добавь себя в группу dialout
|
||
(или uucp) и перелогинься: sudo usermod -aG dialout $USER
|
||
- data bits/parity/stop bits/flow control должны совпадать с устройством
|
||
на другом конце — по умолчанию тут 8N1 без flow control.
|
||
```
|
||
|
||
Ограничения, которые стоит держать в голове:
|
||
- **Занятость порта.** ОС не даёт двум процессам открыть один и тот же
|
||
serial-порт одновременно — если он уже занят (в другом терминале,
|
||
в MobaXterm, в `screen`/`minicom`), подключение вернёт ошибку открытия,
|
||
а не зависнет.
|
||
- **Права доступа только на Linux/macOS.** На Windows обычно ничего
|
||
дополнительно настраивать не нужно; на Linux без группы `dialout`
|
||
(`uucp` на некоторых дистрибутивах) — `Permission denied`.
|
||
- **Настройки порта должны совпадать с устройством.** Неверный baud
|
||
rate/parity — не ошибка подключения, а нечитаемая ерунда вместо текста
|
||
("mojibake"), потому что порт открывается успешно независимо от того,
|
||
правильно ли задан протокол данных поверх него.
|
||
- **Нет ни SFTP, ни строки состояния хоста** — оба используют SSH-специфику
|
||
(`open_sftp`/`run_command`), а serial-транспорт не переопределяет эти
|
||
методы трейта, так что дефолтные `Ok(None)` отключают их автоматически,
|
||
без специального кода в UI. Строка состояния просто покажет имя сессии,
|
||
как для Telnet.
|
||
- **`resize()` — no-op.** У физического провода нет понятия "размер
|
||
терминала" — устройство на другом конце просто пишет вслепую в свой
|
||
обычный vt100/ANSI, ресайз окна KomiTerm ни на что не влияет.
|
||
|
||
## Рендер терминала
|
||
|
||
`ui::render_terminal` проходит по сетке `alacritty_terminal` напрямую через
|
||
`grid[Line(row)][Column(col)]`, а не через `display_iter()` — так проще
|
||
гарантированно сохранить построчную раскладку. Поддержано: цвет текста/фона
|
||
(Named/Indexed/Spec — включая true color), bold/dim/italic/underline/
|
||
strikeout/inverse, курсор (инверсией текущей ячейки). Соседние ячейки с
|
||
одинаковым стилем схлопываются в один `Span`, чтобы не плодить по Span на
|
||
символ на длинных однотонных строках.
|
||
|
||
## Что реально работает
|
||
|
||
Ниже — история того, что уже прогонялось через настоящий `cargo build` на
|
||
реальной машине (не только в песочнице), а не только через сверку с
|
||
документацией. Первый прогон нашёл 7 реальных ошибок компиляции, второй —
|
||
ещё одну (сигнатура `Channel::data`). После этого **`cargo build --release`
|
||
прошёл полностью** — остались только предупреждения о неиспользуемом коде
|
||
(почищены). Живое подключение к серверу/устройству пока не проверялось.
|
||
|
||
**Что было сломано и как исправлено (см. git-историю/этот README для
|
||
контекста, если он у тебя есть):**
|
||
- `async-trait` не был объявлен в `Cargo.toml`, хотя использовался everywhere
|
||
для `Transport` — добавлено.
|
||
- `russh::keys::learn_known_hosts` не реэкспортируется на верхнем уровне в
|
||
версии 0.62.2 (там есть только `check_known_hosts`) — записываем строку в
|
||
`~/.ssh/known_hosts` сами, через `PublicKey::to_openssh()` + дозапись в
|
||
файл, без обращения к приватным/непереэкспортированным функциям russh.
|
||
- `authenticate_publickey`/`authenticate_password` возвращают `AuthResult`
|
||
(с методом `.success() -> bool`), а не голый `bool`, как предполагалось
|
||
изначально — поправлено.
|
||
- `Handle::window_change` в этой версии не существует — только
|
||
`Channel::window_change`. Раз уж пришлось трогать эту часть, канал
|
||
(shell/PTY) целиком переехал в отдельную задачу, которая мультиплексирует
|
||
входящие сообщения от сервера и команды (write/resize/close) от
|
||
`Transport` через `tokio::select!`, вместо того чтобы держать `ChannelId`
|
||
и слать всё через `Handle`.
|
||
- `alacritty_terminal::event::WindowSize` не реализует нужный трейт
|
||
`Dimensions` для `Term::new`/`Term::resize` (это разные вещи: `WindowSize`
|
||
— размер в пикселях для PTY, `Dimensions` — размер в колонках/строках
|
||
символьной сетки) — завели свою маленькую структуру `GridSize` в `term.rs`.
|
||
- `Handle::data(...)` возвращает `Result<(), Bytes>` (не пропущенные данные
|
||
возвращаются как есть, а не завёрнуты в нормальный `Error`) — обёрнуто
|
||
через `map_err`, чтобы `?` не спотыкался о недостающий `impl StdError`.
|
||
- (Второй раунд, после того как первый прогон дошёл до `Channel::data`)
|
||
— `Channel::data<R: AsyncRead + Unpin>(&self, data: R)` принимает
|
||
источник для чтения, а не готовый буфер/что-то конвертируемое через
|
||
`Into` (в отличие от `Handle::data`, у которого сигнатура другая) — вместо
|
||
`data.into()` теперь `channel.data(&data[..])`, `&[u8]` сам реализует
|
||
`AsyncRead`.
|
||
- (Третий раунд — уже не ошибка компиляции, а баг в поведении: экран при
|
||
запуске не очищался.) `enable_raw_mode()` включает построчный ввод, но
|
||
сам по себе не переключает терминал на отдельный "alternate screen
|
||
buffer" — тот самый механизм, из-за которого `vim`/`htop`/`less` рисуют
|
||
на чистом экране, а при выходе возвращают терминал ровно в то состояние,
|
||
в котором он был (включая историю команд шелла). Без него komiterm рисовал
|
||
свой UI поверх текущего скроллбэка — добавлены `enter_tui()`/`leave_tui()`
|
||
в `app.rs`, вызывающие `EnterAlternateScreen`/`LeaveAlternateScreen` из
|
||
`crossterm` парно с `enable_raw_mode()`/`disable_raw_mode()` на каждом
|
||
пути выхода из `run()` (их несколько — отмена выбора сессии, ошибка
|
||
подключения, обычное завершение).
|
||
- (Четвёртый раунд — **первое подключение к реальному SSH-серверу прошло
|
||
успешно**, но нашлось два визуальных бага.) Промпт passphrase от
|
||
`rpassword` (и TOFU-промпт known_hosts) пишет прямо в tty в обход
|
||
`ratatui` — `Terminal` про эту запись не знает и её не перерисовывает,
|
||
поэтому огрызок текста промпта оставался на экране поверх строки
|
||
состояния. Добавлен `terminal.clear()?` сразу после каждого
|
||
`transport::connect()`, случающегося уже внутри TUI (стартовый picker,
|
||
`F2`, `Enter` по сессии в фокусированной панели) — форсирует полную
|
||
перерисовку экрана с нуля вместо диффинга, который не видел "чужой" записи.
|
||
- (Пятый раунд, тот же скриншот.) Название сессии в боковой панели не
|
||
помещалось / было не видно целиком. Вероятная причина — "тяжёлые"
|
||
юникод-символы (● ➤ 📁 и т.п.), чья реальная ширина в конкретном шрифте
|
||
терминала может не совпадать с тем, что предполагает расчёт колонок в
|
||
`ratatui` (типичная проблема для символов вне базовой латиницы/кириллицы —
|
||
не все шрифты меряют их как честную одну ячейку). Заменил все декоративные
|
||
символы на простой ASCII (`*`, `>`, `[D]`, `_`, `-`) и увеличил
|
||
`SIDEBAR_WIDTH` с 24 до 30 колонок с запасом. Если после этого текст
|
||
всё ещё обрезается — это, вероятно, уже про шрифт терминала (кириллица
|
||
не всегда идеально моноширинная в некоторых шрифтах), а не про код.
|
||
- (Шестой раунд — не баг, а конфликт хоткея, плюс найденный заодно баг.)
|
||
Дефолт `quit` сменён с `ctrl+alt+c` на `f10` (конфликтовал с DE/WM у
|
||
пользователя; существующие `hotkeys.toml` это не трогает — старые
|
||
конфиги правятся вручную). Заодно нашлась дыра: если сессия завершалась
|
||
сама (`exit`/`Ctrl+D` в шелле), таб не закрывался, а следующая же клавиша
|
||
в нём валила **всё приложение** — `tab.transport.write(...).await?`
|
||
пробрасывал ошибку записи в мёртвый канал через весь `main_loop`. Теперь
|
||
канал каждого таба каждый кадр проверяется на `TryRecvError::Disconnected`
|
||
(не просто "пусто сейчас", а "закрыт навсегда") — таб с завершившейся
|
||
сессией закрывается автоматически, без валящего приложение падения.
|
||
|
||
**Что осталось (честные пробелы):**
|
||
|
||
1. **ssh.rs**: `AuthMethod::Agent` **временно отключён** — метод, которым
|
||
это было изначально реализовано (`authenticate_future` на `Handle`, взят
|
||
из старого примера в репозитории russh), в используемой версии (0.62.2)
|
||
не существует. Актуальную замену не удалось подтвердить без живой
|
||
компиляции — вместо третьей попытки угадать API вслепую, честно
|
||
возвращаю ошибку с указанием использовать `Key` или `Password`. Если
|
||
agent важен — дай знать, разберёмся предметно по фактической ошибке
|
||
компилятора, как в этот раз.
|
||
2. **ui.rs**: мастер создания сессии (`n` в списке сессий) — текстовые поля
|
||
только добавляют/стирают символы с конца строки, без перемещения курсора
|
||
внутри неё и без валидации на лету (например, нечисловой порт просто
|
||
тихо упадёт на дефолт при парсинге). Для тех же целей `komiterm profile
|
||
add` (создание credential-профиля) по-прежнему только через
|
||
`komiterm profile add` в терминале, не из этого мастера — они пока не
|
||
связаны. Из боковой панели в режиме списка сессий (`F1`, не `F2`) `n`
|
||
пока не работает — только из самого пикера при `F2`/начальном запуске.
|
||
3. **sftp.rs**: закачка привязана к UI, но download/upload по-прежнему
|
||
грузят файл целиком в память, нет rename/mkdir/delete в браузере (у
|
||
SftpSession методы есть) — см. TODO в конце `sftp.rs`.
|
||
4. **hoststats.rs**: только Linux (`/proc`) и только SSH; опрос блокирует
|
||
главный цикл на время round-trip. Не проверено на реальном хосте.
|
||
5. **serial.rs**: создать serial-сессию можно и через мастер (`n`), и руками
|
||
в `sessions.toml` — но нет `SerialPort::available_ports()` ни там, ни
|
||
там (списка реально подключённых портов, а не текстового поля вслепую).
|
||
Не проверено на реальном устройстве.
|
||
6. **telnet.rs**: отвечаем на DO/WILL только для ECHO и SUPPRESS-GO-AHEAD;
|
||
NAWS (передача размера окна) не реализован.
|
||
7. **sshconfig.rs**: `Match`-блоки и полноценные glob-паттерны не поддержаны.
|
||
8. **profiles.rs**: добавление профиля — простые вопросы в stdin, не форма
|
||
в ratatui (осознанно: это CLI-подкоманда вне raw-режима основного TUI).
|
||
9. Версии крейтов в `Cargo.toml` по-прежнему не проверялись сборкой **в
|
||
песочнице** (тот же древний `rustc 1.75`, упирается в edition2024 на
|
||
первом же транзитивном пакете) — но теперь у нас есть подтверждение с
|
||
реальной машины на актуальном тулчейне, и именно оттуда взяты все
|
||
исправления выше.
|
||
|
||
|
||
|
||
## Дальше по roadmap (после MVP)
|
||
|
||
- SSH-туннели (local/remote/dynamic port forwarding) — russh это умеет
|
||
- Логирование сессии в файл
|
||
- NAWS для telnet (передача размера окна серверу)
|
||
- rename/mkdir/delete в SFTP-браузере (методы у SftpSession уже есть)
|
||
|
||
## Сборка
|
||
|
||
```bash
|
||
cargo build --release
|
||
```
|
||
|
||
Сессии редактируются в `~/.config/komiterm/sessions.toml`, формат:
|
||
|
||
```toml
|
||
# Явный ключ прямо в сессии, host — реальный, ssh_config не трогаем
|
||
[[session]]
|
||
name = "myserver"
|
||
protocol = "ssh"
|
||
host = "example.com"
|
||
port = 22
|
||
user = "vladimir"
|
||
use_ssh_config = false
|
||
[session.auth]
|
||
method = "key"
|
||
path = "/home/vladimir/.ssh/id_ed25519"
|
||
passphrase = { source = "prompt" }
|
||
|
||
# host — алиас из ~/.ssh/config; HostName/User/IdentityFile подтянутся
|
||
# оттуда сами, если тут не переопределены
|
||
[[session]]
|
||
name = "prod-box"
|
||
protocol = "ssh"
|
||
host = "prod" # ищется в ~/.ssh/config как `Host prod`
|
||
|
||
# Логин/способ входа переиспользуются из именованного профиля
|
||
[[session]]
|
||
name = "staging-box"
|
||
protocol = "ssh"
|
||
host = "staging.internal"
|
||
credential = "work-key" # см. `komiterm profile add` / credentials.toml
|
||
|
||
[[session]]
|
||
name = "old-switch"
|
||
protocol = "telnet"
|
||
host = "192.168.1.1"
|
||
port = 23
|
||
```
|
||
|
||
Профили входа лежат отдельно в `~/.config/komiterm/credentials.toml` и
|
||
управляются через `komiterm profile add/list/remove` (см. раздел выше) —
|
||
руками их редактировать не обязательно.
|