feat: support custom SSH config files via -c flag

This commit is contained in:
2025-09-02 17:00:17 +02:00
parent 98aa2b6579
commit 94225cbfbe
8 changed files with 613 additions and 49 deletions

View File

@@ -181,15 +181,18 @@ func ParseSSHConfigFile(configPath string) ([]SSHHost, error) {
// AddSSHHost adds a new SSH host to the config file
func AddSSHHost(host SSHHost) error {
configMutex.Lock()
defer configMutex.Unlock()
homeDir, err := os.UserHomeDir()
if err != nil {
return err
}
configPath := filepath.Join(homeDir, ".ssh", "config")
return AddSSHHostToFile(host, configPath)
}
// AddSSHHostToFile adds a new SSH host to a specific config file
func AddSSHHostToFile(host SSHHost, configPath string) error {
configMutex.Lock()
defer configMutex.Unlock()
// Create backup before modification if file exists
if _, err := os.Stat(configPath); err == nil {
@@ -198,8 +201,8 @@ func AddSSHHost(host SSHHost) error {
}
}
// Check if host already exists
exists, err := HostExists(host.Name)
// Check if host already exists in the specified config file
exists, err := HostExistsInFile(host.Name, configPath)
if err != nil {
return err
}
@@ -354,6 +357,21 @@ func HostExists(hostName string) (bool, error) {
return false, nil
}
// HostExistsInFile checks if a host exists in a specific config file
func HostExistsInFile(hostName string, configPath string) (bool, error) {
hosts, err := ParseSSHConfigFile(configPath)
if err != nil {
return false, err
}
for _, host := range hosts {
if host.Name == hostName {
return true, nil
}
}
return false, nil
}
// GetSSHHost retrieves a specific host configuration by name
func GetSSHHost(hostName string) (*SSHHost, error) {
hosts, err := ParseSSHConfig()
@@ -369,17 +387,35 @@ func GetSSHHost(hostName string) (*SSHHost, error) {
return nil, fmt.Errorf("host '%s' not found", hostName)
}
// GetSSHHostFromFile retrieves a specific host configuration by name from a specific config file
func GetSSHHostFromFile(hostName string, configPath string) (*SSHHost, error) {
hosts, err := ParseSSHConfigFile(configPath)
if err != nil {
return nil, err
}
for _, host := range hosts {
if host.Name == hostName {
return &host, nil
}
}
return nil, fmt.Errorf("host '%s' not found", hostName)
}
// UpdateSSHHost updates an existing SSH host configuration
func UpdateSSHHost(oldName string, newHost SSHHost) error {
configMutex.Lock()
defer configMutex.Unlock()
homeDir, err := os.UserHomeDir()
if err != nil {
return err
}
configPath := filepath.Join(homeDir, ".ssh", "config")
return UpdateSSHHostInFile(oldName, newHost, configPath)
}
// UpdateSSHHostInFile updates an existing SSH host configuration in a specific file
func UpdateSSHHostInFile(oldName string, newHost SSHHost, configPath string) error {
configMutex.Lock()
defer configMutex.Unlock()
// Create backup before modification
if err := backupConfig(configPath); err != nil {
@@ -528,15 +564,18 @@ func UpdateSSHHost(oldName string, newHost SSHHost) error {
// DeleteSSHHost removes an SSH host configuration from the config file
func DeleteSSHHost(hostName string) error {
configMutex.Lock()
defer configMutex.Unlock()
homeDir, err := os.UserHomeDir()
if err != nil {
return err
}
configPath := filepath.Join(homeDir, ".ssh", "config")
return DeleteSSHHostFromFile(hostName, configPath)
}
// DeleteSSHHostFromFile deletes an SSH host from a specific config file
func DeleteSSHHostFromFile(hostName, configPath string) error {
configMutex.Lock()
defer configMutex.Unlock()
// Create backup before modification
if err := backupConfig(configPath); err != nil {

View File

@@ -13,17 +13,18 @@ import (
)
type addFormModel struct {
inputs []textinput.Model
focused int
err string
styles Styles
success bool
width int
height int
inputs []textinput.Model
focused int
err string
styles Styles
success bool
width int
height int
configFile string
}
// NewAddForm creates a new add form model
func NewAddForm(hostname string, styles Styles, width, height int) *addFormModel {
func NewAddForm(hostname string, styles Styles, width, height int, configFile string) *addFormModel {
// Get current user for default
currentUser, _ := user.Current()
defaultUser := "root"
@@ -100,11 +101,12 @@ func NewAddForm(hostname string, styles Styles, width, height int) *addFormModel
inputs[tagsInput].Width = 50
return &addFormModel{
inputs: inputs,
focused: nameInput,
styles: styles,
width: width,
height: height,
inputs: inputs,
focused: nameInput,
styles: styles,
width: width,
height: height,
configFile: configFile,
}
}
@@ -270,7 +272,7 @@ func (m standaloneAddForm) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// RunAddForm provides backward compatibility for standalone add form
func RunAddForm(hostname string) error {
styles := NewStyles(80)
addForm := NewAddForm(hostname, styles, 80, 24)
addForm := NewAddForm(hostname, styles, 80, 24, "")
m := standaloneAddForm{addForm}
p := tea.NewProgram(m, tea.WithAltScreen())
@@ -327,7 +329,12 @@ func (m *addFormModel) submitForm() tea.Cmd {
}
// Add to config
err := config.AddSSHHost(host)
var err error
if m.configFile != "" {
err = config.AddSSHHostToFile(host, m.configFile)
} else {
err = config.AddSSHHost(host)
}
return addFormSubmitMsg{hostname: name, err: err}
}
}

View File

@@ -18,12 +18,21 @@ type editFormModel struct {
originalName string
width int
height int
configFile string
}
// NewEditForm creates a new edit form model
func NewEditForm(hostName string, styles Styles, width, height int) (*editFormModel, error) {
func NewEditForm(hostName string, styles Styles, width, height int, configFile string) (*editFormModel, error) {
// Get the existing host configuration
host, err := config.GetSSHHost(hostName)
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
}
@@ -93,6 +102,7 @@ func NewEditForm(hostName string, styles Styles, width, height int) (*editFormMo
inputs: inputs,
focused: nameInput,
originalName: hostName,
configFile: configFile,
styles: styles,
width: width,
height: height,
@@ -250,7 +260,7 @@ func (m standaloneEditForm) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// RunEditForm provides backward compatibility for standalone edit form
func RunEditForm(hostName string) error {
styles := NewStyles(80)
editForm, err := NewEditForm(hostName, styles, 80, 24)
editForm, err := NewEditForm(hostName, styles, 80, 24, "")
if err != nil {
return err
}
@@ -308,7 +318,12 @@ func (m *editFormModel) submitEditForm() tea.Cmd {
}
// Update the configuration
err := config.UpdateSSHHost(m.originalName, host)
var err error
if m.configFile != "" {
err = config.UpdateSSHHostInFile(m.originalName, host, m.configFile)
} else {
err = config.UpdateSSHHost(m.originalName, host)
}
return editFormSubmitMsg{hostname: name, err: err}
}
}

View File

@@ -48,6 +48,7 @@ type Model struct {
deleteHost string
historyManager *history.HistoryManager
sortMode SortMode
configFile string // Path to the SSH config file
// View management
viewMode ViewMode

View File

@@ -14,7 +14,7 @@ import (
)
// NewModel creates a new TUI model with the given SSH hosts
func NewModel(hosts []config.SSHHost) Model {
func NewModel(hosts []config.SSHHost, configFile string) Model {
// Initialize the history manager
historyManager, err := history.NewHistoryManager()
if err != nil {
@@ -31,6 +31,7 @@ func NewModel(hosts []config.SSHHost) Model {
hosts: hosts,
historyManager: historyManager,
sortMode: SortByName,
configFile: configFile,
styles: styles,
width: 80,
height: 24,
@@ -138,8 +139,8 @@ func NewModel(hosts []config.SSHHost) Model {
}
// RunInteractiveMode starts the interactive TUI interface
func RunInteractiveMode(hosts []config.SSHHost) error {
m := NewModel(hosts)
func RunInteractiveMode(hosts []config.SSHHost, configFile string) error {
m := NewModel(hosts, configFile)
// Start the application in alt screen mode for clean output
p := tea.NewProgram(m, tea.WithAltScreen())

View File

@@ -53,7 +53,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
} else {
// Success: refresh hosts and return to list view
hosts, err := config.ParseSSHConfig()
var hosts []config.SSHHost
var err error
if m.configFile != "" {
hosts, err = config.ParseSSHConfigFile(m.configFile)
} else {
hosts, err = config.ParseSSHConfig()
}
if err != nil {
return m, tea.Quit
}
@@ -82,7 +90,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
} else {
// Success: refresh hosts and return to list view
hosts, err := config.ParseSSHConfig()
var hosts []config.SSHHost
var err error
if m.configFile != "" {
hosts, err = config.ParseSSHConfigFile(m.configFile)
} else {
hosts, err = config.ParseSSHConfig()
}
if err != nil {
return m, tea.Quit
}
@@ -183,7 +199,12 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
} else if m.deleteMode {
// Confirm deletion
err := config.DeleteSSHHost(m.deleteHost)
var err error
if m.configFile != "" {
err = config.DeleteSSHHostFromFile(m.deleteHost, m.configFile)
} else {
err = config.DeleteSSHHost(m.deleteHost)
}
if err != nil {
// Could display an error message here
m.deleteMode = false
@@ -192,8 +213,16 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
// Refresh the hosts list
hosts, err := config.ParseSSHConfig()
if err != nil {
var hosts []config.SSHHost
var parseErr error
if m.configFile != "" {
hosts, parseErr = config.ParseSSHConfigFile(m.configFile)
} else {
hosts, parseErr = config.ParseSSHConfig()
}
if parseErr != nil {
// Could display an error message here
m.deleteMode = false
m.deleteHost = ""
@@ -222,7 +251,15 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
}
}
return m, tea.ExecProcess(exec.Command("ssh", hostName), func(err error) tea.Msg {
// Build the SSH command with the appropriate config file
var sshCmd *exec.Cmd
if m.configFile != "" {
sshCmd = exec.Command("ssh", "-F", m.configFile, hostName)
} else {
sshCmd = exec.Command("ssh", hostName)
}
return m, tea.ExecProcess(sshCmd, func(err error) tea.Msg {
return tea.Quit()
})
}
@@ -233,7 +270,7 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
selected := m.table.SelectedRow()
if len(selected) > 0 {
hostName := selected[0] // The hostname is in the first column
editForm, err := NewEditForm(hostName, m.styles, m.width, m.height)
editForm, err := NewEditForm(hostName, m.styles, m.width, m.height, m.configFile)
if err != nil {
// Handle error - could show in UI
return m, nil
@@ -246,7 +283,7 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "a":
if !m.searchMode && !m.deleteMode {
// Add a new host
m.addForm = NewAddForm("", m.styles, m.width, m.height)
m.addForm = NewAddForm("", m.styles, m.width, m.height, m.configFile)
m.viewMode = ViewAdd
return m, textinput.Blink
}