mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2026-01-27 03:04:21 +01:00
refactor(ui): split TUI logic into multiple files and improve styling
This commit is contained in:
@@ -2,10 +2,7 @@ package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sshm/internal/config"
|
||||
"sshm/internal/history"
|
||||
@@ -16,490 +13,35 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var searchStyleFocused = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("36")).
|
||||
Padding(0, 1)
|
||||
|
||||
var searchStyleUnfocused = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("240")).
|
||||
Padding(0, 1)
|
||||
|
||||
var headerStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("36")).
|
||||
Bold(true).
|
||||
Align(lipgloss.Center)
|
||||
|
||||
const asciiTitle = `
|
||||
_____ _____ _ _ _____
|
||||
| __| __| | | |
|
||||
|__ |__ | | | | |
|
||||
|_____|_____|__|__|_|_|_|
|
||||
`
|
||||
|
||||
type SortMode int
|
||||
|
||||
const (
|
||||
SortByName SortMode = iota
|
||||
SortByLastUsed
|
||||
)
|
||||
|
||||
func (s SortMode) String() string {
|
||||
switch s {
|
||||
case SortByName:
|
||||
return "Name (A-Z)"
|
||||
case SortByLastUsed:
|
||||
return "Last Login"
|
||||
default:
|
||||
return "Name (A-Z)"
|
||||
}
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
table table.Model
|
||||
searchInput textinput.Model
|
||||
hosts []config.SSHHost
|
||||
filteredHosts []config.SSHHost
|
||||
searchMode bool
|
||||
deleteMode bool
|
||||
deleteHost string
|
||||
exitAction string
|
||||
exitHostName string
|
||||
historyManager *history.HistoryManager
|
||||
sortMode SortMode
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
// Handle key messages
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
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.table.Blur()
|
||||
m.searchInput.Focus()
|
||||
return m, textinput.Blink
|
||||
}
|
||||
case "tab":
|
||||
if !m.deleteMode {
|
||||
// Toggle focus between search input and table
|
||||
if m.searchMode {
|
||||
// Switch from search to table
|
||||
m.searchMode = false
|
||||
m.searchInput.Blur()
|
||||
m.table.Focus()
|
||||
} else {
|
||||
// Switch from table to search
|
||||
m.searchMode = true
|
||||
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.searchInput.Blur()
|
||||
m.table.Focus()
|
||||
return m, nil
|
||||
} else if m.deleteMode {
|
||||
// Confirm deletion
|
||||
err := config.DeleteSSHHost(m.deleteHost)
|
||||
if err != nil {
|
||||
// Could show error message here
|
||||
m.deleteMode = false
|
||||
m.deleteHost = ""
|
||||
m.table.Focus()
|
||||
return m, nil
|
||||
}
|
||||
// Refresh the host list
|
||||
hosts, err := config.ParseSSHConfig()
|
||||
if err != nil {
|
||||
// Could show error message here
|
||||
m.deleteMode = false
|
||||
m.deleteHost = ""
|
||||
m.table.Focus()
|
||||
return m, nil
|
||||
}
|
||||
m.hosts = sortHostsByName(hosts)
|
||||
m.filteredHosts = m.hosts
|
||||
m.updateTableRows()
|
||||
m.deleteMode = false
|
||||
m.deleteHost = ""
|
||||
m.table.Focus()
|
||||
return m, nil
|
||||
} else {
|
||||
// Connect to selected host
|
||||
selected := m.table.SelectedRow()
|
||||
if len(selected) > 0 {
|
||||
hostName := selected[0] // Host name is in the first column
|
||||
|
||||
// Record the connection in history
|
||||
if m.historyManager != nil {
|
||||
err := m.historyManager.RecordConnection(hostName)
|
||||
if err != nil {
|
||||
// Log error but don't prevent connection
|
||||
fmt.Printf("Warning: Could not record connection history: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return m, tea.ExecProcess(exec.Command("ssh", hostName), func(err error) tea.Msg {
|
||||
return tea.Quit()
|
||||
})
|
||||
}
|
||||
}
|
||||
case "e":
|
||||
if !m.searchMode && !m.deleteMode {
|
||||
// Edit selected host using dedicated edit form
|
||||
selected := m.table.SelectedRow()
|
||||
if len(selected) > 0 {
|
||||
hostName := selected[0] // Host name is in the first column
|
||||
// Store the edit action and exit
|
||||
m.exitAction = "edit"
|
||||
m.exitHostName = hostName
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
case "a":
|
||||
if !m.searchMode && !m.deleteMode {
|
||||
// Add new host using dedicated add form
|
||||
m.exitAction = "add"
|
||||
return m, tea.Quit
|
||||
}
|
||||
case "d":
|
||||
if !m.searchMode && !m.deleteMode {
|
||||
// Delete selected host
|
||||
selected := m.table.SelectedRow()
|
||||
if len(selected) > 0 {
|
||||
hostName := selected[0] // Host name is in the first column
|
||||
m.deleteMode = true
|
||||
m.deleteHost = hostName
|
||||
m.table.Blur()
|
||||
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 current filter with 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 current filter with 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 current filter with 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 components based on mode
|
||||
if m.searchMode {
|
||||
oldValue := m.searchInput.Value()
|
||||
m.searchInput, cmd = m.searchInput.Update(msg)
|
||||
// Only update filtered hosts if search value 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
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
if m.deleteMode {
|
||||
return m.renderDeleteConfirmation()
|
||||
}
|
||||
|
||||
var view strings.Builder
|
||||
|
||||
// Add ASCII title
|
||||
view.WriteString(headerStyle.Render(asciiTitle) + "\n")
|
||||
|
||||
// Add search bar (always visible) with appropriate style based on focus
|
||||
searchPrompt := "Search (/ to focus, Tab to switch): "
|
||||
if m.searchMode {
|
||||
view.WriteString(searchStyleFocused.Render(searchPrompt+m.searchInput.View()) + "\n")
|
||||
} else {
|
||||
view.WriteString(searchStyleUnfocused.Render(searchPrompt+m.searchInput.View()) + "\n")
|
||||
}
|
||||
|
||||
// Add sort mode indicator
|
||||
sortInfo := fmt.Sprintf("Sort: %s", m.sortMode.String())
|
||||
view.WriteString(lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("240")).
|
||||
Render(sortInfo) + "\n\n")
|
||||
|
||||
// Add table with appropriate style based on focus
|
||||
if m.searchMode {
|
||||
// Table is not focused, use gray border
|
||||
tableStyle := lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240"))
|
||||
view.WriteString(tableStyle.Render(m.table.View()))
|
||||
} else {
|
||||
// Table is focused, use green border
|
||||
tableStyle := lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("36"))
|
||||
view.WriteString(tableStyle.Render(m.table.View()))
|
||||
}
|
||||
|
||||
// Add help text
|
||||
if !m.searchMode {
|
||||
view.WriteString("\nUse ↑/↓ to navigate • Enter to connect • (a)dd • (e)dit • (d)elete • / to search • Tab to switch")
|
||||
view.WriteString("\nSort: (s)witch • (r)ecent • (n)ame • q/ESC to quit")
|
||||
} else {
|
||||
view.WriteString("\nType to filter hosts • Enter to validate search • Tab to switch to table • ESC to quit")
|
||||
}
|
||||
|
||||
return view.String()
|
||||
}
|
||||
|
||||
// sortHosts sorts hosts based on the current sort mode
|
||||
func (m Model) sortHosts(hosts []config.SSHHost) []config.SSHHost {
|
||||
if m.historyManager == nil {
|
||||
return sortHostsByName(hosts)
|
||||
}
|
||||
|
||||
switch m.sortMode {
|
||||
case SortByLastUsed:
|
||||
return m.historyManager.SortHostsByLastUsed(hosts)
|
||||
case SortByName:
|
||||
fallthrough
|
||||
default:
|
||||
return sortHostsByName(hosts)
|
||||
}
|
||||
}
|
||||
|
||||
// sortHostsByName sorts a slice of SSH hosts alphabetically by name
|
||||
func sortHostsByName(hosts []config.SSHHost) []config.SSHHost {
|
||||
sorted := make([]config.SSHHost, len(hosts))
|
||||
copy(sorted, hosts)
|
||||
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return strings.ToLower(sorted[i].Name) < strings.ToLower(sorted[j].Name)
|
||||
})
|
||||
|
||||
return sorted
|
||||
}
|
||||
|
||||
// calculateNameColumnWidth calculates the optimal width for the Name column
|
||||
// based on the longest host name, with a minimum of 8 and maximum of 40 characters
|
||||
func calculateNameColumnWidth(hosts []config.SSHHost) int {
|
||||
maxLength := 8 // Minimum width to accommodate the "Name" header
|
||||
|
||||
for _, host := range hosts {
|
||||
if len(host.Name) > maxLength {
|
||||
maxLength = len(host.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Add some padding (2 characters) for better visual spacing
|
||||
maxLength += 2
|
||||
|
||||
// Cap the maximum width to avoid extremely wide columns
|
||||
if maxLength > 40 {
|
||||
maxLength = 40
|
||||
}
|
||||
|
||||
return maxLength
|
||||
}
|
||||
|
||||
// calculateTagsColumnWidth calculates the optimal width for the Tags column
|
||||
// based on the longest tags string, with a minimum of 8 and maximum of 40 characters
|
||||
func calculateTagsColumnWidth(hosts []config.SSHHost) int {
|
||||
maxLength := 8 // Minimum width to accommodate the "Tags" header
|
||||
|
||||
for _, host := range hosts {
|
||||
// Format tags exactly the same way they appear in the table
|
||||
var tagsStr string
|
||||
if len(host.Tags) > 0 {
|
||||
// Add # prefix to each tag and join with spaces
|
||||
var formattedTags []string
|
||||
for _, tag := range host.Tags {
|
||||
formattedTags = append(formattedTags, "#"+tag)
|
||||
}
|
||||
tagsStr = strings.Join(formattedTags, " ")
|
||||
}
|
||||
|
||||
if len(tagsStr) > maxLength {
|
||||
maxLength = len(tagsStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Add some padding (2 characters) for better visual spacing
|
||||
maxLength += 2
|
||||
|
||||
// Cap the maximum width to avoid extremely wide columns
|
||||
if maxLength > 40 {
|
||||
maxLength = 40
|
||||
}
|
||||
|
||||
return maxLength
|
||||
}
|
||||
|
||||
// formatTimeAgo formats a time into a human-readable "time ago" string
|
||||
func formatTimeAgo(t time.Time) string {
|
||||
now := time.Now()
|
||||
duration := now.Sub(t)
|
||||
|
||||
switch {
|
||||
case duration < time.Minute:
|
||||
seconds := int(duration.Seconds())
|
||||
if seconds <= 1 {
|
||||
return "1 second ago"
|
||||
}
|
||||
return fmt.Sprintf("%d seconds ago", seconds)
|
||||
case duration < time.Hour:
|
||||
minutes := int(duration.Minutes())
|
||||
if minutes == 1 {
|
||||
return "1 minute ago"
|
||||
}
|
||||
return fmt.Sprintf("%d minutes ago", minutes)
|
||||
case duration < 24*time.Hour:
|
||||
hours := int(duration.Hours())
|
||||
if hours == 1 {
|
||||
return "1 hour ago"
|
||||
}
|
||||
return fmt.Sprintf("%d hours ago", hours)
|
||||
case duration < 7*24*time.Hour:
|
||||
days := int(duration.Hours() / 24)
|
||||
if days == 1 {
|
||||
return "1 day ago"
|
||||
}
|
||||
return fmt.Sprintf("%d days ago", days)
|
||||
case duration < 30*24*time.Hour:
|
||||
weeks := int(duration.Hours() / (24 * 7))
|
||||
if weeks == 1 {
|
||||
return "1 week ago"
|
||||
}
|
||||
return fmt.Sprintf("%d weeks ago", weeks)
|
||||
case duration < 365*24*time.Hour:
|
||||
months := int(duration.Hours() / (24 * 30))
|
||||
if months == 1 {
|
||||
return "1 month ago"
|
||||
}
|
||||
return fmt.Sprintf("%d months ago", months)
|
||||
default:
|
||||
years := int(duration.Hours() / (24 * 365))
|
||||
if years == 1 {
|
||||
return "1 year ago"
|
||||
}
|
||||
return fmt.Sprintf("%d years ago", years)
|
||||
}
|
||||
}
|
||||
|
||||
// calculateLastLoginColumnWidth calculates the optimal width for the Last Login column
|
||||
// based on the longest time format, with a minimum of 12 and maximum of 20 characters
|
||||
func calculateLastLoginColumnWidth(hosts []config.SSHHost, historyManager *history.HistoryManager) int {
|
||||
maxLength := 12 // Minimum width to accommodate the "Last Login" header
|
||||
|
||||
if historyManager != nil {
|
||||
for _, host := range hosts {
|
||||
if lastConnect, exists := historyManager.GetLastConnectionTime(host.Name); exists {
|
||||
timeStr := formatTimeAgo(lastConnect)
|
||||
if len(timeStr) > maxLength {
|
||||
maxLength = len(timeStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add some padding (2 characters) for better visual spacing
|
||||
maxLength += 2
|
||||
|
||||
// Cap the maximum width to avoid extremely wide columns
|
||||
if maxLength > 20 {
|
||||
maxLength = 20
|
||||
}
|
||||
|
||||
return maxLength
|
||||
}
|
||||
|
||||
// calculateInfoColumnWidth calculates the optimal width for the combined Info column
|
||||
// based on the longest combined tags and history string, with a minimum of 12 and maximum of 60 characters
|
||||
// enterEditMode initializes edit mode for a specific host
|
||||
|
||||
// NewModel creates a new TUI model with the given SSH hosts
|
||||
func NewModel(hosts []config.SSHHost) Model {
|
||||
// Initialize history manager
|
||||
// Initialize the history manager
|
||||
historyManager, err := history.NewHistoryManager()
|
||||
if err != nil {
|
||||
// Log error but continue without history functionality
|
||||
// Log the error but continue without the history functionality
|
||||
fmt.Printf("Warning: Could not initialize history manager: %v\n", err)
|
||||
historyManager = nil
|
||||
}
|
||||
|
||||
// Create initial styles (will be updated on first WindowSizeMsg)
|
||||
styles := NewStyles(80) // Default width
|
||||
|
||||
// Create the model with default sorting by name
|
||||
m := Model{
|
||||
hosts: hosts,
|
||||
historyManager: historyManager,
|
||||
sortMode: SortByName,
|
||||
styles: styles,
|
||||
width: 80,
|
||||
height: 24,
|
||||
ready: false,
|
||||
viewMode: ViewList,
|
||||
}
|
||||
|
||||
// Sort hosts based on default sort mode
|
||||
// Sort hosts according to the default sort mode
|
||||
sortedHosts := m.sortHosts(hosts)
|
||||
|
||||
// Create search input
|
||||
// Create the search input
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "Search hosts or tags..."
|
||||
ti.CharLimit = 50
|
||||
@@ -530,7 +72,7 @@ func NewModel(hosts []config.SSHHost) Model {
|
||||
// Format tags for display
|
||||
var tagsStr string
|
||||
if len(host.Tags) > 0 {
|
||||
// Add # prefix to each tag and join with spaces
|
||||
// Add the # prefix to each tag and join them with spaces
|
||||
var formattedTags []string
|
||||
for _, tag := range host.Tags {
|
||||
formattedTags = append(formattedTags, "#"+tag)
|
||||
@@ -538,7 +80,7 @@ func NewModel(hosts []config.SSHHost) Model {
|
||||
tagsStr = strings.Join(formattedTags, " ")
|
||||
}
|
||||
|
||||
// Format last login info
|
||||
// Format last login information
|
||||
var lastLoginStr string
|
||||
if historyManager != nil {
|
||||
if lastConnect, exists := historyManager.GetLastConnectionTime(host.Name); exists {
|
||||
@@ -556,7 +98,7 @@ func NewModel(hosts []config.SSHHost) Model {
|
||||
})
|
||||
}
|
||||
|
||||
// Déterminer la hauteur du tableau : 1 (header) + nombre de hosts (max 10)
|
||||
// Determine table height: 1 (header) + number of hosts (max 10)
|
||||
hostCount := len(rows)
|
||||
tableHeight := 1 // header
|
||||
if hostCount < 10 {
|
||||
@@ -565,7 +107,7 @@ func NewModel(hosts []config.SSHHost) Model {
|
||||
tableHeight += 10
|
||||
}
|
||||
|
||||
// Create table
|
||||
// Create the table
|
||||
t := table.New(
|
||||
table.WithColumns(columns),
|
||||
table.WithRows(rows),
|
||||
@@ -577,13 +119,11 @@ func NewModel(hosts []config.SSHHost) Model {
|
||||
s := table.DefaultStyles()
|
||||
s.Header = s.Header.
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240")).
|
||||
BorderForeground(lipgloss.Color(SecondaryColor)).
|
||||
BorderBottom(true).
|
||||
Bold(false)
|
||||
s.Selected = s.Selected.
|
||||
Foreground(lipgloss.Color("229")).
|
||||
Background(lipgloss.Color("57")).
|
||||
Bold(false)
|
||||
s.Selected = m.styles.Selected
|
||||
|
||||
t.SetStyles(s)
|
||||
|
||||
// Update the model with the table and other properties
|
||||
@@ -591,153 +131,22 @@ func NewModel(hosts []config.SSHHost) Model {
|
||||
m.searchInput = ti
|
||||
m.filteredHosts = sortedHosts
|
||||
|
||||
// Initialize table styles based on initial focus state
|
||||
m.updateTableStyles()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// RunInteractiveMode starts the interactive TUI
|
||||
// RunInteractiveMode starts the interactive TUI interface
|
||||
func RunInteractiveMode(hosts []config.SSHHost) error {
|
||||
for {
|
||||
m := NewModel(hosts)
|
||||
m := NewModel(hosts)
|
||||
|
||||
// Start the application in alt screen mode for clean exit
|
||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||
finalModel, err := p.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running TUI: %w", err)
|
||||
}
|
||||
|
||||
// Check if the final model indicates an action
|
||||
if model, ok := finalModel.(Model); ok {
|
||||
if model.exitAction == "edit" && model.exitHostName != "" {
|
||||
// Launch the dedicated edit form (opens in separate window)
|
||||
if err := RunEditForm(model.exitHostName); err != nil {
|
||||
fmt.Printf("Error editing host: %v\n", err)
|
||||
// Continue the loop to return to the main interface
|
||||
continue
|
||||
}
|
||||
|
||||
// Refresh the hosts list after editing
|
||||
refreshedHosts, err := config.ParseSSHConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error refreshing hosts after edit: %w", err)
|
||||
}
|
||||
hosts = refreshedHosts
|
||||
|
||||
// Continue the loop to return to the main interface
|
||||
continue
|
||||
} else if model.exitAction == "add" {
|
||||
// Launch the dedicated add form (opens in separate window)
|
||||
if err := RunAddForm(""); err != nil {
|
||||
fmt.Printf("Error adding host: %v\n", err)
|
||||
// Continue the loop to return to the main interface
|
||||
continue
|
||||
}
|
||||
|
||||
// Refresh the hosts list after adding
|
||||
refreshedHosts, err := config.ParseSSHConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error refreshing hosts after add: %w", err)
|
||||
}
|
||||
hosts = refreshedHosts
|
||||
|
||||
// Continue the loop to return to the main interface
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If no special command, exit normally
|
||||
break
|
||||
// Start the application in alt screen mode for clean output
|
||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||
_, err := p.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running TUI: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterHosts filters hosts based on search query (name or tags)
|
||||
func (m Model) filterHosts(query string) []config.SSHHost {
|
||||
var filtered []config.SSHHost
|
||||
|
||||
if query == "" {
|
||||
filtered = m.hosts
|
||||
} else {
|
||||
query = strings.ToLower(query)
|
||||
|
||||
for _, host := range m.hosts {
|
||||
// Check host name
|
||||
if strings.Contains(strings.ToLower(host.Name), query) {
|
||||
filtered = append(filtered, host)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check hostname
|
||||
if strings.Contains(strings.ToLower(host.Hostname), query) {
|
||||
filtered = append(filtered, host)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check tags
|
||||
for _, tag := range host.Tags {
|
||||
if strings.Contains(strings.ToLower(tag), query) {
|
||||
filtered = append(filtered, host)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m.sortHosts(filtered)
|
||||
}
|
||||
|
||||
// updateTableRows updates the table with filtered hosts
|
||||
func (m *Model) updateTableRows() {
|
||||
var rows []table.Row
|
||||
hostsToShow := m.filteredHosts
|
||||
if hostsToShow == nil {
|
||||
hostsToShow = m.hosts
|
||||
}
|
||||
|
||||
for _, host := range hostsToShow {
|
||||
// Format tags for display
|
||||
var tagsStr string
|
||||
if len(host.Tags) > 0 {
|
||||
// Add # prefix to each tag and join with spaces
|
||||
var formattedTags []string
|
||||
for _, tag := range host.Tags {
|
||||
formattedTags = append(formattedTags, "#"+tag)
|
||||
}
|
||||
tagsStr = strings.Join(formattedTags, " ")
|
||||
}
|
||||
|
||||
// Format last login info
|
||||
var lastLoginStr string
|
||||
if m.historyManager != nil {
|
||||
if lastConnect, exists := m.historyManager.GetLastConnectionTime(host.Name); exists {
|
||||
lastLoginStr = formatTimeAgo(lastConnect)
|
||||
}
|
||||
}
|
||||
|
||||
rows = append(rows, table.Row{
|
||||
host.Name,
|
||||
host.Hostname,
|
||||
host.User,
|
||||
host.Port,
|
||||
tagsStr,
|
||||
lastLoginStr,
|
||||
})
|
||||
}
|
||||
|
||||
m.table.SetRows(rows)
|
||||
}
|
||||
|
||||
// enterEditMode initializes edit mode for a specific host
|
||||
// renderDeleteConfirmation renders the delete confirmation dialog
|
||||
func (m Model) renderDeleteConfirmation() string {
|
||||
var view strings.Builder
|
||||
|
||||
view.WriteString(lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("1")). // Red border
|
||||
Padding(1, 2).
|
||||
Render(fmt.Sprintf("⚠️ Delete SSH Host\n\nAre you sure you want to delete host '%s'?\n\nThis action cannot be undone.\n\nPress Enter to confirm or Esc to cancel", m.deleteHost)))
|
||||
|
||||
return view.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user