Fix: universal hotkeys using Ctrl+combinations

- All hotkeys now use Ctrl+letter (Ctrl+A, Ctrl+E, Ctrl+D, Ctrl+T, Ctrl+F, Ctrl+Q)
- Works on ANY keyboard layout (EN, FR, DE, RU, etc.)
- Ctrl+letter generates same control character regardless of layout
- Updated help text and README

Rationale: Ctrl+A always produces byte 0x01 (SOH) regardless of whether
the physical key is QWERTY 'a', AZERTY 'a', or ЙЦУКЕН 'ф'. This is the
standard approach used by vim, tmux, ranger, etc.
This commit is contained in:
mirivlad 2026-05-26 16:41:42 +08:00
parent 3b509d5d2e
commit d1bb216d82
2 changed files with 55 additions and 58 deletions

View File

@ -82,17 +82,17 @@ sshkeeper ssh-config install-include
sshkeeper
```
Клавиши (раскладка не важна, работает и на русской):
Клавиши (работают на любой раскладке — используются Ctrl+комбинации):
| Клавиша | Действие |
|---------|----------|
| Enter | Подключиться к серверу |
| q / й | Выход |
| a / ф | Добавить сервер |
| e / у | Редактировать сервер |
| d / в | Удалить сервер |
| t / е | Проверить подключение |
| / | Поиск |
| Ctrl+A | Добавить сервер |
| Ctrl+E | Редактировать сервер |
| Ctrl+D | Удалить сервер |
| Ctrl+T | Проверить подключение |
| Ctrl+F | Поиск |
| Ctrl+Q / Ctrl+C | Выход |
В форме добавления/редактирования:

View File

@ -222,35 +222,35 @@ func (m *tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func matchKeys(msg tea.KeyMsg, en, ru string) bool {
if len(msg.Runes) != 1 {
return false
func (m *tuiModel) updateList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.Type {
case tea.KeyEnter:
// Connect to selected server
if item, ok := m.list.SelectedItem().(serverItem); ok {
return m, func() tea.Msg {
return connectRequestMsg{server: item.server}
}
r := msg.Runes[0]
return r == []rune(en)[0] || r == []rune(ru)[0]
}
func (m *tuiModel) updateList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// Check key by runes (layout-independent)
if msg.Type == tea.KeyRunes {
switch {
case matchKeys(msg, "q", "й"):
case tea.KeyCtrlC, tea.KeyCtrlQ:
return m, tea.Quit
case matchKeys(msg, "/", "?"):
m.screen = screenSearch
m.searchInput.Focus()
return m, nil
case matchKeys(msg, "a", "ф"):
case tea.KeyCtrlA:
// Add server
m.form = newFormModel(m.width, m.height)
m.screen = screenForm
return m, nil
case matchKeys(msg, "e", "у"):
case tea.KeyCtrlE:
// Edit selected server
if item, ok := m.list.SelectedItem().(serverItem); ok {
m.form = newEditFormModel(item.server, m.width, m.height)
m.screen = screenForm
}
return m, nil
case matchKeys(msg, "d", "в"):
case tea.KeyCtrlD:
// Delete selected server
if item, ok := m.list.SelectedItem().(serverItem); ok {
return m, func() tea.Msg {
err := DeleteServer(item.server.Alias)
@ -261,25 +261,22 @@ func (m *tuiModel) updateList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return serversLoadedMsg{servers: servers, err: err}
}
}
case matchKeys(msg, "t", "е"):
case tea.KeyCtrlT:
// Test connection
if item, ok := m.list.SelectedItem().(serverItem); ok {
return m, func() tea.Msg {
ok, testErr := TestConnection(item.server)
return testDoneMsg{ok: ok, err: testErr}
}
}
}
}
switch msg.Type {
case tea.KeyEnter:
if item, ok := m.list.SelectedItem().(serverItem); ok {
return m, func() tea.Msg {
return connectRequestMsg{server: item.server}
}
}
case tea.KeyCtrlC:
return m, tea.Quit
case tea.KeyCtrlF, tea.KeyCtrlS:
// Search
m.screen = screenSearch
m.searchInput.Focus()
return m, nil
default:
var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
@ -342,11 +339,11 @@ func (m *tuiModel) View() string {
case screenList:
b.WriteString(m.list.View())
b.WriteString("\n")
b.WriteString(helpStyle.Render("Enter connect | a add | e edit | d delete | t test | / search | q quit"))
b.WriteString(helpStyle.Render("Enter connect | Ctrl+A add | Ctrl+E edit | Ctrl+D del | Ctrl+T test | Ctrl+F search | Ctrl+Q quit"))
case screenSearch:
b.WriteString("Search: " + m.searchInput.View() + "\n")
b.WriteString(helpStyle.Render("Enter search | Esc cancel"))
b.WriteString(helpStyle.Render("Type to search | Enter confirm | Esc cancel"))
case screenForm:
b.WriteString(m.form.View())