sshm/internal/ui/update.go

635 lines
16 KiB
Go

package ui
import (
"context"
"fmt"
"os/exec"
"time"
"sshm/internal/config"
"sshm/internal/connectivity"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
)
// Messages for SSH ping functionality
type (
pingResultMsg *connectivity.HostPingResult
)
// startPingAllCmd creates a command to ping all hosts concurrently
func (m Model) startPingAllCmd() tea.Cmd {
if m.pingManager == nil {
return nil
}
return tea.Batch(
// Create individual ping commands for each host
func() tea.Cmd {
var cmds []tea.Cmd
for _, host := range m.hosts {
cmds = append(cmds, pingSingleHostCmd(m.pingManager, host))
}
return tea.Batch(cmds...)
}(),
)
}
// listenForPingResultsCmd is no longer needed since we use individual ping commands
// pingSingleHostCmd creates a command to ping a single host
func pingSingleHostCmd(pingManager *connectivity.PingManager, host config.SSHHost) tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result := pingManager.PingHost(ctx, host)
return pingResultMsg(result)
}
}
// Init initializes the model
func (m Model) Init() tea.Cmd {
return tea.Batch(
textinput.Blink,
// Ping is now optional - use 'p' key to start ping
)
}
// Update handles model updates
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
// Handle different message types
switch msg := msg.(type) {
case tea.WindowSizeMsg:
// Update terminal size and recalculate styles
m.width = msg.Width
m.height = msg.Height
m.styles = NewStyles(m.width)
m.ready = true
// Update table height and columns based on new window size
m.updateTableHeight()
m.updateTableColumns()
// Update sub-forms if they exist
if m.addForm != nil {
m.addForm.width = m.width
m.addForm.height = m.height
m.addForm.styles = m.styles
}
if m.editForm != nil {
m.editForm.width = m.width
m.editForm.height = m.height
m.editForm.styles = m.styles
}
if m.infoForm != nil {
m.infoForm.width = m.width
m.infoForm.height = m.height
m.infoForm.styles = m.styles
}
if m.portForwardForm != nil {
m.portForwardForm.width = m.width
m.portForwardForm.height = m.height
m.portForwardForm.styles = m.styles
}
if m.helpForm != nil {
m.helpForm.width = m.width
m.helpForm.height = m.height
m.helpForm.styles = m.styles
}
if m.fileSelectorForm != nil {
m.fileSelectorForm.width = m.width
m.fileSelectorForm.height = m.height
m.fileSelectorForm.styles = m.styles
}
return m, nil
case pingResultMsg:
// Handle ping result - update table display
if msg != nil {
// Update the table to reflect the new ping status
m.updateTableRows()
}
return m, nil
case addFormSubmitMsg:
if msg.err != nil {
// Show error in form
if m.addForm != nil {
m.addForm.err = msg.err.Error()
}
return m, nil
} else {
// Success: refresh hosts and return to list view
var hosts []config.SSHHost
var err error
if m.configFile != "" {
hosts, err = config.ParseSSHConfigFile(m.configFile)
} else {
hosts, err = config.ParseSSHConfig()
}
if err != nil {
return m, tea.Quit
}
m.hosts = m.sortHosts(hosts)
// Reapply search filter if there is one active
if m.searchInput.Value() != "" {
m.filteredHosts = m.filterHosts(m.searchInput.Value())
} else {
m.filteredHosts = m.hosts
}
m.updateTableRows()
m.viewMode = ViewList
m.addForm = nil
m.table.Focus()
return m, nil
}
case addFormCancelMsg:
// Cancel: return to list view
m.viewMode = ViewList
m.addForm = nil
m.table.Focus()
return m, nil
case editFormSubmitMsg:
if msg.err != nil {
// Show error in form
if m.editForm != nil {
m.editForm.err = msg.err.Error()
}
return m, nil
} else {
// Success: refresh hosts and return to list view
var hosts []config.SSHHost
var err error
if m.configFile != "" {
hosts, err = config.ParseSSHConfigFile(m.configFile)
} else {
hosts, err = config.ParseSSHConfig()
}
if err != nil {
return m, tea.Quit
}
m.hosts = m.sortHosts(hosts)
// Reapply search filter if there is one active
if m.searchInput.Value() != "" {
m.filteredHosts = m.filterHosts(m.searchInput.Value())
} else {
m.filteredHosts = m.hosts
}
m.updateTableRows()
m.viewMode = ViewList
m.editForm = nil
m.table.Focus()
return m, nil
}
case editFormCancelMsg:
// Cancel: return to list view
m.viewMode = ViewList
m.editForm = nil
m.table.Focus()
return m, nil
case infoFormCancelMsg:
// Cancel: return to list view
m.viewMode = ViewList
m.infoForm = nil
m.table.Focus()
return m, nil
case fileSelectorMsg:
if msg.cancelled {
// Cancel: return to list view
m.viewMode = ViewList
m.fileSelectorForm = nil
m.table.Focus()
return m, nil
} else {
// File selected: proceed to add form with selected file
m.addForm = NewAddForm("", m.styles, m.width, m.height, msg.selectedFile)
m.viewMode = ViewAdd
m.fileSelectorForm = nil
return m, textinput.Blink
}
case infoFormEditMsg:
// Switch from info to edit mode
editForm, err := NewEditForm(msg.hostName, m.styles, m.width, m.height, m.configFile)
if err != nil {
// Handle error - could show in UI, for now just go back to list
m.viewMode = ViewList
m.infoForm = nil
m.table.Focus()
return m, nil
}
m.editForm = editForm
m.infoForm = nil
m.viewMode = ViewEdit
return m, textinput.Blink
case portForwardSubmitMsg:
if msg.err != nil {
// Show error in form
if m.portForwardForm != nil {
m.portForwardForm.err = msg.err.Error()
}
return m, nil
} else {
// Success: execute SSH command with port forwarding
if len(msg.sshArgs) > 0 {
sshCmd := exec.Command("ssh", msg.sshArgs...)
// Record the connection in history
if m.historyManager != nil && m.portForwardForm != nil {
err := m.historyManager.RecordConnection(m.portForwardForm.hostName)
if err != nil {
fmt.Printf("Warning: Could not record connection history: %v\n", err)
}
}
return m, tea.ExecProcess(sshCmd, func(err error) tea.Msg {
return tea.Quit()
})
}
// If no SSH args, just return to list view
m.viewMode = ViewList
m.portForwardForm = nil
m.table.Focus()
return m, nil
}
case portForwardCancelMsg:
// Cancel: return to list view
m.viewMode = ViewList
m.portForwardForm = nil
m.table.Focus()
return m, nil
case helpCloseMsg:
// Close help: return to list view
m.viewMode = ViewList
m.helpForm = nil
m.table.Focus()
return m, nil
case tea.KeyMsg:
// Handle view-specific key presses
switch m.viewMode {
case ViewAdd:
if m.addForm != nil {
var newForm *addFormModel
newForm, cmd = m.addForm.Update(msg)
m.addForm = newForm
return m, cmd
}
case ViewEdit:
if m.editForm != nil {
var newForm *editFormModel
newForm, cmd = m.editForm.Update(msg)
m.editForm = newForm
return m, cmd
}
case ViewInfo:
if m.infoForm != nil {
var newForm *infoFormModel
newForm, cmd = m.infoForm.Update(msg)
m.infoForm = newForm
return m, cmd
}
case ViewPortForward:
if m.portForwardForm != nil {
var newForm *portForwardModel
newForm, cmd = m.portForwardForm.Update(msg)
m.portForwardForm = newForm
return m, cmd
}
case ViewHelp:
if m.helpForm != nil {
var newForm *helpModel
newForm, cmd = m.helpForm.Update(msg)
m.helpForm = newForm
return m, cmd
}
case ViewFileSelector:
if m.fileSelectorForm != nil {
var newForm *fileSelectorModel
newForm, cmd = m.fileSelectorForm.Update(msg)
m.fileSelectorForm = newForm
return m, cmd
}
case ViewList:
// Handle list view keys
return m.handleListViewKeys(msg)
}
}
return m, cmd
}
func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg.String() {
case "esc", "ctrl+c":
if m.deleteMode {
// Exit delete mode
m.deleteMode = false
m.deleteHost = ""
m.table.Focus()
return m, nil
}
return m, tea.Quit
case "q":
if !m.searchMode && !m.deleteMode {
return m, tea.Quit
}
case "/", "ctrl+f":
if !m.searchMode && !m.deleteMode {
// Enter search mode
m.searchMode = true
m.updateTableStyles()
m.table.Blur()
m.searchInput.Focus()
return m, textinput.Blink
}
case "tab":
if !m.deleteMode {
// Switch focus between search input and table
if m.searchMode {
// Switch from search to table
m.searchMode = false
m.updateTableStyles()
m.searchInput.Blur()
m.table.Focus()
} else {
// Switch from table to search
m.searchMode = true
m.updateTableStyles()
m.table.Blur()
m.searchInput.Focus()
return m, textinput.Blink
}
return m, nil
}
case "enter":
if m.searchMode {
// Validate search and return to table mode to allow commands
m.searchMode = false
m.updateTableStyles()
m.searchInput.Blur()
m.table.Focus()
return m, nil
} else if m.deleteMode {
// Confirm deletion
var err error
if m.configFile != "" {
err = config.DeleteSSHHostFromFile(m.deleteHost, m.configFile)
} else {
err = config.DeleteSSHHost(m.deleteHost)
}
if err != nil {
// Could display an error message here
m.deleteMode = false
m.deleteHost = ""
m.table.Focus()
return m, nil
}
// Refresh the hosts list
var hosts []config.SSHHost
var parseErr error
if m.configFile != "" {
hosts, parseErr = config.ParseSSHConfigFile(m.configFile)
} else {
hosts, parseErr = config.ParseSSHConfig()
}
if parseErr != nil {
// Could display an error message here
m.deleteMode = false
m.deleteHost = ""
m.table.Focus()
return m, nil
}
m.hosts = m.sortHosts(hosts)
// Reapply search filter if there is one active
if m.searchInput.Value() != "" {
m.filteredHosts = m.filterHosts(m.searchInput.Value())
} else {
m.filteredHosts = m.hosts
}
m.updateTableRows()
m.deleteMode = false
m.deleteHost = ""
m.table.Focus()
return m, nil
} else {
// Connect to the selected host
selected := m.table.SelectedRow()
if len(selected) > 0 {
hostName := extractHostNameFromTableRow(selected[0]) // Extract hostname from first column
// Record the connection in history
if m.historyManager != nil {
err := m.historyManager.RecordConnection(hostName)
if err != nil {
// Log the error but don't prevent the connection
fmt.Printf("Warning: Could not record connection history: %v\n", err)
}
}
// Build the SSH command with the appropriate config file
var sshCmd *exec.Cmd
if m.configFile != "" {
sshCmd = exec.Command("ssh", "-F", m.configFile, hostName)
} else {
sshCmd = exec.Command("ssh", hostName)
}
return m, tea.ExecProcess(sshCmd, func(err error) tea.Msg {
return tea.Quit()
})
}
}
case "e":
if !m.searchMode && !m.deleteMode {
// Edit the selected host
selected := m.table.SelectedRow()
if len(selected) > 0 {
hostName := extractHostNameFromTableRow(selected[0]) // Extract hostname from first column
editForm, err := NewEditForm(hostName, m.styles, m.width, m.height, m.configFile)
if err != nil {
// Handle error - could show in UI
return m, nil
}
m.editForm = editForm
m.viewMode = ViewEdit
return m, textinput.Blink
}
}
case "i":
if !m.searchMode && !m.deleteMode {
// Show info for the selected host
selected := m.table.SelectedRow()
if len(selected) > 0 {
hostName := extractHostNameFromTableRow(selected[0]) // Extract hostname from first column
infoForm, err := NewInfoForm(hostName, m.styles, m.width, m.height, m.configFile)
if err != nil {
// Handle error - could show in UI
return m, nil
}
m.infoForm = infoForm
m.viewMode = ViewInfo
return m, nil
}
}
case "a":
if !m.searchMode && !m.deleteMode {
// Check if there are multiple config files starting from the current base config
var configFiles []string
var err error
if m.configFile != "" {
// Use the specified config file as base
configFiles, err = config.GetAllConfigFilesFromBase(m.configFile)
} else {
// Use the default config file as base
configFiles, err = config.GetAllConfigFiles()
}
if err != nil || len(configFiles) <= 1 {
// Only one config file (or error), go directly to add form
var configFile string
if len(configFiles) == 1 {
configFile = configFiles[0]
} else {
configFile = m.configFile
}
m.addForm = NewAddForm("", m.styles, m.width, m.height, configFile)
m.viewMode = ViewAdd
} else {
// Multiple config files, show file selector
fileSelectorForm, err := NewFileSelectorFromBase("Select config file to add host to:", m.styles, m.width, m.height, m.configFile)
if err != nil {
// Fallback to default behavior if file selector fails
m.addForm = NewAddForm("", m.styles, m.width, m.height, m.configFile)
m.viewMode = ViewAdd
} else {
m.fileSelectorForm = fileSelectorForm
m.viewMode = ViewFileSelector
}
}
return m, textinput.Blink
}
case "d":
if !m.searchMode && !m.deleteMode {
// Delete the selected host
selected := m.table.SelectedRow()
if len(selected) > 0 {
hostName := extractHostNameFromTableRow(selected[0]) // Extract hostname from first column
m.deleteMode = true
m.deleteHost = hostName
m.table.Blur()
return m, nil
}
}
case "p":
if !m.searchMode && !m.deleteMode {
// Ping all hosts
return m, m.startPingAllCmd()
}
case "f":
if !m.searchMode && !m.deleteMode {
// Port forwarding for the selected host
selected := m.table.SelectedRow()
if len(selected) > 0 {
hostName := extractHostNameFromTableRow(selected[0]) // Extract hostname from first column
m.portForwardForm = NewPortForwardForm(hostName, m.styles, m.width, m.height, m.configFile)
m.viewMode = ViewPortForward
return m, textinput.Blink
}
}
case "h":
if !m.searchMode && !m.deleteMode {
// Show help
m.helpForm = NewHelpForm(m.styles, m.width, m.height)
m.viewMode = ViewHelp
return m, nil
}
case "s":
if !m.searchMode && !m.deleteMode {
// Cycle through sort modes (only 2 modes now)
m.sortMode = (m.sortMode + 1) % 2
// Re-apply the current filter with the new sort mode
if m.searchInput.Value() != "" {
m.filteredHosts = m.filterHosts(m.searchInput.Value())
} else {
m.filteredHosts = m.sortHosts(m.hosts)
}
m.updateTableRows()
return m, nil
}
case "r":
if !m.searchMode && !m.deleteMode {
// Switch to sort by recent (last used)
m.sortMode = SortByLastUsed
// Re-apply the current filter with the new sort mode
if m.searchInput.Value() != "" {
m.filteredHosts = m.filterHosts(m.searchInput.Value())
} else {
m.filteredHosts = m.sortHosts(m.hosts)
}
m.updateTableRows()
return m, nil
}
case "n":
if !m.searchMode && !m.deleteMode {
// Switch to sort by name
m.sortMode = SortByName
// Re-apply the current filter with the new sort mode
if m.searchInput.Value() != "" {
m.filteredHosts = m.filterHosts(m.searchInput.Value())
} else {
m.filteredHosts = m.sortHosts(m.hosts)
}
m.updateTableRows()
return m, nil
}
}
// Update the appropriate component based on mode
if m.searchMode {
oldValue := m.searchInput.Value()
m.searchInput, cmd = m.searchInput.Update(msg)
// Update filtered hosts only if the search value has changed
if m.searchInput.Value() != oldValue {
if m.searchInput.Value() != "" {
m.filteredHosts = m.filterHosts(m.searchInput.Value())
} else {
m.filteredHosts = m.sortHosts(m.hosts)
}
m.updateTableRows()
}
} else {
m.table, cmd = m.table.Update(msg)
}
return m, cmd
}