diff --git a/cmd/root.go b/cmd/root.go index b8d44ae..75b2c4e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -140,11 +140,17 @@ func connectToHost(hostName string) { fmt.Printf("Connecting to %s...\n", hostName) var sshCmd *exec.Cmd + var args []string + if configFile != "" { - sshCmd = exec.Command("ssh", "-F", configFile, hostName) - } else { - sshCmd = exec.Command("ssh", hostName) + args = append(args, "-F", configFile) } + args = append(args, hostName) + + // Note: We don't add RemoteCommand here because if it's configured in SSH config, + // SSH will handle it automatically. Adding it as a command line argument would conflict. + + sshCmd = exec.Command("ssh", args...) // Set up the command to use the same stdin, stdout, and stderr as the parent process sshCmd.Stdin = os.Stdin diff --git a/internal/config/ssh.go b/internal/config/ssh.go index abd1f3d..9f7ae91 100644 --- a/internal/config/ssh.go +++ b/internal/config/ssh.go @@ -13,15 +13,17 @@ import ( // SSHHost represents an SSH host configuration type SSHHost struct { - Name string - Hostname string - User string - Port string - Identity string - ProxyJump string - Options string - Tags []string - SourceFile string // Path to the config file where this host is defined + Name string + Hostname string + User string + Port string + Identity string + ProxyJump string + Options string + RemoteCommand string // Command to execute after SSH connection + RequestTTY string // Request TTY (yes, no, force, auto) + Tags []string + SourceFile string // Path to the config file where this host is defined // Temporary field to handle multiple aliases during parsing aliasNames []string `json:"-"` // Do not serialize this field @@ -326,6 +328,14 @@ func parseSSHConfigFileWithProcessedFiles(configPath string, processedFiles map[ if currentHost != nil { currentHost.ProxyJump = value } + case "remotecommand": + if currentHost != nil { + currentHost.RemoteCommand = value + } + case "requesttty": + if currentHost != nil { + currentHost.RequestTTY = value + } default: // Handle other SSH options if currentHost != nil && strings.TrimSpace(line) != "" { @@ -603,6 +613,20 @@ func AddSSHHostToFile(host SSHHost, configPath string) error { } } + if host.RemoteCommand != "" { + _, err = file.WriteString(fmt.Sprintf(" RemoteCommand %s\n", host.RemoteCommand)) + if err != nil { + return err + } + } + + if host.RequestTTY != "" { + _, err = file.WriteString(fmt.Sprintf(" RequestTTY %s\n", host.RequestTTY)) + if err != nil { + return err + } + } + // Write SSH options if host.Options != "" { // Split options by newlines and write each one @@ -1020,6 +1044,12 @@ func UpdateSSHHostInFile(oldName string, newHost SSHHost, configPath string) err if newHost.ProxyJump != "" { newLines = append(newLines, " ProxyJump "+newHost.ProxyJump) } + if newHost.RemoteCommand != "" { + newLines = append(newLines, " RemoteCommand "+newHost.RemoteCommand) + } + if newHost.RequestTTY != "" { + newLines = append(newLines, " RequestTTY "+newHost.RequestTTY) + } // Write SSH options if newHost.Options != "" { options := strings.Split(newHost.Options, "\n") @@ -1068,6 +1098,12 @@ func UpdateSSHHostInFile(oldName string, newHost SSHHost, configPath string) err if newHost.ProxyJump != "" { newLines = append(newLines, " ProxyJump "+newHost.ProxyJump) } + if newHost.RemoteCommand != "" { + newLines = append(newLines, " RemoteCommand "+newHost.RemoteCommand) + } + if newHost.RequestTTY != "" { + newLines = append(newLines, " RequestTTY "+newHost.RequestTTY) + } // Write SSH options if newHost.Options != "" { options := strings.Split(newHost.Options, "\n") @@ -1152,6 +1188,12 @@ func UpdateSSHHostInFile(oldName string, newHost SSHHost, configPath string) err if newHost.ProxyJump != "" { newLines = append(newLines, " ProxyJump "+newHost.ProxyJump) } + if newHost.RemoteCommand != "" { + newLines = append(newLines, " RemoteCommand "+newHost.RemoteCommand) + } + if newHost.RequestTTY != "" { + newLines = append(newLines, " RequestTTY "+newHost.RequestTTY) + } // Write SSH options if newHost.Options != "" { options := strings.Split(newHost.Options, "\n") @@ -1200,6 +1242,12 @@ func UpdateSSHHostInFile(oldName string, newHost SSHHost, configPath string) err if newHost.ProxyJump != "" { newLines = append(newLines, " ProxyJump "+newHost.ProxyJump) } + if newHost.RemoteCommand != "" { + newLines = append(newLines, " RemoteCommand "+newHost.RemoteCommand) + } + if newHost.RequestTTY != "" { + newLines = append(newLines, " RequestTTY "+newHost.RequestTTY) + } // Write SSH options if newHost.Options != "" { options := strings.Split(newHost.Options, "\n") @@ -1694,6 +1742,12 @@ func UpdateMultiHostBlock(originalHosts, newHosts []string, commonProperties SSH if commonProperties.ProxyJump != "" { newLines = append(newLines, " ProxyJump "+commonProperties.ProxyJump) } + if commonProperties.RemoteCommand != "" { + newLines = append(newLines, " RemoteCommand "+commonProperties.RemoteCommand) + } + if commonProperties.RequestTTY != "" { + newLines = append(newLines, " RequestTTY "+commonProperties.RequestTTY) + } // Write SSH options if commonProperties.Options != "" { @@ -1774,6 +1828,12 @@ func UpdateMultiHostBlock(originalHosts, newHosts []string, commonProperties SSH if commonProperties.ProxyJump != "" { newLines = append(newLines, " ProxyJump "+commonProperties.ProxyJump) } + if commonProperties.RemoteCommand != "" { + newLines = append(newLines, " RemoteCommand "+commonProperties.RemoteCommand) + } + if commonProperties.RequestTTY != "" { + newLines = append(newLines, " RequestTTY "+commonProperties.RequestTTY) + } // Write SSH options if commonProperties.Options != "" { diff --git a/internal/ui/add_form.go b/internal/ui/add_form.go index 8e02325..8a9a830 100644 --- a/internal/ui/add_form.go +++ b/internal/ui/add_form.go @@ -47,7 +47,7 @@ func NewAddForm(hostname string, styles Styles, width, height int, configFile st } } - inputs := make([]textinput.Model, 8) + inputs := make([]textinput.Model, 10) // Increased from 9 to 10 for RequestTTY // Name input inputs[nameInput] = textinput.New() @@ -101,6 +101,18 @@ func NewAddForm(hostname string, styles Styles, width, height int, configFile st inputs[tagsInput].CharLimit = 200 inputs[tagsInput].Width = 50 + // Remote Command input + inputs[remoteCommandInput] = textinput.New() + inputs[remoteCommandInput].Placeholder = "ls -la, htop, bash" + inputs[remoteCommandInput].CharLimit = 300 + inputs[remoteCommandInput].Width = 70 + + // RequestTTY input + inputs[requestTTYInput] = textinput.New() + inputs[requestTTYInput].Placeholder = "yes, no, force, auto" + inputs[requestTTYInput].CharLimit = 10 + inputs[requestTTYInput].Width = 30 + return &addFormModel{ inputs: inputs, focused: nameInput, @@ -120,6 +132,8 @@ const ( proxyJumpInput optionsInput tagsInput + remoteCommandInput + requestTTYInput ) // Messages for communication with parent model @@ -225,6 +239,8 @@ func (m *addFormModel) View() string { "ProxyJump", "SSH Options", "Tags (comma-separated)", + "Remote Command", + "Request TTY", } for i, field := range fields { @@ -291,6 +307,8 @@ func (m *addFormModel) submitForm() tea.Cmd { identity := strings.TrimSpace(m.inputs[identityInput].Value()) proxyJump := strings.TrimSpace(m.inputs[proxyJumpInput].Value()) options := strings.TrimSpace(m.inputs[optionsInput].Value()) + remoteCommand := strings.TrimSpace(m.inputs[remoteCommandInput].Value()) + requestTTY := strings.TrimSpace(m.inputs[requestTTYInput].Value()) // Set defaults if user == "" { @@ -319,14 +337,16 @@ func (m *addFormModel) submitForm() tea.Cmd { // Create host configuration host := config.SSHHost{ - Name: name, - Hostname: hostname, - User: user, - Port: port, - Identity: identity, - ProxyJump: proxyJump, - Options: config.ParseSSHOptionsFromCommand(options), - Tags: tags, + Name: name, + Hostname: hostname, + User: user, + Port: port, + Identity: identity, + ProxyJump: proxyJump, + Options: config.ParseSSHOptionsFromCommand(options), + RemoteCommand: remoteCommand, + RequestTTY: requestTTY, + Tags: tags, } // Add to config diff --git a/internal/ui/edit_form.go b/internal/ui/edit_form.go index 69b1810..01ecefd 100644 --- a/internal/ui/edit_form.go +++ b/internal/ui/edit_form.go @@ -9,7 +9,6 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) const ( @@ -30,7 +29,6 @@ type editFormModel struct { focusArea int // 0=hosts, 1=properties focused int err string - success bool styles Styles originalName string originalHosts []string // Store original host names for multi-host detection @@ -92,7 +90,7 @@ func NewEditForm(hostName string, styles Styles, width, height int, configFile s } } - inputs := make([]textinput.Model, 7) // Reduced from 8 since we removed nameInput + inputs := make([]textinput.Model, 9) // Increased from 8 to 9 for RequestTTY // Hostname input inputs[0] = textinput.New() @@ -147,6 +145,20 @@ func NewEditForm(hostName string, styles Styles, width, height int, configFile s inputs[6].SetValue(strings.Join(host.Tags, ", ")) } + // Remote Command input + inputs[7] = textinput.New() + inputs[7].Placeholder = "ls -la, htop, bash" + inputs[7].CharLimit = 300 + inputs[7].Width = 70 + inputs[7].SetValue(host.RemoteCommand) + + // RequestTTY input + inputs[8] = textinput.New() + inputs[8].Placeholder = "yes, no, force, auto" + inputs[8].CharLimit = 10 + inputs[8].Width = 30 + inputs[8].SetValue(host.RequestTTY) + return &editFormModel{ hostInputs: hostInputs, inputs: inputs, @@ -247,7 +259,6 @@ func (m *editFormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c", "esc": m.err = "" - m.success = false return m, func() tea.Msg { return editFormCancelMsg{} } case "ctrl+s": @@ -306,10 +317,10 @@ func (m *editFormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case editFormSubmitMsg: if msg.err != nil { m.err = msg.err.Error() - m.success = false } else { - m.success = true - m.err = "" + // Success: let the wrapper handle this + // In TUI mode, this will be handled by the parent + // In standalone mode, the wrapper will quit } return m, nil } @@ -334,13 +345,6 @@ func (m *editFormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *editFormModel) View() string { var b strings.Builder - if m.success { - b.WriteString(m.styles.FormField.Foreground(lipgloss.Color("#10B981")).Render("✓ Host updated successfully!")) - b.WriteString("\n\n") - b.WriteString(m.styles.FormHelp.Render("Press Ctrl+C or Esc to go back")) - return b.String() - } - if m.err != "" { b.WriteString(m.styles.Error.Render("Error: " + m.err)) b.WriteString("\n\n") @@ -385,6 +389,8 @@ func (m *editFormModel) View() string { "Proxy Jump", "SSH Options", "Tags (comma-separated)", + "Remote Command", + "Request TTY", } for i, field := range fields { @@ -416,6 +422,30 @@ func (m *editFormModel) View() string { return b.String() } +// Standalone wrapper for edit form +type standaloneEditForm struct { + *editFormModel +} + +func (m standaloneEditForm) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case editFormSubmitMsg: + if msg.err != nil { + m.editFormModel.err = msg.err.Error() + return m, nil + } else { + // Success: quit the program + return m, tea.Quit + } + case editFormCancelMsg: + return m, tea.Quit + } + + newForm, cmd := m.editFormModel.Update(msg) + m.editFormModel = newForm.(*editFormModel) + return m, cmd +} + // RunEditForm runs the edit form as a standalone program func RunEditForm(hostName string, configFile string) error { styles := NewStyles(80) // Default width @@ -424,17 +454,10 @@ func RunEditForm(hostName string, configFile string) error { return err } - p := tea.NewProgram(editForm, tea.WithAltScreen()) + m := standaloneEditForm{editForm} + p := tea.NewProgram(m, tea.WithAltScreen()) _, err = p.Run() - if err != nil { - return err - } - - if editForm.err != "" { - return fmt.Errorf(editForm.err) - } - - return nil + return err } func (m *editFormModel) submitEditForm() tea.Cmd { @@ -453,12 +476,14 @@ func (m *editFormModel) submitEditForm() tea.Cmd { } // Get property values using direct indices - hostname := strings.TrimSpace(m.inputs[0].Value()) // hostnameInput - user := strings.TrimSpace(m.inputs[1].Value()) // userInput - port := strings.TrimSpace(m.inputs[2].Value()) // portInput - identity := strings.TrimSpace(m.inputs[3].Value()) // identityInput - proxyJump := strings.TrimSpace(m.inputs[4].Value()) // proxyJumpInput - options := strings.TrimSpace(m.inputs[5].Value()) // optionsInput + hostname := strings.TrimSpace(m.inputs[0].Value()) // hostnameInput + user := strings.TrimSpace(m.inputs[1].Value()) // userInput + port := strings.TrimSpace(m.inputs[2].Value()) // portInput + identity := strings.TrimSpace(m.inputs[3].Value()) // identityInput + proxyJump := strings.TrimSpace(m.inputs[4].Value()) // proxyJumpInput + options := strings.TrimSpace(m.inputs[5].Value()) // optionsInput + remoteCommand := strings.TrimSpace(m.inputs[7].Value()) // remoteCommandInput + requestTTY := strings.TrimSpace(m.inputs[8].Value()) // requestTTYInput // Set defaults if port == "" { @@ -491,13 +516,15 @@ func (m *editFormModel) submitEditForm() tea.Cmd { // Create the common host configuration commonHost := config.SSHHost{ - Hostname: hostname, - User: user, - Port: port, - Identity: identity, - ProxyJump: proxyJump, - Options: options, - Tags: tags, + Hostname: hostname, + User: user, + Port: port, + Identity: identity, + ProxyJump: proxyJump, + Options: options, + RemoteCommand: remoteCommand, + RequestTTY: requestTTY, + Tags: tags, } var err error