mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2025-10-18 17:07:19 +02:00
feat: add support for SSH RemoteCommand and RequestTTY in host configuration and TUI forms
- Allow users to specify a RemoteCommand to execute on SSH connection, both via TUI and config file - Add RequestTTY option (yes, no, force, auto) to host configuration and forms - Update config parsing and writing to handle new fields - Improve TUI forms to support editing and adding these options - Fix edit form standalone mode to allow proper quit/save via keyboard shortcuts
This commit is contained in:
parent
12d97270f0
commit
c1457af73a
12
cmd/root.go
12
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
|
||||
|
@ -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 != "" {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user