mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-09-07 13:20:40 +02:00
• Add SourceFile field to SSHHost struct to track config file origins • Implement FindHostInAllConfigs() to locate hosts across all config files • Fix "host not found" errors when editing/deleting hosts from included files • Add GetAllConfigFiles() and GetAllConfigFilesFromBase() for config discovery • Create UpdateSSHHostV2() and DeleteSSHHostV2() for cross-file operations • Display config file source in edit and info forms for better visibility • Add intelligent file selector for host addition when multiple configs exist • Support -c parameter context with proper file resolution • Exclude .backup files from Include directive processing • Maintain backward compatibility with existing SSH config workflows Resolves limitation where hosts from included config files could be viewed but not edited, deleted, or properly managed through the interface.
229 lines
5.1 KiB
Go
229 lines
5.1 KiB
Go
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},
|
|
{"Config File", formatConfigFile(m.host.SourceFile)},
|
|
{"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
|
|
}
|