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:
parent
3b509d5d2e
commit
d1bb216d82
14
README.md
14
README.md
|
|
@ -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 | Выход |
|
||||||
|
|
||||||
В форме добавления/редактирования:
|
В форме добавления/редактирования:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -222,64 +222,61 @@ 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)
|
|
||||||
if msg.Type == tea.KeyRunes {
|
|
||||||
switch {
|
|
||||||
case matchKeys(msg, "q", "й"):
|
|
||||||
return m, tea.Quit
|
|
||||||
case matchKeys(msg, "/", "?"):
|
|
||||||
m.screen = screenSearch
|
|
||||||
m.searchInput.Focus()
|
|
||||||
return m, nil
|
|
||||||
case matchKeys(msg, "a", "ф"):
|
|
||||||
m.form = newFormModel(m.width, m.height)
|
|
||||||
m.screen = screenForm
|
|
||||||
return m, nil
|
|
||||||
case matchKeys(msg, "e", "у"):
|
|
||||||
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", "в"):
|
|
||||||
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}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case tea.KeyEnter:
|
case tea.KeyEnter:
|
||||||
|
// Connect to 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 {
|
||||||
return connectRequestMsg{server: item.server}
|
return connectRequestMsg{server: item.server}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case tea.KeyCtrlC:
|
|
||||||
|
case tea.KeyCtrlC, tea.KeyCtrlQ:
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case tea.KeyCtrlA:
|
||||||
|
// Add server
|
||||||
|
m.form = newFormModel(m.width, m.height)
|
||||||
|
m.screen = screenForm
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
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 tea.KeyCtrlD:
|
||||||
|
// Delete selected server
|
||||||
|
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 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}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case tea.KeyCtrlF, tea.KeyCtrlS:
|
||||||
|
// Search
|
||||||
|
m.screen = screenSearch
|
||||||
|
m.searchInput.Focus()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
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())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue