mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-09-07 21:30:39 +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.
160 lines
4.1 KiB
Go
160 lines
4.1 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// View renders the complete user interface
|
|
func (m Model) View() string {
|
|
if !m.ready {
|
|
return "Loading..."
|
|
}
|
|
|
|
// Handle different view modes
|
|
switch m.viewMode {
|
|
case ViewAdd:
|
|
if m.addForm != nil {
|
|
return m.addForm.View()
|
|
}
|
|
case ViewEdit:
|
|
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 ViewFileSelector:
|
|
if m.fileSelectorForm != nil {
|
|
return m.fileSelectorForm.View()
|
|
}
|
|
case ViewList:
|
|
return m.renderListView()
|
|
}
|
|
|
|
return m.renderListView()
|
|
}
|
|
|
|
// renderListView renders the main list interface
|
|
func (m Model) renderListView() string {
|
|
// Build the interface components
|
|
components := []string{}
|
|
|
|
// Add the ASCII title
|
|
components = append(components, m.styles.Header.Render(asciiTitle))
|
|
|
|
// Add the search bar with the appropriate style based on focus
|
|
searchPrompt := "Search (/ to focus): "
|
|
if m.searchMode {
|
|
components = append(components, m.styles.SearchFocused.Render(searchPrompt+m.searchInput.View()))
|
|
} else {
|
|
components = append(components, m.styles.SearchUnfocused.Render(searchPrompt+m.searchInput.View()))
|
|
}
|
|
|
|
// Add the table with the appropriate style based on focus
|
|
if m.searchMode {
|
|
// The table is not focused, use the unfocused style
|
|
components = append(components, m.styles.TableUnfocused.Render(m.table.View()))
|
|
} else {
|
|
// The table is focused, use the focused style with the primary color
|
|
components = append(components, m.styles.TableFocused.Render(m.table.View()))
|
|
}
|
|
|
|
// Add the help text
|
|
var helpText string
|
|
if !m.searchMode {
|
|
helpText = " ↑/↓: navigate • Enter: connect • i: info • h: help • q: quit"
|
|
} else {
|
|
helpText = " Type to filter • Enter: validate • Tab: switch • ESC: quit"
|
|
}
|
|
components = append(components, m.styles.HelpText.Render(helpText))
|
|
|
|
// Join all components vertically with appropriate spacing
|
|
mainView := m.styles.App.Render(
|
|
lipgloss.JoinVertical(
|
|
lipgloss.Left,
|
|
components...,
|
|
),
|
|
)
|
|
|
|
// If in delete mode, overlay the confirmation dialog
|
|
if m.deleteMode {
|
|
// Combine the main view with the confirmation dialog overlay
|
|
confirmation := m.renderDeleteConfirmation()
|
|
|
|
// Center the confirmation dialog on the screen
|
|
centeredConfirmation := lipgloss.Place(
|
|
m.width,
|
|
m.height,
|
|
lipgloss.Center,
|
|
lipgloss.Center,
|
|
confirmation,
|
|
)
|
|
|
|
return centeredConfirmation
|
|
}
|
|
|
|
return mainView
|
|
}
|
|
|
|
// renderDeleteConfirmation renders a clean delete confirmation dialog
|
|
func (m Model) renderDeleteConfirmation() string {
|
|
// Remove emojis (uncertain width depending on terminal) to stabilize the frame
|
|
title := "DELETE SSH HOST"
|
|
question := fmt.Sprintf("Are you sure you want to delete host '%s'?", m.deleteHost)
|
|
action := "This action cannot be undone."
|
|
help := "Enter: confirm • Esc: cancel"
|
|
|
|
// Individual styles (do not affect width via internal centering)
|
|
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("196"))
|
|
questionStyle := lipgloss.NewStyle()
|
|
actionStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("203"))
|
|
helpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
|
|
|
lines := []string{
|
|
titleStyle.Render(title),
|
|
"",
|
|
questionStyle.Render(question),
|
|
"",
|
|
actionStyle.Render(action),
|
|
"",
|
|
helpStyle.Render(help),
|
|
}
|
|
|
|
// Compute the real maximum width (ANSI-safe via lipgloss.Width)
|
|
maxw := 0
|
|
for _, ln := range lines {
|
|
w := lipgloss.Width(ln)
|
|
if w > maxw {
|
|
maxw = w
|
|
}
|
|
}
|
|
// Minimal width for aesthetics
|
|
if maxw < 40 {
|
|
maxw = 40
|
|
}
|
|
|
|
// Build the raw text block (without centering) then apply the container style
|
|
raw := strings.Join(lines, "\n")
|
|
|
|
// Container style: wider horizontal padding, stable border
|
|
box := lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(lipgloss.Color("196")).
|
|
PaddingTop(1).PaddingBottom(1).PaddingLeft(2).PaddingRight(2).
|
|
Width(maxw + 4) // +4 = internal margin (2 spaces of left/right padding)
|
|
|
|
return box.Render(raw)
|
|
}
|