diff --git a/cmd/goto/main.go b/cmd/goto/main.go index 2d0bb8a..17919e3 100644 --- a/cmd/goto/main.go +++ b/cmd/goto/main.go @@ -61,6 +61,7 @@ func main() { utils.LogAndCloseApp(lgr, constant.AppExitCodeError, logMessage) } + // Handle application shutdown lgr.Debug("[MAIN] Save application state") if err = st.Persist(); err != nil { logMessage := fmt.Sprintf("[MAIN] Can't save application state before closing: %v", err) diff --git a/internal/ui/component/grouplist/grouplist.go b/internal/ui/component/grouplist/grouplist.go index ce78a12..4186114 100644 --- a/internal/ui/component/grouplist/grouplist.go +++ b/internal/ui/component/grouplist/grouplist.go @@ -46,7 +46,7 @@ func New(_ context.Context, repo storage.HostStorage, appState *state.State, log delegate.SetSpacing(0) model := list.New(listItems, delegate, 0, 0) - model.SetFilteringEnabled(false) + model.DisableQuitKeybindings() // We don't want to quit the app from this view. // Setup model styles. model.Styles = styles.styleList @@ -65,7 +65,6 @@ func New(_ context.Context, repo storage.HostStorage, appState *state.State, log } m.Title = "select group" - m.SetShowStatusBar(false) return &m } @@ -74,6 +73,7 @@ func (m *Model) Init() tea.Cmd { return nil } func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd + var cmds []tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: @@ -83,13 +83,16 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case tea.KeyMsg: cmd = m.handleKeyboardEvent(msg) - return m, cmd - case message.OpenViewSelectGroup: + cmds = append(cmds, cmd) + case message.ViewGroupListOpen: return m, m.loadItems() } m.Model, cmd = m.Model.Update(msg) - return m, cmd + // Only calculate status bar visibility AFTER the model is updated. + m.SetShowStatusBar(m.FilterState() != list.Unfiltered) + + return m, tea.Batch(append(cmds, cmd)...) } func (m *Model) View() string { @@ -97,35 +100,58 @@ func (m *Model) View() string { } func (m *Model) handleKeyboardEvent(msg tea.KeyMsg) tea.Cmd { - var cmd tea.Cmd - - //exhaustive:ignore + //exhaustive:ignore // Handle only specific keys, other events are handled by the list model. switch msg.Type { case tea.KeyEscape: - m.logger.Debug("[UI] Escape key. Deselect group and exit from group list view.") - return tea.Sequence( - // If group view is shown and user presses ESC, we should - // deselect the group view and then show the full host list. - message.TeaCmd(message.GroupSelected{Name: ""}), - message.TeaCmd(message.CloseViewSelectGroup{}), - ) + return m.handleEscapeKey() case tea.KeyEnter: - selected := m.SelectedItem().(ListItemHostGroup).Title() //nolint:errcheck // SelectedItem always returns ListItemHostGroup - selected = strings.TrimSpace(selected) + return m.handleEnterKey() + } - if selected == noGroupSelected { - selected = "" - } + return nil +} - m.logger.Debug("[UI] Enter key. Select group '%s' and exit from group list view.", selected) - return tea.Sequence( - message.TeaCmd(message.GroupSelected{Name: selected}), - message.TeaCmd(message.CloseViewSelectGroup{}), - ) +func (m *Model) handleEscapeKey() tea.Cmd { + // If model is in filter mode and press ESC, just disable filtering. + if m.FilterState() == list.Filtering || m.FilterState() == list.FilterApplied { + m.logger.Debug("[UI] Escape key. Deactivate filter in group list view.") + return nil } - m.Model, cmd = m.Model.Update(msg) - return cmd + // If group view is shown and user presses ESC, we should + // deselect the group view and then show the full host list. + m.logger.Debug("[UI] Escape key. Deselect group and exit from group list view.") + return tea.Sequence( + message.TeaCmd(message.GroupSelect{Name: ""}), + message.TeaCmd(message.ViewGroupListClose{}), + ) +} + +func (m *Model) handleEnterKey() tea.Cmd { + // If filter is active, by default pressing Enter just selects + // the first item from the list of filtered items. Prevent that. + if m.FilterState() == list.Filtering { + m.logger.Debug("[UI] Enter key. Select item in group list view.") + return nil + } + + if m.SelectedItem() == nil { + m.logger.Debug("[UI] Enter key. No group selected, nothing to do.") + return nil + } + + selected := m.SelectedItem().(ListItemHostGroup).Title() //nolint:errcheck // SelectedItem always returns ListItemHostGroup + selected = strings.TrimSpace(selected) + + if selected == noGroupSelected { + selected = "" + } + + m.logger.Debug("[UI] Enter key. Select group '%s' and exit from group list view.", selected) + return tea.Sequence( + message.TeaCmd(message.GroupSelect{Name: selected}), + message.TeaCmd(message.ViewGroupListClose{}), + ) } func (m *Model) loadItems() tea.Cmd { diff --git a/internal/ui/component/grouplist/grouplist_test.go b/internal/ui/component/grouplist/grouplist_test.go index 639d603..19fe4ee 100644 --- a/internal/ui/component/grouplist/grouplist_test.go +++ b/internal/ui/component/grouplist/grouplist_test.go @@ -15,9 +15,10 @@ import ( func TestNew(t *testing.T) { model := NewMockGroupModel(false) - require.False(t, model.FilteringEnabled()) - require.False(t, model.ShowStatusBar()) - require.False(t, model.FilteringEnabled()) + require.True(t, model.FilteringEnabled()) + // Quit app keys is disabled + require.False(t, model.KeyMap.Quit.Enabled()) + require.False(t, model.KeyMap.ForceQuit.Enabled()) require.Equal(t, "select group", model.Title) } @@ -40,58 +41,134 @@ func TestUpdate(t *testing.T) { // Loads hosts when the form is shown listModel = NewMockGroupModel(false) - listModel.Update(message.OpenViewSelectGroup{}) + listModel.Update(message.ViewGroupListOpen{}) // Selected group is "no group" require.Equal(t, noGroupSelected, listModel.SelectedItem().(ListItemHostGroup).Title()) } -func TestHandleKeyboardEvent_Enter(t *testing.T) { - // Can Select group +func Test_handleKeyboardEvent(t *testing.T) { listModel := NewMockGroupModel(false) listModel.loadItems() - require.Equal(t, noGroupSelected, listModel.SelectedItem().(ListItemHostGroup).Title()) - // Select Group 1 - listModel.Update(tea.KeyMsg{Type: tea.KeyDown}) - _, cmd := listModel.Update(tea.KeyMsg{Type: tea.KeyEnter}) + tests := []struct { + name string + keyMsg tea.KeyMsg + expectCmd bool + }{ + { + name: "Can handle Enter key", + keyMsg: tea.KeyMsg{Type: tea.KeyEnter}, + expectCmd: true, + }, + { + name: "Can handle Esc key", + keyMsg: tea.KeyMsg{Type: tea.KeyEsc}, + expectCmd: true, + }, + { + name: "Unhandled key returns nil cmd", + keyMsg: tea.KeyMsg{Type: tea.KeyDown}, + expectCmd: false, + }, + } - var actualMsgs []tea.Msg - testutils.CmdToMessage(cmd, &actualMsgs) - expectedMsgs := []tea.Msg{ - message.GroupSelected{Name: "Group 1"}, - message.CloseViewSelectGroup{}, + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := listModel.handleKeyboardEvent(tt.keyMsg) + if tt.expectCmd { + require.NotNil(t, cmd) + } else { + require.Nil(t, cmd) + } + }) + } +} + +func Test_handleEnterKey(t *testing.T) { + tests := []struct { + name string + group string + itemIndex int + expectedMsgs []tea.Msg + }{ + { + name: "Can handle 'no group' selection", + group: noGroupSelected, + itemIndex: 0, + expectedMsgs: []tea.Msg{ + message.GroupSelect{Name: ""}, + message.ViewGroupListClose{}, + }, + }, + { + name: "Can handle Group 1 selection ", + group: "Group 1", + itemIndex: 1, + expectedMsgs: []tea.Msg{ + message.GroupSelect{Name: "Group 1"}, + message.ViewGroupListClose{}, + }, + }, + { + name: "Can handle empty group list ", + group: "", + itemIndex: 55, // Out of range index, selected item will be nil + expectedMsgs: nil, + }, } - require.ElementsMatch(t, expectedMsgs, actualMsgs) - require.Equal(t, "Group 1", listModel.SelectedItem().(ListItemHostGroup).Title()) + listModel := NewMockGroupModel(false) + listModel.loadItems() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + listModel.Select(tt.itemIndex) + cmd := listModel.handleEnterKey() + + var actualMsgs []tea.Msg + testutils.CmdToMessage(cmd, &actualMsgs) + require.ElementsMatch(t, tt.expectedMsgs, actualMsgs) + if listModel.SelectedItem() != nil { + require.Equal(t, tt.group, listModel.SelectedItem().(ListItemHostGroup).Title()) + } + }) + } } -func TestHandleKeyboardEvent_Esc(t *testing.T) { - // Can handle Esc key +func Test_handleEnterKey_WhenFiltering(t *testing.T) { + // When pressing Enter key while filtering, it should return nil cmd, as we do not want to select + // the first item in the list, but instead let user to select an item using Up/Down keys. listModel := NewMockGroupModel(false) listModel.loadItems() - require.Equal(t, noGroupSelected, listModel.SelectedItem().(ListItemHostGroup).Title()) + // Put the list in filter mode + listModel.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}) - // Select Group 1 - listModel.Update(tea.KeyMsg{Type: tea.KeyDown}) - listModel.Update(tea.KeyMsg{Type: tea.KeyEnter}) - require.Equal(t, "Group 1", listModel.SelectedItem().(ListItemHostGroup).Title()) + require.True(t, listModel.SettingFilter()) // Activate filter mode + _, cmd := listModel.Update(tea.KeyMsg{Type: tea.KeyEnter}) + require.Nil(t, cmd) +} - // Now press Escape key and ensure that Model will send group unselect message and closed the form +func Test_handleEscapeKey(t *testing.T) { + // Test case 1: Press escape in filter mode + // Can handle Esc key + listModel := NewMockGroupModel(false) + listModel.loadItems() + // Put the list in filter mode + listModel.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("/")}) + require.True(t, listModel.SettingFilter()) // Verify that filter mode is activate _, cmd := listModel.Update(tea.KeyMsg{Type: tea.KeyEsc}) + require.Nil(t, cmd) + + // Test case 2: Press escape when not in filter mode - it must deselect the group and close the form + require.False(t, listModel.SettingFilter()) // Verify that we're not in filter mode after the first test case + _, cmd = listModel.Update(tea.KeyMsg{Type: tea.KeyEsc}) var actualMsgs []tea.Msg testutils.CmdToMessage(cmd, &actualMsgs) - - expectedMsgs := []tea.Msg{ - message.GroupSelected{Name: ""}, - message.CloseViewSelectGroup{}, - } - - // Note that though, Escape key was pressed, the group remains selected inside the group list component. - // This is by design - if user opens group list dialog again, previously selected group will be focused. - require.Equal(t, "Group 1", listModel.SelectedItem().(ListItemHostGroup).Title()) - require.ElementsMatch(t, expectedMsgs, actualMsgs) + require.ElementsMatch(t, actualMsgs, []tea.Msg{ + message.GroupSelect{Name: ""}, + message.ViewGroupListClose{}, + }) } func TestLoadItems(t *testing.T) { diff --git a/internal/ui/component/hostedit/hostedit.go b/internal/ui/component/hostedit/hostedit.go index ceebcad..2869104 100644 --- a/internal/ui/component/hostedit/hostedit.go +++ b/internal/ui/component/hostedit/hostedit.go @@ -227,7 +227,7 @@ func (m *EditModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.SetContent(m.inputsView()) case debouncedMessage: cmd = m.handleDebouncedMessage(msg) - case message.HostSSHConfigLoaded: + case message.HostSSHConfigLoadComplete: m.host.SSHHostConfig = &msg.Config m.updateInputFields() m.viewport.SetContent(m.inputsView()) @@ -262,7 +262,7 @@ func (m *EditModel) handleKeyboardEvent(msg tea.KeyMsg) tea.Cmd { switch { case key.Matches(msg, m.keyMap.Discard): m.logger.Info("[UI] Discard changes for host id: %v", m.host.ID) - return message.TeaCmd(message.CloseViewHostEdit{}) + return message.TeaCmd(message.ViewHostEditClose{}) case m.host.IsReadOnly(): m.logger.Debug("[UI] Received a key event. Cannot modify a readonly host.") return m.displayNotificationMsg("host loaded from SSH config is readonly") @@ -340,14 +340,14 @@ func (m *EditModel) save(_ tea.Msg) tea.Cmd { cmd = message.TeaCmd(message.ErrorOccurred{Err: err}) } else { cmd = lo.Ternary(m.isNewHost, - message.TeaCmd(message.HostCreated{Host: host}), - message.TeaCmd(message.HostUpdated{Host: host})) + message.TeaCmd(message.HostCreate{Host: host}), + message.TeaCmd(message.HostUpdate{Host: host})) } return tea.Sequence( - message.TeaCmd(message.CloseViewHostEdit{}), + message.TeaCmd(message.ViewHostEditClose{}), // Order matters here! That's why we use tea.Sequence instead of tea.Batch. - message.TeaCmd(message.HostSelected{HostID: host.ID}), + message.TeaCmd(message.HostSelect{HostID: host.ID}), cmd, ) } diff --git a/internal/ui/component/hostedit/hostedit_test.go b/internal/ui/component/hostedit/hostedit_test.go index 9babdb2..035e358 100644 --- a/internal/ui/component/hostedit/hostedit_test.go +++ b/internal/ui/component/hostedit/hostedit_test.go @@ -117,8 +117,8 @@ func TestSave(t *testing.T) { var dst []tea.Msg testutils.CmdToMessage(messageSequence, &dst) - require.Contains(t, dst, message.CloseViewHostEdit{}) - require.Contains(t, dst, message.HostSelected{HostID: 0}) + require.Contains(t, dst, message.ViewHostEditClose{}) + require.Contains(t, dst, message.HostSelect{HostID: 0}) } func TestCopyInputValueFromTo(t *testing.T) { @@ -230,7 +230,7 @@ func TestUpdate_HostSSHConfigLoaded(t *testing.T) { require.NotEqual(t, "default: Mock User", model.inputs[inputLogin].Placeholder) require.NotEqual(t, "default: Mock Port", model.inputs[inputNetworkPort].Placeholder) - model.Update(message.HostSSHConfigLoaded{ + model.Update(message.HostSSHConfigLoadComplete{ HostID: 0, Config: sshconfig.Config{ IdentityFile: "Mock Identity File", @@ -380,7 +380,7 @@ func TestUpdate_KeyDiscard(t *testing.T) { Type: tea.KeyEscape, }) - require.Equal(t, message.CloseViewHostEdit{}, cmd()) + require.Equal(t, message.ViewHostEditClose{}, cmd()) } func TestUpdate_KeySave(t *testing.T) { @@ -394,13 +394,13 @@ func TestUpdate_KeySave(t *testing.T) { testutils.CmdToMessage(cmd, &msgs) for _, msg := range msgs { - if _, ok := msg.(message.HostSelected); ok { + if _, ok := msg.(message.HostSelect); ok { continue } - if _, ok := msg.(message.CloseViewHostEdit); ok { + if _, ok := msg.(message.ViewHostEditClose); ok { continue } - if _, ok := msg.(message.HostUpdated); ok { + if _, ok := msg.(message.HostUpdate); ok { continue } diff --git a/internal/ui/component/hostlist/hostlist.go b/internal/ui/component/hostlist/hostlist.go index 532e30d..d52beaf 100644 --- a/internal/ui/component/hostlist/hostlist.go +++ b/internal/ui/component/hostlist/hostlist.go @@ -145,24 +145,18 @@ func (m *ListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.SetSize(msg.Width-h, msg.Height-v) m.logger.Debug("[UI] Set host list size: %d %d", m.Width(), m.Height()) return m, nil - case message.HostSSHConfigLoaded: + case message.HostSSHConfigLoadComplete: m.onHostSSHConfigLoaded(msg) return m, nil - case message.HostUpdated: + case message.HostUpdate: cmd := m.onHostUpdated(msg) return m, cmd - case message.HostCreated: + case message.HostCreate: cmd := m.onHostCreated(msg) return m, cmd - case message.GroupSelected: - m.logger.Debug("[UI] Update app state. Active group: %q", msg.Name) - m.appState.Group = msg.Name - // Reset filter when group is selected - m.ResetFilter() - // We re-load hosts every time a group is selected. This is not the best way - // to handle this, as it leads to series of hacks here and there. But it's the - // simplest way to implement it. - return m, m.loadHosts() + case message.GroupSelect: + cmd := m.onGroupSelect(msg) + return m, cmd case message.HideUINotification: if msg.ComponentName == "hostlist" { m.logger.Debug("[UI] Hide notification message") @@ -208,7 +202,7 @@ func (m *ListModel) handleKeyboardEvent(msg tea.KeyMsg) tea.Cmd { // Handle key event when some mode is enabled. For instance "removeMode". return m.handleKeyEventWhenModeEnabled(msg) case key.Matches(msg, m.keyMap.selectGroup): - return message.TeaCmd(message.OpenViewSelectGroup{}) + return message.TeaCmd(message.ViewGroupListOpen{}) case key.Matches(msg, m.keyMap.connect): return m.constructProcessCmd(constant.ProcessTypeSSHConnect) case key.Matches(msg, m.keyMap.copyID): @@ -218,19 +212,12 @@ func (m *ListModel) handleKeyboardEvent(msg tea.KeyMsg) tea.Cmd { case key.Matches(msg, m.keyMap.edit): return m.editItem() case key.Matches(msg, m.keyMap.append): - return message.TeaCmd(message.OpenViewHostEdit{}) // When create a new item, jump to edit mode. + return message.TeaCmd(message.ViewHostEditOpen{}) // When create a new item, jump to edit mode. case key.Matches(msg, m.keyMap.clone): return m.copyItem() case key.Matches(msg, m.keyMap.toggleLayout): return m.onToggleLayout() case msg.Type == tea.KeyEsc: - if m.appState.Group != "" { - // When user presses Escape key while group is selected, - // we should open select group form. - m.logger.Debug("[UI] Receive Escape key when group selected. Open view select group.") - return message.TeaCmd(message.OpenViewSelectGroup{}) - } - m.logger.Debug("[UI] Receive Escape key. Ask user for confirmation to close the app.") m.enterCloseAppMode() return nil @@ -351,7 +338,7 @@ func (m *ListModel) editItem() tea.Cmd { // m.Model.ResetFilter() m.logger.Info("[UI] Edit item id: %d, title: %s", item.ID, item.Title()) return tea.Sequence( - message.TeaCmd(message.OpenViewHostEdit{HostID: item.ID}), + message.TeaCmd(message.ViewHostEditOpen{HostID: item.ID}), // Load SSH config for the selected host message.TeaCmd(message.RunProcessSSHLoadConfig{Host: item.Host}), ) @@ -407,7 +394,7 @@ func (m *ListModel) copyItem() tea.Cmd { // onHostUpdated - not only updates a host, it also re-inserts the host into // a correct position of the host list, to keep it sorted. -func (m *ListModel) onHostUpdated(msg message.HostUpdated) tea.Cmd { +func (m *ListModel) onHostUpdated(msg message.HostUpdate) tea.Cmd { updatedHost := ListItemHost{Host: msg.Host} // Get all item titles, replacing the updated host's title allItems := lo.Map(m.Items(), func(item list.Item, _ int) list.Item { @@ -435,7 +422,7 @@ func (m *ListModel) onHostUpdated(msg message.HostUpdated) tea.Cmd { return tea.Sequence( cmd, m.onFocusChanged(), - m.displayNotificationMsg(fmt.Sprintf("saved \"%s\"", updatedHost.Title())), + m.displayNotificationMsg(fmt.Sprintf("saved %q", updatedHost.Title())), ) } @@ -459,7 +446,7 @@ func (m *ListModel) setItemAndReorder(newIndex, currentIndex int, updatedHost Li return cmd } -func (m *ListModel) onHostCreated(msg message.HostCreated) tea.Cmd { +func (m *ListModel) onHostCreated(msg message.HostCreate) tea.Cmd { // ResetFilter is required here because, user can create a new Item which will be filtered out, // therefore the user will not see any changes in the UI which is confusing. // ResetFilter must be done before calculating index of the new item. @@ -483,10 +470,29 @@ func (m *ListModel) onHostCreated(msg message.HostCreated) tea.Cmd { // If host position coincides with other host, then let the underlying model to handle that cmd, m.onFocusChanged(), - m.displayNotificationMsg(fmt.Sprintf("created \"%s\"", createdHostItem.Title())), + m.displayNotificationMsg(fmt.Sprintf("created %q", createdHostItem.Title())), ) } +func (m *ListModel) onGroupSelect(msg message.GroupSelect) tea.Cmd { + var cmds []tea.Cmd + m.logger.Debug("[UI] Update app state. Active group: %q", msg.Name) + m.appState.Group = msg.Name + // Reset filter when group is selected + m.ResetFilter() + // We re-load hosts every time a group is selected. This is not the best way + // to handle this, as it leads to series of hacks here and there. But it's the + // simplest way to implement it. + cmds = append(cmds, m.loadHosts()) + // Display selected group notification + if !utils.StringEmpty(&msg.Name) { + notificationMsg := fmt.Sprintf("group %q", msg.Name) + cmds = append(cmds, m.displayNotificationMsg(notificationMsg)) + } + + return tea.Sequence(cmds...) +} + func (m *ListModel) onFocusChanged() tea.Cmd { m.updateTitle() m.updateKeyMap() @@ -495,7 +501,7 @@ func (m *ListModel) onFocusChanged() tea.Cmd { m.logger.Debug("[UI] Focus changed to host id: %v, title: %q", hostItem.ID, hostItem.Title()) return tea.Sequence( - message.TeaCmd(message.HostSelected{HostID: hostItem.ID}), + message.TeaCmd(message.HostSelect{HostID: hostItem.ID}), message.TeaCmd(message.RunProcessSSHLoadConfig{Host: hostItem.Host}), ) } @@ -504,7 +510,7 @@ func (m *ListModel) onFocusChanged() tea.Cmd { return nil } -func (m *ListModel) onHostSSHConfigLoaded(msg message.HostSSHConfigLoaded) { +func (m *ListModel) onHostSSHConfigLoaded(msg message.HostSSHConfigLoadComplete) { for index, item := range m.Items() { if hostListItem, ok := item.(ListItemHost); ok && hostListItem.ID == msg.HostID { hostListItem.SSHHostConfig = &msg.Config diff --git a/internal/ui/component/hostlist/hostlist_test.go b/internal/ui/component/hostlist/hostlist_test.go index 1330b06..3b08527 100644 --- a/internal/ui/component/hostlist/hostlist_test.go +++ b/internal/ui/component/hostlist/hostlist_test.go @@ -31,7 +31,7 @@ func TestListModel_Init(t *testing.T) { var dst []tea.Msg testutils.CmdToMessage(teaCmd, &dst) require.Equal(t, []tea.Msg{ - message.HostSelected{HostID: 1}, + message.HostSelect{HostID: 1}, message.RunProcessSSHLoadConfig{ Host: host.Host{ ID: 1, @@ -158,7 +158,7 @@ func TestRemoveItem(t *testing.T) { want: []tea.Msg{ message.HideUINotification{ComponentName: "hostlist"}, // Because we remote item "Mock Host 1" (which has index 0), we should ensure that next available item will be focused - message.HostSelected{HostID: 2}, + message.HostSelect{HostID: 2}, message.RunProcessSSHLoadConfig{ Host: host.Host{ ID: 2, @@ -185,7 +185,7 @@ func TestRemoveItem(t *testing.T) { want: []tea.Msg{ message.HideUINotification{ComponentName: "hostlist"}, // Because we remote item "Mock Host 1" (which has index 0), we should ensure that next available item will be focused - message.HostSelected{HostID: 2}, + message.HostSelect{HostID: 2}, message.RunProcessSSHLoadConfig{ Host: host.Host{ ID: 2, @@ -358,7 +358,7 @@ func TestExitRemoveItemMode(t *testing.T) { expected := []tea.Msg{ // Because we remote item "Mock Host 1" (which has index 0), we should ensure that next available item will be focused - message.HostSelected{HostID: 1}, + message.HostSelect{HostID: 1}, message.RunProcessSSHLoadConfig{ Host: host.Host{ ID: 1, @@ -495,7 +495,7 @@ func TestListModel_editItem(t *testing.T) { var dst []tea.Msg testutils.CmdToMessage(teaCmd, &dst) - require.Contains(t, dst, message.OpenViewHostEdit{HostID: 1}) + require.Contains(t, dst, message.ViewHostEditOpen{HostID: 1}) require.Contains(t, dst, message.RunProcessSSHLoadConfig{Host: lm.SelectedItem().(ListItemHost).Host}) } @@ -576,7 +576,7 @@ func TestUpdate_HostSSHConfigLoaded(t *testing.T) { Port: "9999", User: "mock_username", } - lm.Update(message.HostSSHConfigLoaded{ + lm.Update(message.HostSSHConfigLoadComplete{ HostID: 1, Config: expectedConfig, }) @@ -604,7 +604,7 @@ func TestUpdate_HostUpdated(t *testing.T) { SSHHostConfig: nil, } - lm.Update(message.HostUpdated{Host: updatedHost}) + lm.Update(message.HostUpdate{Host: updatedHost}) require.Equal(t, updatedHost, lm.Items()[0].(ListItemHost).Host) // Also check that host is inserted into a correct position of the hostlist model @@ -619,7 +619,7 @@ func TestUpdate_HostUpdated(t *testing.T) { SSHHostConfig: nil, } - lm.Update(message.HostUpdated{Host: updatedHost}) + lm.Update(message.HostUpdate{Host: updatedHost}) lastIndex := 2 require.Equal(t, updatedHost, lm.Items()[lastIndex].(ListItemHost).Host) } @@ -643,7 +643,7 @@ func TestUpdate_HostCreated(t *testing.T) { SSHHostConfig: nil, } - lm.Update(message.HostCreated{Host: createdHost1}) + lm.Update(message.HostCreate{Host: createdHost1}) require.Len(t, lm.Items(), 4, "Wrong host list size") require.Equal(t, createdHost1, lm.Items()[0].(ListItemHost).Host) @@ -659,7 +659,7 @@ func TestUpdate_HostCreated(t *testing.T) { SSHHostConfig: nil, } - lm.Update(message.HostCreated{Host: createdHost2}) + lm.Update(message.HostCreate{Host: createdHost2}) require.Len(t, lm.Items(), 5, "Wrong host list size") lastIndex := 4 // because we have 5 hosts in total require.Equal(t, createdHost2, lm.Items()[lastIndex].(ListItemHost).Host) @@ -681,7 +681,7 @@ func TestUpdate_GroupListSelectItem(t *testing.T) { require.Equal(t, list.Filtering, model.FilterState()) // Dispatch message - model.Update(message.GroupSelected{Name: "Group 1"}) + model.Update(message.GroupSelect{Name: "Group 1"}) // Now test, that there is only a single list in the collection require.Len(t, model.Items(), 1) @@ -833,7 +833,7 @@ func Test_handleKeyboardEvent_selectGroup(t *testing.T) { model.Init() _, cmd := model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'z'}}) res := cmd() - require.IsType(t, message.OpenViewSelectGroup{}, res) + require.IsType(t, message.ViewGroupListOpen{}, res) } func Test_handleKeyboardEvent_connect(t *testing.T) { @@ -1016,18 +1016,10 @@ func TestUpdate_ToggleBetweenScreenLayouts(t *testing.T) { } func Test_HandleKeyboardEvent_Escape(t *testing.T) { - // If group is selected and type Escape key, the model - // should dispatch open group view message + // If press Escape key, the app should ask the user whether + // it wants to close the program model := newMockListModel(false) - model.appState.Group = "Group 1" _, cmd := model.Update(tea.KeyMsg{Type: tea.KeyEsc}) - require.IsType(t, message.OpenViewSelectGroup{}, cmd()) - - // If group is NOT selected and press Escape key, the app - // should ask the user whether it wants to close the program - model = newMockListModel(false) - model.appState.Group = "" - _, cmd = model.Update(tea.KeyMsg{Type: tea.KeyEsc}) require.Nil(t, cmd) require.Equal(t, modeCloseApp, model.mode) require.Equal(t, "close app? (y/N)", utils.StripStyles(model.Title)) diff --git a/internal/ui/message/message.go b/internal/ui/message/message.go index ce6de02..365b445 100644 --- a/internal/ui/message/message.go +++ b/internal/ui/message/message.go @@ -16,30 +16,30 @@ type ( InitComplete struct{} // TerminalSizePolling - is a message which is sent when terminal width and/or height changes. TerminalSizePolling struct{ Width, Height int } - // HostSelected is required to let host list know that it's time to update title. - HostSelected struct{ HostID int } - // HostCreated - is dispatched when a new host was added to the database. - HostCreated struct{ Host host.Host } - // HostUpdated - is dispatched when host model is updated. - HostUpdated struct{ Host host.Host } - // HostSSHConfigLoaded triggers when app loads a host config using ssh -G . + // HostSelect is required to let host list know that it's time to update title. + HostSelect struct{ HostID int } + // HostCreate - is dispatched when a new host was added to the database. + HostCreate struct{ Host host.Host } + // HostUpdate - is dispatched when host model is updated. + HostUpdate struct{ Host host.Host } + // HostSSHConfigLoadComplete triggers when app loads a host config using ssh -G . // The config is stored in main model: m.appState.HostSSHConfig. - HostSSHConfigLoaded struct { + HostSSHConfigLoadComplete struct { HostID int Config sshconfig.Config } - // OpenViewSelectGroup - dispatched when it's required to open group list view. - OpenViewSelectGroup struct{} - // CloseViewSelectGroup - dispatched when it's required to close group list view. - CloseViewSelectGroup struct{} - // GroupSelected - is dispatched when select a group in group list view. - GroupSelected struct{ Name string } + // ViewGroupListOpen - dispatched when it's required to open group list view. + ViewGroupListOpen struct{} + // ViewGroupListClose - dispatched when it's required to close group list view. + ViewGroupListClose struct{} + // GroupSelect - is dispatched when select a group in group list view. + GroupSelect struct{ Name string } // HideUINotification - is dispatched when it's time to hide UI notification and display normal component's title. HideUINotification struct{ ComponentName string } - // OpenViewHostEdit fires when user press edit button on a selected host. - OpenViewHostEdit struct{ HostID int } - // CloseViewHostEdit triggers when users exits from edit form without saving results. - CloseViewHostEdit struct{} + // ViewHostEditOpen fires when user press edit button on a selected host. + ViewHostEditOpen struct{ HostID int } + // ViewHostEditClose triggers when users exits from edit form without saving results. + ViewHostEditClose struct{} // ErrorOccurred - is dispatched when an error occurs. ErrorOccurred struct{ Err error } // ExitWithError - indicates that something bad happened and we need to close the application. diff --git a/internal/ui/model.go b/internal/ui/model.go index d07018f..2fb5187 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -83,21 +83,21 @@ func (m *MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.appState.Width = msg.Width m.appState.Height = msg.Height m.updateViewPort(msg.Width, msg.Height) - case message.OpenViewHostEdit: + case message.ViewHostEditOpen: m.logger.Debug("[UI] Open host edit form") m.appState.CurrentView = state.ViewEditItem ctx := context.WithValue(m.appContext, hostedit.ItemID, msg.HostID) m.modelHostEdit = hostedit.New(ctx, m.hostStorage, m.appState, m.logger) - case message.CloseViewHostEdit: + case message.ViewHostEditClose: m.logger.Debug("[UI] Close host edit form") m.appState.CurrentView = state.ViewHostList - case message.OpenViewSelectGroup: + case message.ViewGroupListOpen: m.logger.Debug("[UI] Open select group form") m.appState.CurrentView = state.ViewGroupList - case message.CloseViewSelectGroup: + case message.ViewGroupListClose: m.logger.Debug("[UI] Close select group form") m.appState.CurrentView = state.ViewHostList - case message.HostSelected: + case message.HostSelect: m.logger.Debug("[UI] Update app state. Active host id: %d", msg.HostID) m.appState.Selected = msg.HostID case message.RunProcessSSHConnect: @@ -296,7 +296,7 @@ func (m *MainModel) handleProcessSuccess(msg message.RunProcessSuccess) tea.Cmd if msg.ProcessType == constant.ProcessTypeSSHLoadConfig { parsedSSHConfig := sshconfig.Parse(msg.StdOut) m.logger.Debug("[EXEC] Host SSH config loaded: %+v", *parsedSSHConfig) - return message.TeaCmd(message.HostSSHConfigLoaded{ + return message.TeaCmd(message.HostSSHConfigLoadComplete{ HostID: m.appState.Selected, Config: *parsedSSHConfig, }) diff --git a/internal/ui/model_test.go b/internal/ui/model_test.go index 2fafd1a..2afe03d 100644 --- a/internal/ui/model_test.go +++ b/internal/ui/model_test.go @@ -30,7 +30,7 @@ func TestNew(t *testing.T) { var msgs []tea.Msg testutils.CmdToMessage(cmd, &msgs) - require.IsType(t, message.HostSelected{}, msgs[0]) + require.IsType(t, message.HostSelect{}, msgs[0]) require.IsType(t, message.RunProcessSSHLoadConfig{}, msgs[1]) } @@ -122,7 +122,7 @@ func TestHandleProcessSuccess_SSH_load_config(t *testing.T) { StdOut: "hostname localhost\r\nport 2222\r\nidentityfile /tmp\r\nuser root", } - expected := message.HostSSHConfigLoaded{ + expected := message.HostSSHConfigLoadComplete{ HostID: 0, Config: sshconfig.Config{ Hostname: "localhost",