Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 155 additions & 19 deletions internal/adapters/ui/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,39 @@ const (
ForwardModeForwardSSH = "Forward + SSH"
)

type connectionConfirmationAction int

const (
connectionNoAction connectionConfirmationAction = iota
connectionConfirm
connectionEdit
connectionCancel
)

func connectionActionForKey(event *tcell.EventKey) connectionConfirmationAction {
switch event.Key() {
case tcell.KeyEnter:
return connectionConfirm
case tcell.KeyEscape:
return connectionCancel
}
if commandKey(event) == 'e' {
return connectionEdit
}
return connectionNoAction
}

func connectionConfirmationMessage(alias string) string {
return fmt.Sprintf("You are about to connect to %q.\n\nTo edit this item instead, press E.", alias)
}

func (t *tui) handleGlobalKeys(event *tcell.EventKey) *tcell.EventKey {
// Don't handle global keys when search has focus
if t.app.GetFocus() == t.searchBar {
return event
}

switch event.Rune() {
switch commandKey(event) {
case 'q':
t.handleQuit()
return nil
Expand Down Expand Up @@ -95,13 +121,64 @@ func (t *tui) handleGlobalKeys(event *tcell.EventKey) *tcell.EventKey {
}

if event.Key() == tcell.KeyEnter {
t.handleServerConnect()
if server, ok := t.serverList.GetSelectedServer(); ok {
t.showConnectionConfirmModal(server)
}
return nil
}

return event
}

// commandKey normalizes runes only in command contexts. Text inputs, search,
// and dropdown filtering intentionally consume the original event unchanged.
func commandKey(event *tcell.EventKey) rune {
return normalizeGlobalHotkey(event.Rune())
}

// normalizeGlobalHotkey preserves command semantics for ASCII keys while
// keeping all other runes untouched. Terminal applications receive the
// resulting rune for printable keys, not a portable physical key code, so
// command handling must not guess a user's keyboard layout.
func normalizeGlobalHotkey(key rune) rune {
switch key {
case 'q', 'Q':
return 'q'
case '/':
return '/'
case 'a', 'A':
return 'a'
case 'e', 'E':
return 'e'
case 'd', 'D':
return 'd'
case 'p', 'P':
return 'p'
case 's':
return 's'
case 'S':
return 'S'
case 'c', 'C':
return 'c'
case 'g', 'G':
return 'g'
case 'r', 'R':
return 'r'
case 't', 'T':
return 't'
case 'f', 'F':
return 'f'
case 'x', 'X':
return 'x'
case 'j', 'J':
return 'j'
case 'k', 'K':
return 'k'
default:
return key
}
}

func (t *tui) handleQuit() {
t.app.Stop()
}
Expand Down Expand Up @@ -220,14 +297,11 @@ func (t *tui) handleReturnToSearch() {
}
}

func (t *tui) handleServerConnect() {
if server, ok := t.serverList.GetSelectedServer(); ok {

t.app.Suspend(func() {
_ = t.serverService.SSH(server.Alias)
})
t.refreshServerList()
}
func (t *tui) handleServerConnect(server domain.Server) {
t.app.Suspend(func() {
_ = t.serverService.SSH(server.Alias)
})
t.refreshServerList()
}

func (t *tui) handleServerSelectionChange(server domain.Server) {
Expand All @@ -245,15 +319,19 @@ func (t *tui) handleServerAdd() {

func (t *tui) handleServerEdit() {
if server, ok := t.serverList.GetSelectedServer(); ok {
form := NewServerForm(ServerFormEdit, &server).
SetApp(t.app).
SetVersionInfo(t.version, t.commit).
OnSave(t.handleServerSave).
OnCancel(t.handleFormCancel)
t.app.SetRoot(form, true)
t.showServerEditForm(server)
}
}

func (t *tui) showServerEditForm(server domain.Server) {
form := NewServerForm(ServerFormEdit, &server).
SetApp(t.app).
SetVersionInfo(t.version, t.commit).
OnSave(t.handleServerSave).
OnCancel(t.handleFormCancel)
t.app.SetRoot(form, true)
}

func (t *tui) handleServerSave(server domain.Server, original *domain.Server) {
var err error
if original != nil {
Expand Down Expand Up @@ -351,6 +429,64 @@ func (t *tui) handleRefreshBackground() {
// UI Display Functions (show UI elements/modals)
// =============================================================================

func (t *tui) showConnectionConfirmModal(server domain.Server) {
modal, pages := t.newConnectionConfirmationOverlay(server)
t.app.SetRoot(pages, true)
t.app.SetFocus(modal)
}

func (t *tui) newConnectionConfirmationOverlay(server domain.Server) (*tview.Modal, *tview.Pages) {
modal := tview.NewModal().
SetText(tview.Escape(connectionConfirmationMessage(server.Alias))).
AddButtons([]string{"Connect"}).
SetBackgroundColor(tcell.Color235).
SetTextColor(tcell.Color252).
SetButtonStyle(tcell.StyleDefault.Foreground(tcell.Color252).Background(tcell.Color232)).
SetButtonActivatedStyle(tcell.StyleDefault.Foreground(tcell.Color232).Background(tcell.Color252)).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
if buttonIndex == 0 {
t.returnToMain()
t.handleServerConnect(server)
return
}
t.returnToMain()
t.app.SetFocus(t.serverList)
})
modal.SetBorderColor(tcell.Color238)
modal.SetTitle(" Confirm Connection ")
modal.SetTitleAlign(tview.AlignCenter)
modal.SetTitleColor(tcell.Color250)

modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch connectionActionForKey(event) {
case connectionConfirm:
return event
case connectionEdit:
t.showServerEditForm(server)
return nil
case connectionCancel:
t.returnToMain()
t.app.SetFocus(t.serverList)
return nil
default:
return event
}
})

modal.SetFocus(0)

pages := tview.NewPages().
AddPage("main", t.root, true, true).
AddPage("connection-confirmation", modal, true, true)
pages.SetMouseCapture(func(action tview.MouseAction, event *tcell.EventMouse) (tview.MouseAction, *tcell.EventMouse) {
if modal.InRect(event.Position()) {
return action, event
}
return tview.MouseConsumed, nil
})
return modal, pages
}

func (t *tui) showDeleteConfirmModal(server domain.Server) {
msg := fmt.Sprintf("Delete server %s (%s@%s:%d)?\n\nThis action cannot be undone.",
server.Alias, server.User, server.Host, server.Port)
Expand All @@ -368,12 +504,12 @@ func (t *tui) showDeleteConfirmModal(server domain.Server) {

// Add keyboard shortcuts for the modal
modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Rune() {
case 'c', 'C':
switch commandKey(event) {
case 'c':
// Cancel
t.handleModalClose()
return nil
case 'd', 'D':
case 'd':
// Delete
_ = t.serverService.DeleteServer(server)
t.refreshServerList()
Expand Down
Loading
Loading