mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2026-01-27 03:04:21 +01: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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user