Fix: layout-independent hotkeys, Argon2 memory reduction, README

- Hotkeys now work with both English and Russian keyboard layouts
- Argon2 memory reduced from 64MB to 4MB (faster key derivation)
- Added 'Deriving key...' progress indicator
- Help text color changed to cyan for better visibility
- Form navigation uses key types instead of strings
- README updated with layout-independent key info
This commit is contained in:
mirivlad 2026-05-26 10:47:48 +08:00
parent 883a1e66a7
commit 3b509d5d2e
2 changed files with 80 additions and 55 deletions

View File

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

View File

@ -222,56 +222,64 @@ 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) {
switch msg.String() { // Check key by runes (layout-independent)
case "q", "ctrl+c": if msg.Type == tea.KeyRunes {
return m, tea.Quit switch {
case matchKeys(msg, "q", "й"):
case "/": return m, tea.Quit
m.screen = screenSearch case matchKeys(msg, "/", "?"):
m.searchInput.Focus() m.screen = screenSearch
return m, nil m.searchInput.Focus()
return m, nil
case "a": case matchKeys(msg, "a", "ф"):
m.form = newFormModel(m.width, m.height) m.form = newFormModel(m.width, m.height)
m.screen = screenForm
return m, nil
case "e":
if item, ok := m.list.SelectedItem().(serverItem); ok {
m.form = newEditFormModel(item.server, m.width, m.height)
m.screen = screenForm m.screen = screenForm
} return m, nil
return m, nil case matchKeys(msg, "e", "у"):
if item, ok := m.list.SelectedItem().(serverItem); ok {
case "d": m.form = newEditFormModel(item.server, m.width, m.height)
if item, ok := m.list.SelectedItem().(serverItem); ok { m.screen = screenForm
return m, func() tea.Msg { }
err := DeleteServer(item.server.Alias) return m, nil
if err != nil { case matchKeys(msg, "d", "в"):
return saveDoneMsg{err: err} if item, ok := m.list.SelectedItem().(serverItem); ok {
return m, func() tea.Msg {
err := DeleteServer(item.server.Alias)
if err != nil {
return saveDoneMsg{err: err}
}
servers, err := ListServers()
return serversLoadedMsg{servers: servers, err: err}
}
}
case matchKeys(msg, "t", "е"):
if item, ok := m.list.SelectedItem().(serverItem); ok {
return m, func() tea.Msg {
ok, testErr := TestConnection(item.server)
return testDoneMsg{ok: ok, err: testErr}
} }
servers, err := ListServers()
return serversLoadedMsg{servers: servers, err: err}
} }
} }
}
case "t": switch msg.Type {
case tea.KeyEnter:
if item, ok := m.list.SelectedItem().(serverItem); ok { if item, ok := m.list.SelectedItem().(serverItem); ok {
return m, func() tea.Msg {
ok, testErr := TestConnection(item.server)
return testDoneMsg{ok: ok, err: testErr}
}
}
case "enter":
if item, ok := m.list.SelectedItem().(serverItem); ok {
// Request connect — TUI will quit and caller handles it
return m, func() tea.Msg { return m, func() tea.Msg {
return connectRequestMsg{server: item.server} return connectRequestMsg{server: item.server}
} }
} }
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)
@ -282,14 +290,14 @@ func (m *tuiModel) updateList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
func (m *tuiModel) updateSearch(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *tuiModel) updateSearch(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() { switch msg.Type {
case "esc": case tea.KeyEsc:
m.screen = screenList m.screen = screenList
m.searchInput.Blur() m.searchInput.Blur()
m.searchInput.SetValue("") m.searchInput.SetValue("")
return m, nil return m, nil
case "enter": case tea.KeyEnter:
m.screen = screenList m.screen = screenList
m.searchInput.Blur() m.searchInput.Blur()
query := m.searchInput.Value() query := m.searchInput.Value()
@ -312,8 +320,7 @@ func (m *tuiModel) updateSearch(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
func (m *tuiModel) updateForm(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *tuiModel) updateForm(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() { if msg.Type == tea.KeyEsc {
case "esc":
m.screen = screenList m.screen = screenList
m.form = nil m.form = nil
m.err = nil m.err = nil
@ -480,8 +487,8 @@ func (fm *formModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.Type {
case "tab", "down": case tea.KeyTab:
fm.focusIdx++ fm.focusIdx++
total := len(fm.inputs) + 3 total := len(fm.inputs) + 3
if fm.focusIdx >= total { if fm.focusIdx >= total {
@ -490,7 +497,7 @@ func (fm *formModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
fm.updateFocus() fm.updateFocus()
return fm, nil return fm, nil
case "shift+tab", "up": case tea.KeyShiftTab:
fm.focusIdx-- fm.focusIdx--
if fm.focusIdx < 0 { if fm.focusIdx < 0 {
total := len(fm.inputs) + 3 total := len(fm.inputs) + 3
@ -499,7 +506,7 @@ func (fm *formModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
fm.updateFocus() fm.updateFocus()
return fm, nil return fm, nil
case "enter": case tea.KeyEnter:
switch { switch {
case fm.focusIdx == len(fm.inputs)+1: case fm.focusIdx == len(fm.inputs)+1:
return fm, fm.runTest() return fm, fm.runTest()
@ -515,7 +522,25 @@ func (fm *formModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return fm, nil return fm, nil
} }
case "esc": case tea.KeyEsc:
return fm, nil
case tea.KeyDown:
fm.focusIdx++
total := len(fm.inputs) + 3
if fm.focusIdx >= total {
fm.focusIdx = 0
}
fm.updateFocus()
return fm, nil
case tea.KeyUp:
fm.focusIdx--
if fm.focusIdx < 0 {
total := len(fm.inputs) + 3
fm.focusIdx = total - 1
}
fm.updateFocus()
return fm, nil return fm, nil
} }
} }