mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-09-07 21:30:39 +02:00
feat: refactor TUI with read-only info view and optimized layout
- Add new 'i' command for read-only host information display - Implement info view with option to switch to edit mode (e/Enter) - Hide User and Port columns to optimize table space usage - Improve table height calculation for better host visibility - Add proper message handling for info view navigation - Interface optimization
This commit is contained in:
parent
2ade315ddc
commit
1cea3795e4
109
internal/ui/help_form.go
Normal file
109
internal/ui/help_form.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
type helpModel struct {
|
||||||
|
styles Styles
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpCloseMsg is sent when the help window is closed
|
||||||
|
type helpCloseMsg struct{}
|
||||||
|
|
||||||
|
// NewHelpForm creates a new help form model
|
||||||
|
func NewHelpForm(styles Styles, width, height int) *helpModel {
|
||||||
|
return &helpModel{
|
||||||
|
styles: styles,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *helpModel) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *helpModel) Update(msg tea.Msg) (*helpModel, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc", "q", "h", "enter", "ctrl+c":
|
||||||
|
return m, func() tea.Msg { return helpCloseMsg{} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *helpModel) View() string {
|
||||||
|
// Title
|
||||||
|
title := m.styles.Header.Render("📖 SSHM - Help & Commands")
|
||||||
|
|
||||||
|
// Create horizontal sections with compact layout
|
||||||
|
line1 := lipgloss.JoinHorizontal(lipgloss.Center,
|
||||||
|
m.styles.FocusedLabel.Render("🧭 ↑/↓/j/k"),
|
||||||
|
" ",
|
||||||
|
m.styles.HelpText.Render("navigate"),
|
||||||
|
" ",
|
||||||
|
m.styles.FocusedLabel.Render("⏎"),
|
||||||
|
" ",
|
||||||
|
m.styles.HelpText.Render("connect"),
|
||||||
|
" ",
|
||||||
|
m.styles.FocusedLabel.Render("a/e/d"),
|
||||||
|
" ",
|
||||||
|
m.styles.HelpText.Render("add/edit/delete"),
|
||||||
|
)
|
||||||
|
|
||||||
|
line2 := lipgloss.JoinHorizontal(lipgloss.Center,
|
||||||
|
m.styles.FocusedLabel.Render("Tab"),
|
||||||
|
" ",
|
||||||
|
m.styles.HelpText.Render("switch focus"),
|
||||||
|
" ",
|
||||||
|
m.styles.FocusedLabel.Render("f"),
|
||||||
|
" ",
|
||||||
|
m.styles.HelpText.Render("port forward"),
|
||||||
|
" ",
|
||||||
|
m.styles.FocusedLabel.Render("s/r/n"),
|
||||||
|
" ",
|
||||||
|
m.styles.HelpText.Render("sort modes"),
|
||||||
|
)
|
||||||
|
|
||||||
|
line3 := lipgloss.JoinHorizontal(lipgloss.Center,
|
||||||
|
m.styles.FocusedLabel.Render("/"),
|
||||||
|
" ",
|
||||||
|
m.styles.HelpText.Render("search"),
|
||||||
|
" ",
|
||||||
|
m.styles.FocusedLabel.Render("h"),
|
||||||
|
" ",
|
||||||
|
m.styles.HelpText.Render("help"),
|
||||||
|
" ",
|
||||||
|
m.styles.FocusedLabel.Render("q/ESC"),
|
||||||
|
" ",
|
||||||
|
m.styles.HelpText.Render("quit"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create the main content
|
||||||
|
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
|
title,
|
||||||
|
"",
|
||||||
|
line1,
|
||||||
|
"",
|
||||||
|
line2,
|
||||||
|
"",
|
||||||
|
line3,
|
||||||
|
"",
|
||||||
|
m.styles.HelpText.Render("Press ESC, h, q or Enter to close"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Center the help window
|
||||||
|
return lipgloss.Place(
|
||||||
|
m.width,
|
||||||
|
m.height,
|
||||||
|
lipgloss.Center,
|
||||||
|
lipgloss.Center,
|
||||||
|
m.styles.FormContainer.Render(content),
|
||||||
|
)
|
||||||
|
}
|
227
internal/ui/info_form.go
Normal file
227
internal/ui/info_form.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sshm/internal/config"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
type infoFormModel struct {
|
||||||
|
host *config.SSHHost
|
||||||
|
styles Styles
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
configFile string
|
||||||
|
hostName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages for communication with parent model
|
||||||
|
type infoFormEditMsg struct {
|
||||||
|
hostName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type infoFormCancelMsg struct{}
|
||||||
|
|
||||||
|
// NewInfoForm creates a new info form model for displaying host details in read-only mode
|
||||||
|
func NewInfoForm(hostName string, styles Styles, width, height int, configFile string) (*infoFormModel, error) {
|
||||||
|
// Get the existing host configuration
|
||||||
|
var host *config.SSHHost
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if configFile != "" {
|
||||||
|
host, err = config.GetSSHHostFromFile(hostName, configFile)
|
||||||
|
} else {
|
||||||
|
host, err = config.GetSSHHost(hostName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &infoFormModel{
|
||||||
|
host: host,
|
||||||
|
hostName: hostName,
|
||||||
|
configFile: configFile,
|
||||||
|
styles: styles,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *infoFormModel) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *infoFormModel) Update(msg tea.Msg) (*infoFormModel, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
m.styles = NewStyles(m.width)
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "esc", "q":
|
||||||
|
return m, func() tea.Msg { return infoFormCancelMsg{} }
|
||||||
|
|
||||||
|
case "e", "enter":
|
||||||
|
// Switch to edit mode
|
||||||
|
return m, func() tea.Msg { return infoFormEditMsg{hostName: m.hostName} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *infoFormModel) View() string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
// Title
|
||||||
|
title := fmt.Sprintf("SSH Host Information: %s", m.host.Name)
|
||||||
|
b.WriteString(m.styles.FormTitle.Render(title))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
|
// Create info sections with consistent formatting
|
||||||
|
sections := []struct {
|
||||||
|
label string
|
||||||
|
value string
|
||||||
|
}{
|
||||||
|
{"Host Name", m.host.Name},
|
||||||
|
{"Hostname/IP", m.host.Hostname},
|
||||||
|
{"User", formatOptionalValue(m.host.User)},
|
||||||
|
{"Port", formatOptionalValue(m.host.Port)},
|
||||||
|
{"Identity File", formatOptionalValue(m.host.Identity)},
|
||||||
|
{"ProxyJump", formatOptionalValue(m.host.ProxyJump)},
|
||||||
|
{"SSH Options", formatSSHOptions(m.host.Options)},
|
||||||
|
{"Tags", formatTags(m.host.Tags)},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render each section
|
||||||
|
for _, section := range sections {
|
||||||
|
// Label style
|
||||||
|
labelStyle := lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(lipgloss.Color("39")). // Bright blue
|
||||||
|
Width(15).
|
||||||
|
AlignHorizontal(lipgloss.Right)
|
||||||
|
|
||||||
|
// Value style
|
||||||
|
valueStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("255")) // White
|
||||||
|
|
||||||
|
// If value is empty or default, use a muted style
|
||||||
|
if section.value == "Not set" || section.value == "22" && section.label == "Port" {
|
||||||
|
valueStyle = valueStyle.Foreground(lipgloss.Color("243")) // Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
line := lipgloss.JoinHorizontal(
|
||||||
|
lipgloss.Top,
|
||||||
|
labelStyle.Render(section.label+":"),
|
||||||
|
" ",
|
||||||
|
valueStyle.Render(section.value),
|
||||||
|
)
|
||||||
|
b.WriteString(line)
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
// Action instructions
|
||||||
|
helpStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("243")).
|
||||||
|
Italic(true)
|
||||||
|
|
||||||
|
b.WriteString(helpStyle.Render("Actions:"))
|
||||||
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
actionStyle := lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("120")). // Green
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
b.WriteString(" ")
|
||||||
|
b.WriteString(actionStyle.Render("e/Enter"))
|
||||||
|
b.WriteString(helpStyle.Render(" - Switch to edit mode"))
|
||||||
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
b.WriteString(" ")
|
||||||
|
b.WriteString(actionStyle.Render("q/Esc"))
|
||||||
|
b.WriteString(helpStyle.Render(" - Return to host list"))
|
||||||
|
|
||||||
|
// Wrap in a border for better visual separation
|
||||||
|
content := b.String()
|
||||||
|
|
||||||
|
borderStyle := lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("39")).
|
||||||
|
Padding(1).
|
||||||
|
Margin(1)
|
||||||
|
|
||||||
|
// Center the info window
|
||||||
|
return lipgloss.Place(
|
||||||
|
m.width,
|
||||||
|
m.height,
|
||||||
|
lipgloss.Center,
|
||||||
|
lipgloss.Center,
|
||||||
|
borderStyle.Render(content),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for formatting values
|
||||||
|
|
||||||
|
func formatOptionalValue(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return "Not set"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSSHOptions(options string) string {
|
||||||
|
if options == "" {
|
||||||
|
return "Not set"
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTags(tags []string) string {
|
||||||
|
if len(tags) == 0 {
|
||||||
|
return "Not set"
|
||||||
|
}
|
||||||
|
return strings.Join(tags, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standalone wrapper for info form (for testing or standalone use)
|
||||||
|
type standaloneInfoForm struct {
|
||||||
|
*infoFormModel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m standaloneInfoForm) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg.(type) {
|
||||||
|
case infoFormCancelMsg:
|
||||||
|
return m, tea.Quit
|
||||||
|
case infoFormEditMsg:
|
||||||
|
// For standalone mode, just quit - parent should handle edit transition
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
newForm, cmd := m.infoFormModel.Update(msg)
|
||||||
|
m.infoFormModel = newForm
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunInfoForm provides a standalone info form for testing
|
||||||
|
func RunInfoForm(hostName string, configFile string) error {
|
||||||
|
styles := NewStyles(80)
|
||||||
|
infoForm, err := NewInfoForm(hostName, styles, 80, 24, configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m := standaloneInfoForm{infoForm}
|
||||||
|
|
||||||
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
|
_, err = p.Run()
|
||||||
|
return err
|
||||||
|
}
|
@ -35,7 +35,9 @@ const (
|
|||||||
ViewList ViewMode = iota
|
ViewList ViewMode = iota
|
||||||
ViewAdd
|
ViewAdd
|
||||||
ViewEdit
|
ViewEdit
|
||||||
|
ViewInfo
|
||||||
ViewPortForward
|
ViewPortForward
|
||||||
|
ViewHelp
|
||||||
)
|
)
|
||||||
|
|
||||||
// PortForwardType defines the type of port forwarding
|
// PortForwardType defines the type of port forwarding
|
||||||
@ -77,7 +79,9 @@ type Model struct {
|
|||||||
viewMode ViewMode
|
viewMode ViewMode
|
||||||
addForm *addFormModel
|
addForm *addFormModel
|
||||||
editForm *editFormModel
|
editForm *editFormModel
|
||||||
|
infoForm *infoFormModel
|
||||||
portForwardForm *portForwardModel
|
portForwardForm *portForwardModel
|
||||||
|
helpForm *helpModel
|
||||||
|
|
||||||
// Terminal size and styles
|
// Terminal size and styles
|
||||||
width int
|
width int
|
||||||
|
@ -42,6 +42,7 @@ type Styles struct {
|
|||||||
FormContainer lipgloss.Style
|
FormContainer lipgloss.Style
|
||||||
Label lipgloss.Style
|
Label lipgloss.Style
|
||||||
FocusedLabel lipgloss.Style
|
FocusedLabel lipgloss.Style
|
||||||
|
HelpSection lipgloss.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStyles creates a new Styles struct with the given terminal width
|
// NewStyles creates a new Styles struct with the given terminal width
|
||||||
@ -88,8 +89,7 @@ func NewStyles(width int) Styles {
|
|||||||
Foreground(lipgloss.Color(SecondaryColor)),
|
Foreground(lipgloss.Color(SecondaryColor)),
|
||||||
|
|
||||||
HelpText: lipgloss.NewStyle().
|
HelpText: lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color(SecondaryColor)).
|
Foreground(lipgloss.Color(SecondaryColor)),
|
||||||
MarginTop(1),
|
|
||||||
|
|
||||||
// Error style
|
// Error style
|
||||||
Error: lipgloss.NewStyle().
|
Error: lipgloss.NewStyle().
|
||||||
@ -118,8 +118,10 @@ func NewStyles(width int) Styles {
|
|||||||
Foreground(lipgloss.Color(SecondaryColor)),
|
Foreground(lipgloss.Color(SecondaryColor)),
|
||||||
|
|
||||||
FocusedLabel: lipgloss.NewStyle().
|
FocusedLabel: lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color(PrimaryColor)).
|
Foreground(lipgloss.Color(PrimaryColor)),
|
||||||
Bold(true),
|
|
||||||
|
HelpSection: lipgloss.NewStyle().
|
||||||
|
Padding(0, 2),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,8 +122,8 @@ func (m *Model) updateTableRows() {
|
|||||||
rows = append(rows, table.Row{
|
rows = append(rows, table.Row{
|
||||||
host.Name,
|
host.Name,
|
||||||
host.Hostname,
|
host.Hostname,
|
||||||
host.User,
|
// host.User, // Commented to save space
|
||||||
host.Port,
|
// host.Port, // Commented to save space
|
||||||
tagsStr,
|
tagsStr,
|
||||||
lastLoginStr,
|
lastLoginStr,
|
||||||
})
|
})
|
||||||
@ -142,40 +142,24 @@ func (m *Model) updateTableHeight() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate dynamic table height based on terminal size
|
|
||||||
// Layout breakdown:
|
|
||||||
// - ASCII title: 5 lines (1 empty + 4 text lines)
|
|
||||||
// - Search bar: 1 line
|
|
||||||
// - Sort info: 1 line
|
|
||||||
// - Help text: 2 lines (multi-line text)
|
|
||||||
// - App margins/spacing: 2 lines
|
|
||||||
// Total reserved: 11 lines, mais réduisons à 7 pour forcer plus d'espace
|
|
||||||
reservedHeight := 7 // Réduction agressive pour tester
|
|
||||||
availableHeight := m.height - reservedHeight
|
|
||||||
hostCount := len(m.table.Rows())
|
hostCount := len(m.table.Rows())
|
||||||
|
|
||||||
// Minimum height should be at least 5 rows for usability
|
// Calculate exactly what we need:
|
||||||
minTableHeight := 6 // 1 header + 5 data rows
|
// 1 line for header + actual number of host rows + 1 extra line for better UX
|
||||||
maxTableHeight := availableHeight
|
tableHeight := 1 + hostCount + 1
|
||||||
if maxTableHeight < minTableHeight {
|
|
||||||
maxTableHeight = minTableHeight
|
// Set a reasonable maximum based on terminal height
|
||||||
|
// Leave space for: title (5) + search (1) + help (1) + margins (2) = 9 lines
|
||||||
|
// But be less conservative, use 7 lines instead of 9
|
||||||
|
maxPossibleHeight := m.height - 7
|
||||||
|
if maxPossibleHeight < 4 {
|
||||||
|
maxPossibleHeight = 4 // Minimum: header + 3 rows
|
||||||
}
|
}
|
||||||
|
|
||||||
tableHeight := 1 // header
|
if tableHeight > maxPossibleHeight {
|
||||||
dataRowsNeeded := hostCount
|
tableHeight = maxPossibleHeight
|
||||||
maxDataRows := maxTableHeight - 1 // subtract 1 for header
|
|
||||||
|
|
||||||
if dataRowsNeeded <= maxDataRows {
|
|
||||||
// We have enough space for all hosts
|
|
||||||
tableHeight += dataRowsNeeded
|
|
||||||
} else {
|
|
||||||
// We need to limit to available space
|
|
||||||
tableHeight += maxDataRows
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FORCE: Ajoutons une ligne supplémentaire pour résoudre le problème
|
|
||||||
tableHeight += 1
|
|
||||||
|
|
||||||
// Update table height
|
// Update table height
|
||||||
m.table.SetHeight(tableHeight)
|
m.table.SetHeight(tableHeight)
|
||||||
}
|
}
|
||||||
@ -198,11 +182,11 @@ func (m *Model) updateTableColumns() {
|
|||||||
|
|
||||||
// Fixed column widths
|
// Fixed column widths
|
||||||
hostnameWidth := 25
|
hostnameWidth := 25
|
||||||
userWidth := 12
|
// userWidth := 12 // Commented to save space
|
||||||
portWidth := 6
|
// portWidth := 6 // Commented to save space
|
||||||
|
|
||||||
// Calculate total width needed for all columns
|
// Calculate total width needed for all columns
|
||||||
totalFixedWidth := hostnameWidth + userWidth + portWidth
|
totalFixedWidth := hostnameWidth // + userWidth + portWidth // Commented columns
|
||||||
totalVariableWidth := nameWidth + tagsWidth + lastLoginWidth
|
totalVariableWidth := nameWidth + tagsWidth + lastLoginWidth
|
||||||
totalWidth := totalFixedWidth + totalVariableWidth
|
totalWidth := totalFixedWidth + totalVariableWidth
|
||||||
|
|
||||||
@ -226,14 +210,25 @@ func (m *Model) updateTableColumns() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new columns with updated widths
|
// Create new columns with updated widths and sort indicators
|
||||||
|
nameTitle := "Name"
|
||||||
|
lastLoginTitle := "Last Login"
|
||||||
|
|
||||||
|
// Add sort indicators based on current sort mode
|
||||||
|
switch m.sortMode {
|
||||||
|
case SortByName:
|
||||||
|
nameTitle += " ↓"
|
||||||
|
case SortByLastUsed:
|
||||||
|
lastLoginTitle += " ↓"
|
||||||
|
}
|
||||||
|
|
||||||
columns := []table.Column{
|
columns := []table.Column{
|
||||||
{Title: "Name", Width: nameWidth},
|
{Title: nameTitle, Width: nameWidth},
|
||||||
{Title: "Hostname", Width: hostnameWidth},
|
{Title: "Hostname", Width: hostnameWidth},
|
||||||
{Title: "User", Width: userWidth},
|
// {Title: "User", Width: userWidth}, // Commented to save space
|
||||||
{Title: "Port", Width: portWidth},
|
// {Title: "Port", Width: portWidth}, // Commented to save space
|
||||||
{Title: "Tags", Width: tagsWidth},
|
{Title: "Tags", Width: tagsWidth},
|
||||||
{Title: "Last Login", Width: lastLoginWidth},
|
{Title: lastLoginTitle, Width: lastLoginWidth},
|
||||||
}
|
}
|
||||||
|
|
||||||
m.table.SetColumns(columns)
|
m.table.SetColumns(columns)
|
||||||
|
@ -61,8 +61,8 @@ func NewModel(hosts []config.SSHHost, configFile string) Model {
|
|||||||
columns := []table.Column{
|
columns := []table.Column{
|
||||||
{Title: "Name", Width: nameWidth},
|
{Title: "Name", Width: nameWidth},
|
||||||
{Title: "Hostname", Width: 25},
|
{Title: "Hostname", Width: 25},
|
||||||
{Title: "User", Width: 12},
|
// {Title: "User", Width: 12}, // Commented to save space
|
||||||
{Title: "Port", Width: 6},
|
// {Title: "Port", Width: 6}, // Commented to save space
|
||||||
{Title: "Tags", Width: tagsWidth},
|
{Title: "Tags", Width: tagsWidth},
|
||||||
{Title: "Last Login", Width: lastLoginWidth},
|
{Title: "Last Login", Width: lastLoginWidth},
|
||||||
}
|
}
|
||||||
@ -92,8 +92,8 @@ func NewModel(hosts []config.SSHHost, configFile string) Model {
|
|||||||
rows = append(rows, table.Row{
|
rows = append(rows, table.Row{
|
||||||
host.Name,
|
host.Name,
|
||||||
host.Hostname,
|
host.Hostname,
|
||||||
host.User,
|
// host.User, // Commented to save space
|
||||||
host.Port,
|
// host.Port, // Commented to save space
|
||||||
tagsStr,
|
tagsStr,
|
||||||
lastLoginStr,
|
lastLoginStr,
|
||||||
})
|
})
|
||||||
|
@ -46,11 +46,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.editForm.height = m.height
|
m.editForm.height = m.height
|
||||||
m.editForm.styles = m.styles
|
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 {
|
if m.portForwardForm != nil {
|
||||||
m.portForwardForm.width = m.width
|
m.portForwardForm.width = m.width
|
||||||
m.portForwardForm.height = m.height
|
m.portForwardForm.height = m.height
|
||||||
m.portForwardForm.styles = m.styles
|
m.portForwardForm.styles = m.styles
|
||||||
}
|
}
|
||||||
|
if m.helpForm != nil {
|
||||||
|
m.helpForm.width = m.width
|
||||||
|
m.helpForm.height = m.height
|
||||||
|
m.helpForm.styles = m.styles
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case addFormSubmitMsg:
|
case addFormSubmitMsg:
|
||||||
@ -141,6 +151,28 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.table.Focus()
|
m.table.Focus()
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case infoFormCancelMsg:
|
||||||
|
// Cancel: return to list view
|
||||||
|
m.viewMode = ViewList
|
||||||
|
m.infoForm = nil
|
||||||
|
m.table.Focus()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
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:
|
case portForwardSubmitMsg:
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
// Show error in form
|
// Show error in form
|
||||||
@ -180,6 +212,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.table.Focus()
|
m.table.Focus()
|
||||||
return m, nil
|
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:
|
case tea.KeyMsg:
|
||||||
// Handle view-specific key presses
|
// Handle view-specific key presses
|
||||||
switch m.viewMode {
|
switch m.viewMode {
|
||||||
@ -197,6 +236,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.editForm = newForm
|
m.editForm = newForm
|
||||||
return m, cmd
|
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:
|
case ViewPortForward:
|
||||||
if m.portForwardForm != nil {
|
if m.portForwardForm != nil {
|
||||||
var newForm *portForwardModel
|
var newForm *portForwardModel
|
||||||
@ -204,6 +250,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.portForwardForm = newForm
|
m.portForwardForm = newForm
|
||||||
return m, cmd
|
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 ViewList:
|
case ViewList:
|
||||||
// Handle list view keys
|
// Handle list view keys
|
||||||
return m.handleListViewKeys(msg)
|
return m.handleListViewKeys(msg)
|
||||||
@ -356,6 +409,22 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, textinput.Blink
|
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 := selected[0] // The hostname is in the 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":
|
case "a":
|
||||||
if !m.searchMode && !m.deleteMode {
|
if !m.searchMode && !m.deleteMode {
|
||||||
// Add a new host
|
// Add a new host
|
||||||
@ -386,6 +455,13 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, textinput.Blink
|
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":
|
case "s":
|
||||||
if !m.searchMode && !m.deleteMode {
|
if !m.searchMode && !m.deleteMode {
|
||||||
// Cycle through sort modes (only 2 modes now)
|
// Cycle through sort modes (only 2 modes now)
|
||||||
|
@ -23,10 +23,18 @@ func (m Model) View() string {
|
|||||||
if m.editForm != nil {
|
if m.editForm != nil {
|
||||||
return m.editForm.View()
|
return m.editForm.View()
|
||||||
}
|
}
|
||||||
|
case ViewInfo:
|
||||||
|
if m.infoForm != nil {
|
||||||
|
return m.infoForm.View()
|
||||||
|
}
|
||||||
case ViewPortForward:
|
case ViewPortForward:
|
||||||
if m.portForwardForm != nil {
|
if m.portForwardForm != nil {
|
||||||
return m.portForwardForm.View()
|
return m.portForwardForm.View()
|
||||||
}
|
}
|
||||||
|
case ViewHelp:
|
||||||
|
if m.helpForm != nil {
|
||||||
|
return m.helpForm.View()
|
||||||
|
}
|
||||||
case ViewList:
|
case ViewList:
|
||||||
return m.renderListView()
|
return m.renderListView()
|
||||||
}
|
}
|
||||||
@ -50,10 +58,6 @@ func (m Model) renderListView() string {
|
|||||||
components = append(components, m.styles.SearchUnfocused.Render(searchPrompt+m.searchInput.View()))
|
components = append(components, m.styles.SearchUnfocused.Render(searchPrompt+m.searchInput.View()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the sort mode indicator
|
|
||||||
sortInfo := fmt.Sprintf(" Sort: %s", m.sortMode.String())
|
|
||||||
components = append(components, m.styles.SortInfo.Render(sortInfo))
|
|
||||||
|
|
||||||
// Add the table with the appropriate style based on focus
|
// Add the table with the appropriate style based on focus
|
||||||
if m.searchMode {
|
if m.searchMode {
|
||||||
// The table is not focused, use the unfocused style
|
// The table is not focused, use the unfocused style
|
||||||
@ -66,9 +70,9 @@ func (m Model) renderListView() string {
|
|||||||
// Add the help text
|
// Add the help text
|
||||||
var helpText string
|
var helpText string
|
||||||
if !m.searchMode {
|
if !m.searchMode {
|
||||||
helpText = " Use ↑/↓ to navigate • Enter to connect • (a)dd • (e)dit • (d)elete • (f)orward • / to search • Tab to switch\n Sort: (s)witch • (r)ecent • (n)ame • q/ESC to quit"
|
helpText = " ↑/↓: navigate • Enter: connect • i: info • h: help • q: quit"
|
||||||
} else {
|
} else {
|
||||||
helpText = " Type to filter hosts • Enter to validate search • Tab to switch to table • ESC to quit"
|
helpText = " Type to filter • Enter: validate • Tab: switch • ESC: quit"
|
||||||
}
|
}
|
||||||
components = append(components, m.styles.HelpText.Render(helpText))
|
components = append(components, m.styles.HelpText.Render(helpText))
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user