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
|
||||
ViewAdd
|
||||
ViewEdit
|
||||
ViewInfo
|
||||
ViewPortForward
|
||||
ViewHelp
|
||||
)
|
||||
|
||||
// PortForwardType defines the type of port forwarding
|
||||
@ -77,7 +79,9 @@ type Model struct {
|
||||
viewMode ViewMode
|
||||
addForm *addFormModel
|
||||
editForm *editFormModel
|
||||
infoForm *infoFormModel
|
||||
portForwardForm *portForwardModel
|
||||
helpForm *helpModel
|
||||
|
||||
// Terminal size and styles
|
||||
width int
|
||||
|
@ -42,6 +42,7 @@ type Styles struct {
|
||||
FormContainer lipgloss.Style
|
||||
Label lipgloss.Style
|
||||
FocusedLabel lipgloss.Style
|
||||
HelpSection lipgloss.Style
|
||||
}
|
||||
|
||||
// NewStyles creates a new Styles struct with the given terminal width
|
||||
@ -88,8 +89,7 @@ func NewStyles(width int) Styles {
|
||||
Foreground(lipgloss.Color(SecondaryColor)),
|
||||
|
||||
HelpText: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(SecondaryColor)).
|
||||
MarginTop(1),
|
||||
Foreground(lipgloss.Color(SecondaryColor)),
|
||||
|
||||
// Error style
|
||||
Error: lipgloss.NewStyle().
|
||||
@ -118,8 +118,10 @@ func NewStyles(width int) Styles {
|
||||
Foreground(lipgloss.Color(SecondaryColor)),
|
||||
|
||||
FocusedLabel: lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color(PrimaryColor)).
|
||||
Bold(true),
|
||||
Foreground(lipgloss.Color(PrimaryColor)),
|
||||
|
||||
HelpSection: lipgloss.NewStyle().
|
||||
Padding(0, 2),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,8 +122,8 @@ func (m *Model) updateTableRows() {
|
||||
rows = append(rows, table.Row{
|
||||
host.Name,
|
||||
host.Hostname,
|
||||
host.User,
|
||||
host.Port,
|
||||
// host.User, // Commented to save space
|
||||
// host.Port, // Commented to save space
|
||||
tagsStr,
|
||||
lastLoginStr,
|
||||
})
|
||||
@ -142,40 +142,24 @@ func (m *Model) updateTableHeight() {
|
||||
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())
|
||||
|
||||
// Minimum height should be at least 5 rows for usability
|
||||
minTableHeight := 6 // 1 header + 5 data rows
|
||||
maxTableHeight := availableHeight
|
||||
if maxTableHeight < minTableHeight {
|
||||
maxTableHeight = minTableHeight
|
||||
// Calculate exactly what we need:
|
||||
// 1 line for header + actual number of host rows + 1 extra line for better UX
|
||||
tableHeight := 1 + hostCount + 1
|
||||
|
||||
// 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
|
||||
dataRowsNeeded := hostCount
|
||||
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
|
||||
if tableHeight > maxPossibleHeight {
|
||||
tableHeight = maxPossibleHeight
|
||||
}
|
||||
|
||||
// FORCE: Ajoutons une ligne supplémentaire pour résoudre le problème
|
||||
tableHeight += 1
|
||||
|
||||
// Update table height
|
||||
m.table.SetHeight(tableHeight)
|
||||
}
|
||||
@ -198,11 +182,11 @@ func (m *Model) updateTableColumns() {
|
||||
|
||||
// Fixed column widths
|
||||
hostnameWidth := 25
|
||||
userWidth := 12
|
||||
portWidth := 6
|
||||
// userWidth := 12 // Commented to save space
|
||||
// portWidth := 6 // Commented to save space
|
||||
|
||||
// Calculate total width needed for all columns
|
||||
totalFixedWidth := hostnameWidth + userWidth + portWidth
|
||||
totalFixedWidth := hostnameWidth // + userWidth + portWidth // Commented columns
|
||||
totalVariableWidth := nameWidth + tagsWidth + lastLoginWidth
|
||||
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{
|
||||
{Title: "Name", Width: nameWidth},
|
||||
{Title: nameTitle, Width: nameWidth},
|
||||
{Title: "Hostname", Width: hostnameWidth},
|
||||
{Title: "User", Width: userWidth},
|
||||
{Title: "Port", Width: portWidth},
|
||||
// {Title: "User", Width: userWidth}, // Commented to save space
|
||||
// {Title: "Port", Width: portWidth}, // Commented to save space
|
||||
{Title: "Tags", Width: tagsWidth},
|
||||
{Title: "Last Login", Width: lastLoginWidth},
|
||||
{Title: lastLoginTitle, Width: lastLoginWidth},
|
||||
}
|
||||
|
||||
m.table.SetColumns(columns)
|
||||
|
@ -61,8 +61,8 @@ func NewModel(hosts []config.SSHHost, configFile string) Model {
|
||||
columns := []table.Column{
|
||||
{Title: "Name", Width: nameWidth},
|
||||
{Title: "Hostname", Width: 25},
|
||||
{Title: "User", Width: 12},
|
||||
{Title: "Port", Width: 6},
|
||||
// {Title: "User", Width: 12}, // Commented to save space
|
||||
// {Title: "Port", Width: 6}, // Commented to save space
|
||||
{Title: "Tags", Width: tagsWidth},
|
||||
{Title: "Last Login", Width: lastLoginWidth},
|
||||
}
|
||||
@ -92,8 +92,8 @@ func NewModel(hosts []config.SSHHost, configFile string) Model {
|
||||
rows = append(rows, table.Row{
|
||||
host.Name,
|
||||
host.Hostname,
|
||||
host.User,
|
||||
host.Port,
|
||||
// host.User, // Commented to save space
|
||||
// host.Port, // Commented to save space
|
||||
tagsStr,
|
||||
lastLoginStr,
|
||||
})
|
||||
|
@ -46,11 +46,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case addFormSubmitMsg:
|
||||
@ -141,6 +151,28 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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 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
|
||||
@ -180,6 +212,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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 {
|
||||
@ -197,6 +236,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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
|
||||
@ -204,6 +250,13 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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 ViewList:
|
||||
// Handle list view keys
|
||||
return m.handleListViewKeys(msg)
|
||||
@ -356,6 +409,22 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
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":
|
||||
if !m.searchMode && !m.deleteMode {
|
||||
// Add a new host
|
||||
@ -386,6 +455,13 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
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)
|
||||
|
@ -23,10 +23,18 @@ func (m Model) View() string {
|
||||
if m.editForm != nil {
|
||||
return m.editForm.View()
|
||||
}
|
||||
case ViewInfo:
|
||||
if m.infoForm != nil {
|
||||
return m.infoForm.View()
|
||||
}
|
||||
case ViewPortForward:
|
||||
if m.portForwardForm != nil {
|
||||
return m.portForwardForm.View()
|
||||
}
|
||||
case ViewHelp:
|
||||
if m.helpForm != nil {
|
||||
return m.helpForm.View()
|
||||
}
|
||||
case ViewList:
|
||||
return m.renderListView()
|
||||
}
|
||||
@ -50,10 +58,6 @@ func (m Model) renderListView() string {
|
||||
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
|
||||
if m.searchMode {
|
||||
// The table is not focused, use the unfocused style
|
||||
@ -66,9 +70,9 @@ func (m Model) renderListView() string {
|
||||
// Add the help text
|
||||
var helpText string
|
||||
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 {
|
||||
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))
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user