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 sshkeeper
``` ```
Клавиши (раскладка не важна, работает и на русской): Клавиши (работают на любой раскладке — используются Ctrl+комбинации):
| Клавиша | Действие | | Клавиша | Действие |
|---------|----------| |---------|----------|
| Enter | Подключиться к серверу | | Enter | Подключиться к серверу |
| q / й | Выход | | Ctrl+A | Добавить сервер |
| a / ф | Добавить сервер | | Ctrl+E | Редактировать сервер |
| e / у | Редактировать сервер | | Ctrl+D | Удалить сервер |
| d / в | Удалить сервер | | Ctrl+T | Проверить подключение |
| 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 return m, nil
} }
func matchKeys(msg tea.KeyMsg, en, ru string) bool {
if len(msg.Runes) != 1 {
return false
}
r := msg.Runes[0]
return r == []rune(en)[0] || r == []rune(ru)[0]
}
func (m *tuiModel) updateList(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *tuiModel) updateList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// Check key by runes (layout-independent) switch msg.Type {
if msg.Type == tea.KeyRunes { case tea.KeyEnter:
switch { // Connect to selected server
case matchKeys(msg, "q", "й"): if item, ok := m.list.SelectedItem().(serverItem); ok {
return m, func() tea.Msg {
return connectRequestMsg{server: item.server}
}
}
case tea.KeyCtrlC, tea.KeyCtrlQ:
return m, tea.Quit return m, tea.Quit
case matchKeys(msg, "/", "?"):
m.screen = screenSearch case tea.KeyCtrlA:
m.searchInput.Focus() // Add server
return m, nil
case matchKeys(msg, "a", "ф"):
m.form = newFormModel(m.width, m.height) m.form = newFormModel(m.width, m.height)
m.screen = screenForm m.screen = screenForm
return m, nil return m, nil
case matchKeys(msg, "e", "у"):
case tea.KeyCtrlE:
// Edit selected server
if item, ok := m.list.SelectedItem().(serverItem); ok { if item, ok := m.list.SelectedItem().(serverItem); ok {
m.form = newEditFormModel(item.server, m.width, m.height) m.form = newEditFormModel(item.server, m.width, m.height)
m.screen = screenForm m.screen = screenForm
} }
return m, nil return m, nil
case matchKeys(msg, "d", "в"):
case tea.KeyCtrlD:
// Delete selected server
if item, ok := m.list.SelectedItem().(serverItem); ok { if item, ok := m.list.SelectedItem().(serverItem); ok {
return m, func() tea.Msg { return m, func() tea.Msg {
err := DeleteServer(item.server.Alias) 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} return serversLoadedMsg{servers: servers, err: err}
} }
} }
case matchKeys(msg, "t", "е"):
case tea.KeyCtrlT:
// Test connection
if item, ok := m.list.SelectedItem().(serverItem); ok { if item, ok := m.list.SelectedItem().(serverItem); ok {
return m, func() tea.Msg { return m, func() tea.Msg {
ok, testErr := TestConnection(item.server) ok, testErr := TestConnection(item.server)
return testDoneMsg{ok: ok, err: testErr} return testDoneMsg{ok: ok, err: testErr}
} }
} }
}
}
switch msg.Type { case tea.KeyCtrlF, tea.KeyCtrlS:
case tea.KeyEnter: // Search
if item, ok := m.list.SelectedItem().(serverItem); ok { m.screen = screenSearch
return m, func() tea.Msg { m.searchInput.Focus()
return connectRequestMsg{server: item.server} return m, nil
}
}
case tea.KeyCtrlC:
return m, tea.Quit
default: default:
var cmd tea.Cmd var cmd tea.Cmd
m.list, cmd = m.list.Update(msg) m.list, cmd = m.list.Update(msg)
@ -342,11 +339,11 @@ func (m *tuiModel) View() string {
case screenList: case screenList:
b.WriteString(m.list.View()) b.WriteString(m.list.View())
b.WriteString("\n") 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: case screenSearch:
b.WriteString("Search: " + m.searchInput.View() + "\n") 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: case screenForm:
b.WriteString(m.form.View()) b.WriteString(m.form.View())