mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-09-06 21:00:45 +02:00
feat: add SSH connection history with TUI sorting by last login
This commit is contained in:
parent
534b7d9a6c
commit
01f2b4e6be
208
internal/history/history.go
Normal file
208
internal/history/history.go
Normal file
@ -0,0 +1,208 @@
|
||||
package history
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"sshm/internal/config"
|
||||
)
|
||||
|
||||
// ConnectionHistory represents the history of SSH connections
|
||||
type ConnectionHistory struct {
|
||||
Connections map[string]ConnectionInfo `json:"connections"`
|
||||
}
|
||||
|
||||
// ConnectionInfo stores information about a specific connection
|
||||
type ConnectionInfo struct {
|
||||
HostName string `json:"host_name"`
|
||||
LastConnect time.Time `json:"last_connect"`
|
||||
ConnectCount int `json:"connect_count"`
|
||||
}
|
||||
|
||||
// HistoryManager manages the connection history
|
||||
type HistoryManager struct {
|
||||
historyPath string
|
||||
history *ConnectionHistory
|
||||
}
|
||||
|
||||
// NewHistoryManager creates a new history manager
|
||||
func NewHistoryManager() (*HistoryManager, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
historyPath := filepath.Join(homeDir, ".ssh", "sshm_history.json")
|
||||
|
||||
hm := &HistoryManager{
|
||||
historyPath: historyPath,
|
||||
history: &ConnectionHistory{Connections: make(map[string]ConnectionInfo)},
|
||||
}
|
||||
|
||||
// Load existing history if it exists
|
||||
err = hm.loadHistory()
|
||||
if err != nil {
|
||||
// If file doesn't exist, that's okay - we'll create it when needed
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return hm, nil
|
||||
}
|
||||
|
||||
// loadHistory loads the connection history from the JSON file
|
||||
func (hm *HistoryManager) loadHistory() error {
|
||||
data, err := os.ReadFile(hm.historyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, hm.history)
|
||||
}
|
||||
|
||||
// saveHistory saves the connection history to the JSON file
|
||||
func (hm *HistoryManager) saveHistory() error {
|
||||
// Ensure the directory exists
|
||||
dir := filepath.Dir(hm.historyPath)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(hm.history, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(hm.historyPath, data, 0600)
|
||||
}
|
||||
|
||||
// RecordConnection records a new connection for the specified host
|
||||
func (hm *HistoryManager) RecordConnection(hostName string) error {
|
||||
now := time.Now()
|
||||
|
||||
if conn, exists := hm.history.Connections[hostName]; exists {
|
||||
// Update existing connection
|
||||
conn.LastConnect = now
|
||||
conn.ConnectCount++
|
||||
hm.history.Connections[hostName] = conn
|
||||
} else {
|
||||
// Create new connection record
|
||||
hm.history.Connections[hostName] = ConnectionInfo{
|
||||
HostName: hostName,
|
||||
LastConnect: now,
|
||||
ConnectCount: 1,
|
||||
}
|
||||
}
|
||||
|
||||
return hm.saveHistory()
|
||||
}
|
||||
|
||||
// GetLastConnectionTime returns the last connection time for a host
|
||||
func (hm *HistoryManager) GetLastConnectionTime(hostName string) (time.Time, bool) {
|
||||
if conn, exists := hm.history.Connections[hostName]; exists {
|
||||
return conn.LastConnect, true
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// GetConnectionCount returns the total number of connections for a host
|
||||
func (hm *HistoryManager) GetConnectionCount(hostName string) int {
|
||||
if conn, exists := hm.history.Connections[hostName]; exists {
|
||||
return conn.ConnectCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// SortHostsByLastUsed sorts hosts by their last connection time (most recent first)
|
||||
func (hm *HistoryManager) SortHostsByLastUsed(hosts []config.SSHHost) []config.SSHHost {
|
||||
sorted := make([]config.SSHHost, len(hosts))
|
||||
copy(sorted, hosts)
|
||||
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
timeI, existsI := hm.GetLastConnectionTime(sorted[i].Name)
|
||||
timeJ, existsJ := hm.GetLastConnectionTime(sorted[j].Name)
|
||||
|
||||
// If both have history, sort by most recent first
|
||||
if existsI && existsJ {
|
||||
return timeI.After(timeJ)
|
||||
}
|
||||
|
||||
// Hosts with history come before hosts without history
|
||||
if existsI && !existsJ {
|
||||
return true
|
||||
}
|
||||
if !existsI && existsJ {
|
||||
return false
|
||||
}
|
||||
|
||||
// If neither has history, sort alphabetically
|
||||
return sorted[i].Name < sorted[j].Name
|
||||
})
|
||||
|
||||
return sorted
|
||||
}
|
||||
|
||||
// SortHostsByMostUsed sorts hosts by their connection count (most used first)
|
||||
func (hm *HistoryManager) SortHostsByMostUsed(hosts []config.SSHHost) []config.SSHHost {
|
||||
sorted := make([]config.SSHHost, len(hosts))
|
||||
copy(sorted, hosts)
|
||||
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
countI := hm.GetConnectionCount(sorted[i].Name)
|
||||
countJ := hm.GetConnectionCount(sorted[j].Name)
|
||||
|
||||
// If counts are different, sort by count (highest first)
|
||||
if countI != countJ {
|
||||
return countI > countJ
|
||||
}
|
||||
|
||||
// If counts are equal, sort by most recent
|
||||
timeI, existsI := hm.GetLastConnectionTime(sorted[i].Name)
|
||||
timeJ, existsJ := hm.GetLastConnectionTime(sorted[j].Name)
|
||||
|
||||
if existsI && existsJ {
|
||||
return timeI.After(timeJ)
|
||||
}
|
||||
|
||||
// If neither has history, sort alphabetically
|
||||
return sorted[i].Name < sorted[j].Name
|
||||
})
|
||||
|
||||
return sorted
|
||||
}
|
||||
|
||||
// CleanupOldEntries removes connection history for hosts that no longer exist
|
||||
func (hm *HistoryManager) CleanupOldEntries(currentHosts []config.SSHHost) error {
|
||||
// Create a set of current host names
|
||||
currentHostNames := make(map[string]bool)
|
||||
for _, host := range currentHosts {
|
||||
currentHostNames[host.Name] = true
|
||||
}
|
||||
|
||||
// Remove entries for hosts that no longer exist
|
||||
for hostName := range hm.history.Connections {
|
||||
if !currentHostNames[hostName] {
|
||||
delete(hm.history.Connections, hostName)
|
||||
}
|
||||
}
|
||||
|
||||
return hm.saveHistory()
|
||||
}
|
||||
|
||||
// GetAllConnectionsInfo returns all connection information sorted by last connection time
|
||||
func (hm *HistoryManager) GetAllConnectionsInfo() []ConnectionInfo {
|
||||
var connections []ConnectionInfo
|
||||
for _, conn := range hm.history.Connections {
|
||||
connections = append(connections, conn)
|
||||
}
|
||||
|
||||
sort.Slice(connections, func(i, j int) bool {
|
||||
return connections[i].LastConnect.After(connections[j].LastConnect)
|
||||
})
|
||||
|
||||
return connections
|
||||
}
|
@ -5,8 +5,10 @@ import (
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sshm/internal/config"
|
||||
"sshm/internal/history"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
@ -36,16 +38,36 @@ 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
|
||||
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 {
|
||||
@ -135,6 +157,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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()
|
||||
})
|
||||
@ -170,24 +202,65 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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)
|
||||
}
|
||||
|
||||
// Always filter hosts when search input changes (regardless of mode)
|
||||
if m.searchInput.Value() != "" {
|
||||
m.filteredHosts = m.filterHosts(m.searchInput.Value())
|
||||
} else {
|
||||
m.filteredHosts = m.hosts
|
||||
}
|
||||
m.updateTableRows()
|
||||
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
@ -204,11 +277,17 @@ func (m Model) View() string {
|
||||
// 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\n")
|
||||
view.WriteString(searchStyleFocused.Render(searchPrompt+m.searchInput.View()) + "\n")
|
||||
} else {
|
||||
view.WriteString(searchStyleUnfocused.Render(searchPrompt+m.searchInput.View()) + "\n\n")
|
||||
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
|
||||
@ -226,7 +305,8 @@ func (m Model) View() string {
|
||||
|
||||
// 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 • q/ESC to quit")
|
||||
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")
|
||||
}
|
||||
@ -234,6 +314,22 @@ func (m Model) View() string {
|
||||
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))
|
||||
@ -269,7 +365,7 @@ func calculateNameColumnWidth(hosts []config.SSHHost) int {
|
||||
}
|
||||
|
||||
// calculateTagsColumnWidth calculates the optimal width for the Tags column
|
||||
// based on the longest tags string, with a minimum of 8 and maximum of 50 characters
|
||||
// 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
|
||||
|
||||
@ -294,17 +390,114 @@ func calculateTagsColumnWidth(hosts []config.SSHHost) int {
|
||||
maxLength += 2
|
||||
|
||||
// Cap the maximum width to avoid extremely wide columns
|
||||
if maxLength > 50 {
|
||||
maxLength = 50
|
||||
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 {
|
||||
// Sort hosts alphabetically by name
|
||||
sortedHosts := sortHostsByName(hosts)
|
||||
// Initialize history manager
|
||||
historyManager, err := history.NewHistoryManager()
|
||||
if err != nil {
|
||||
// Log error but continue without history functionality
|
||||
fmt.Printf("Warning: Could not initialize history manager: %v\n", err)
|
||||
historyManager = nil
|
||||
}
|
||||
|
||||
// Create the model with default sorting by name
|
||||
m := Model{
|
||||
hosts: hosts,
|
||||
historyManager: historyManager,
|
||||
sortMode: SortByName,
|
||||
}
|
||||
|
||||
// Sort hosts based on default sort mode
|
||||
sortedHosts := m.sortHosts(hosts)
|
||||
|
||||
// Create search input
|
||||
ti := textinput.New()
|
||||
@ -317,6 +510,9 @@ func NewModel(hosts []config.SSHHost) Model {
|
||||
|
||||
// Calculate optimal width for the Tags column
|
||||
tagsWidth := calculateTagsColumnWidth(sortedHosts)
|
||||
|
||||
// Calculate optimal width for the Last Login column
|
||||
lastLoginWidth := calculateLastLoginColumnWidth(sortedHosts, historyManager)
|
||||
|
||||
// Create table columns
|
||||
columns := []table.Column{
|
||||
@ -325,6 +521,7 @@ func NewModel(hosts []config.SSHHost) Model {
|
||||
{Title: "User", Width: 12},
|
||||
{Title: "Port", Width: 6},
|
||||
{Title: "Tags", Width: tagsWidth},
|
||||
{Title: "Last Login", Width: lastLoginWidth},
|
||||
}
|
||||
|
||||
// Convert hosts to table rows
|
||||
@ -341,12 +538,21 @@ func NewModel(hosts []config.SSHHost) Model {
|
||||
tagsStr = strings.Join(formattedTags, " ")
|
||||
}
|
||||
|
||||
// Format last login info
|
||||
var lastLoginStr string
|
||||
if historyManager != nil {
|
||||
if lastConnect, exists := historyManager.GetLastConnectionTime(host.Name); exists {
|
||||
lastLoginStr = formatTimeAgo(lastConnect)
|
||||
}
|
||||
}
|
||||
|
||||
rows = append(rows, table.Row{
|
||||
host.Name,
|
||||
host.Hostname,
|
||||
host.User,
|
||||
host.Port,
|
||||
tagsStr,
|
||||
lastLoginStr,
|
||||
})
|
||||
}
|
||||
|
||||
@ -380,13 +586,12 @@ func NewModel(hosts []config.SSHHost) Model {
|
||||
Bold(false)
|
||||
t.SetStyles(s)
|
||||
|
||||
return Model{
|
||||
table: t,
|
||||
searchInput: ti,
|
||||
hosts: sortedHosts,
|
||||
filteredHosts: sortedHosts,
|
||||
searchMode: false,
|
||||
}
|
||||
// Update the model with the table and other properties
|
||||
m.table = t
|
||||
m.searchInput = ti
|
||||
m.filteredHosts = sortedHosts
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// RunInteractiveMode starts the interactive TUI
|
||||
@ -449,36 +654,37 @@ func RunInteractiveMode(hosts []config.SSHHost) error {
|
||||
|
||||
// filterHosts filters hosts based on search query (name or tags)
|
||||
func (m Model) filterHosts(query string) []config.SSHHost {
|
||||
if query == "" {
|
||||
return sortHostsByName(m.hosts)
|
||||
}
|
||||
|
||||
query = strings.ToLower(query)
|
||||
var filtered []config.SSHHost
|
||||
|
||||
for _, host := range m.hosts {
|
||||
// Check host name
|
||||
if strings.Contains(strings.ToLower(host.Name), query) {
|
||||
filtered = append(filtered, host)
|
||||
continue
|
||||
}
|
||||
if query == "" {
|
||||
filtered = m.hosts
|
||||
} else {
|
||||
query = strings.ToLower(query)
|
||||
|
||||
// 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) {
|
||||
for _, host := range m.hosts {
|
||||
// Check host name
|
||||
if strings.Contains(strings.ToLower(host.Name), query) {
|
||||
filtered = append(filtered, host)
|
||||
break
|
||||
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 sortHostsByName(filtered)
|
||||
return m.sortHosts(filtered)
|
||||
}
|
||||
|
||||
// updateTableRows updates the table with filtered hosts
|
||||
@ -489,10 +695,7 @@ func (m *Model) updateTableRows() {
|
||||
hostsToShow = m.hosts
|
||||
}
|
||||
|
||||
// Sort hosts alphabetically by name
|
||||
sortedHosts := sortHostsByName(hostsToShow)
|
||||
|
||||
for _, host := range sortedHosts {
|
||||
for _, host := range hostsToShow {
|
||||
// Format tags for display
|
||||
var tagsStr string
|
||||
if len(host.Tags) > 0 {
|
||||
@ -504,12 +707,21 @@ func (m *Model) updateTableRows() {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user