mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2026-01-27 03:04:21 +01:00
fix: use line numbers to prevent deleting all duplicate SSH hosts when removing one
This commit is contained in:
@@ -25,6 +25,7 @@ type SSHHost struct {
|
|||||||
RequestTTY string // Request TTY (yes, no, force, auto)
|
RequestTTY string // Request TTY (yes, no, force, auto)
|
||||||
Tags []string
|
Tags []string
|
||||||
SourceFile string // Path to the config file where this host is defined
|
SourceFile string // Path to the config file where this host is defined
|
||||||
|
LineNumber int // Line number in the source file where this host block starts (1-indexed)
|
||||||
|
|
||||||
// Temporary field to handle multiple aliases during parsing
|
// Temporary field to handle multiple aliases during parsing
|
||||||
aliasNames []string `json:"-"` // Do not serialize this field
|
aliasNames []string `json:"-"` // Do not serialize this field
|
||||||
@@ -212,8 +213,10 @@ func parseSSHConfigFileWithProcessedFiles(configPath string, processedFiles map[
|
|||||||
var currentHost *SSHHost
|
var currentHost *SSHHost
|
||||||
var pendingTags []string
|
var pendingTags []string
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
|
lineNumber := 0
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
|
lineNumber++
|
||||||
line := strings.TrimSpace(scanner.Text())
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
// Ignore empty lines
|
// Ignore empty lines
|
||||||
@@ -300,6 +303,7 @@ func parseSSHConfigFileWithProcessedFiles(configPath string, processedFiles map[
|
|||||||
Port: "22", // Default port
|
Port: "22", // Default port
|
||||||
Tags: pendingTags, // Assign pending tags to this host
|
Tags: pendingTags, // Assign pending tags to this host
|
||||||
SourceFile: absPath, // Track which file this host comes from
|
SourceFile: absPath, // Track which file this host comes from
|
||||||
|
LineNumber: lineNumber, // Track the line number where Host declaration starts
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store additional host names for later processing
|
// Store additional host names for later processing
|
||||||
@@ -1334,11 +1338,21 @@ func UpdateSSHHostInFile(oldName string, newHost SSHHost, configPath string) err
|
|||||||
|
|
||||||
// DeleteSSHHost removes an SSH host configuration from the config file
|
// DeleteSSHHost removes an SSH host configuration from the config file
|
||||||
func DeleteSSHHost(hostName string) error {
|
func DeleteSSHHost(hostName string) error {
|
||||||
return DeleteSSHHostV2(hostName)
|
return DeleteSSHHostV2(hostName, 0) // Legacy: without line number
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSSHHostWithLine deletes a specific SSH host by name and line number
|
||||||
|
func DeleteSSHHostWithLine(host SSHHost) error {
|
||||||
|
return DeleteSSHHostFromFileWithLine(host.Name, host.SourceFile, host.LineNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSSHHostFromFile deletes an SSH host from a specific config file
|
// DeleteSSHHostFromFile deletes an SSH host from a specific config file
|
||||||
func DeleteSSHHostFromFile(hostName, configPath string) error {
|
func DeleteSSHHostFromFile(hostName, configPath string) error {
|
||||||
|
return DeleteSSHHostFromFileWithLine(hostName, configPath, 0) // Legacy: without line number
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSSHHostFromFileWithLine deletes an SSH host from a specific config file at a specific line
|
||||||
|
func DeleteSSHHostFromFileWithLine(hostName, configPath string, targetLineNumber int) error {
|
||||||
configMutex.Lock()
|
configMutex.Lock()
|
||||||
defer configMutex.Unlock()
|
defer configMutex.Unlock()
|
||||||
|
|
||||||
@@ -1365,11 +1379,13 @@ func DeleteSSHHostFromFile(hostName, configPath string) error {
|
|||||||
hostFound := false
|
hostFound := false
|
||||||
|
|
||||||
for i < len(lines) {
|
for i < len(lines) {
|
||||||
|
currentLineNumber := i + 1 // Convert 0-indexed to 1-indexed
|
||||||
line := strings.TrimSpace(lines[i])
|
line := strings.TrimSpace(lines[i])
|
||||||
|
|
||||||
// Check for tags comment followed by Host
|
// Check for tags comment followed by Host
|
||||||
if strings.HasPrefix(line, "# Tags:") && i+1 < len(lines) {
|
if strings.HasPrefix(line, "# Tags:") && i+1 < len(lines) {
|
||||||
nextLine := strings.TrimSpace(lines[i+1])
|
nextLine := strings.TrimSpace(lines[i+1])
|
||||||
|
nextLineNumber := i + 2 // The Host line is at i+1, so its 1-indexed number is i+2
|
||||||
|
|
||||||
// Check if this is a Host line that contains our target host
|
// Check if this is a Host line that contains our target host
|
||||||
if strings.HasPrefix(nextLine, "Host ") {
|
if strings.HasPrefix(nextLine, "Host ") {
|
||||||
@@ -1385,7 +1401,10 @@ func DeleteSSHHostFromFile(hostName, configPath string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetHostIndex != -1 {
|
// Only proceed if:
|
||||||
|
// 1. We found the host name
|
||||||
|
// 2. Either no line number was specified (targetLineNumber == 0) OR the line numbers match
|
||||||
|
if targetHostIndex != -1 && (targetLineNumber == 0 || nextLineNumber == targetLineNumber) {
|
||||||
hostFound = true
|
hostFound = true
|
||||||
|
|
||||||
if isMultiHost && len(hostNames) > 1 {
|
if isMultiHost && len(hostNames) > 1 {
|
||||||
@@ -1423,7 +1442,12 @@ func DeleteSSHHostFromFile(hostName, configPath string) error {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
// Copy remaining lines and break to prevent deleting other duplicates
|
||||||
|
for i < len(lines) {
|
||||||
|
newLines = append(newLines, lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
break
|
||||||
} else {
|
} else {
|
||||||
// Single host or last host in multi-host block, delete entire block
|
// Single host or last host in multi-host block, delete entire block
|
||||||
// Skip tags comment and Host line
|
// Skip tags comment and Host line
|
||||||
@@ -1439,7 +1463,12 @@ func DeleteSSHHostFromFile(hostName, configPath string) error {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
// Copy remaining lines and break to prevent deleting other duplicates
|
||||||
|
for i < len(lines) {
|
||||||
|
newLines = append(newLines, lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1459,7 +1488,10 @@ func DeleteSSHHostFromFile(hostName, configPath string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetHostIndex != -1 {
|
// Only proceed if:
|
||||||
|
// 1. We found the host name
|
||||||
|
// 2. Either no line number was specified (targetLineNumber == 0) OR the line numbers match
|
||||||
|
if targetHostIndex != -1 && (targetLineNumber == 0 || currentLineNumber == targetLineNumber) {
|
||||||
hostFound = true
|
hostFound = true
|
||||||
|
|
||||||
if isMultiHost && len(hostNames) > 1 {
|
if isMultiHost && len(hostNames) > 1 {
|
||||||
@@ -1494,7 +1526,12 @@ func DeleteSSHHostFromFile(hostName, configPath string) error {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
// Copy remaining lines and break to prevent deleting other duplicates
|
||||||
|
for i < len(lines) {
|
||||||
|
newLines = append(newLines, lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
break
|
||||||
} else {
|
} else {
|
||||||
// Single host, delete entire block
|
// Single host, delete entire block
|
||||||
// Skip Host line
|
// Skip Host line
|
||||||
@@ -1510,7 +1547,12 @@ func DeleteSSHHostFromFile(hostName, configPath string) error {
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
// Copy remaining lines and break to prevent deleting other duplicates
|
||||||
|
for i < len(lines) {
|
||||||
|
newLines = append(newLines, lines[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1593,15 +1635,15 @@ func UpdateSSHHostV2(oldName string, newHost SSHHost) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSSHHostV2 removes an SSH host configuration, searching in all config files
|
// DeleteSSHHostV2 removes an SSH host configuration, searching in all config files
|
||||||
func DeleteSSHHostV2(hostName string) error {
|
func DeleteSSHHostV2(hostName string, targetLineNumber int) error {
|
||||||
// Find the host to determine which file it's in
|
// Find the host to determine which file it's in
|
||||||
existingHost, err := FindHostInAllConfigs(hostName)
|
existingHost, err := FindHostInAllConfigs(hostName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the host from its source file
|
// Delete the host from its source file using line number if provided
|
||||||
return DeleteSSHHostFromFile(hostName, existingHost.SourceFile)
|
return DeleteSSHHostFromFileWithLine(hostName, existingHost.SourceFile, targetLineNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSSHHostWithFileSelection adds a new SSH host to a user-specified config file
|
// AddSSHHostWithFileSelection adds a new SSH host to a user-specified config file
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ type Model struct {
|
|||||||
filteredHosts []config.SSHHost
|
filteredHosts []config.SSHHost
|
||||||
searchMode bool
|
searchMode bool
|
||||||
deleteMode bool
|
deleteMode bool
|
||||||
deleteHost string
|
deleteHost *config.SSHHost // Host to be deleted (with line number for precise targeting)
|
||||||
historyManager *history.HistoryManager
|
historyManager *history.HistoryManager
|
||||||
pingManager *connectivity.PingManager
|
pingManager *connectivity.PingManager
|
||||||
sortMode SortMode
|
sortMode SortMode
|
||||||
|
|||||||
@@ -452,7 +452,7 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
if m.deleteMode {
|
if m.deleteMode {
|
||||||
// Exit delete mode
|
// Exit delete mode
|
||||||
m.deleteMode = false
|
m.deleteMode = false
|
||||||
m.deleteHost = ""
|
m.deleteHost = nil
|
||||||
m.table.Focus()
|
m.table.Focus()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -508,15 +508,13 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
} else if m.deleteMode {
|
} else if m.deleteMode {
|
||||||
// Confirm deletion
|
// Confirm deletion
|
||||||
var err error
|
var err error
|
||||||
if m.configFile != "" {
|
if m.deleteHost != nil {
|
||||||
err = config.DeleteSSHHostFromFile(m.deleteHost, m.configFile)
|
err = config.DeleteSSHHostWithLine(*m.deleteHost)
|
||||||
} else {
|
|
||||||
err = config.DeleteSSHHost(m.deleteHost)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Could display an error message here
|
// Could display an error message here
|
||||||
m.deleteMode = false
|
m.deleteMode = false
|
||||||
m.deleteHost = ""
|
m.deleteHost = nil
|
||||||
m.table.Focus()
|
m.table.Focus()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -533,7 +531,7 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
// Could display an error message here
|
// Could display an error message here
|
||||||
m.deleteMode = false
|
m.deleteMode = false
|
||||||
m.deleteHost = ""
|
m.deleteHost = nil
|
||||||
m.table.Focus()
|
m.table.Focus()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -548,7 +546,7 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
m.updateTableRows()
|
m.updateTableRows()
|
||||||
m.deleteMode = false
|
m.deleteMode = false
|
||||||
m.deleteHost = ""
|
m.deleteHost = nil
|
||||||
m.table.Focus()
|
m.table.Focus()
|
||||||
return m, nil
|
return m, nil
|
||||||
} else {
|
} else {
|
||||||
@@ -673,11 +671,13 @@ func (m Model) handleListViewKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
case "d":
|
case "d":
|
||||||
if !m.searchMode && !m.deleteMode {
|
if !m.searchMode && !m.deleteMode {
|
||||||
// Delete the selected host
|
// Delete the selected host
|
||||||
selected := m.table.SelectedRow()
|
cursor := m.table.Cursor()
|
||||||
if len(selected) > 0 {
|
if cursor >= 0 && cursor < len(m.filteredHosts) {
|
||||||
hostName := extractHostNameFromTableRow(selected[0]) // Extract hostname from first column
|
// Get the host at the cursor position (which corresponds to filteredHosts index)
|
||||||
|
targetHost := &m.filteredHosts[cursor]
|
||||||
|
|
||||||
m.deleteMode = true
|
m.deleteMode = true
|
||||||
m.deleteHost = hostName
|
m.deleteHost = targetHost
|
||||||
m.table.Blur()
|
m.table.Blur()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,11 @@ func (m Model) renderListView() string {
|
|||||||
func (m Model) renderDeleteConfirmation() string {
|
func (m Model) renderDeleteConfirmation() string {
|
||||||
// Remove emojis (uncertain width depending on terminal) to stabilize the frame
|
// Remove emojis (uncertain width depending on terminal) to stabilize the frame
|
||||||
title := "DELETE SSH HOST"
|
title := "DELETE SSH HOST"
|
||||||
question := fmt.Sprintf("Are you sure you want to delete host '%s'?", m.deleteHost)
|
hostName := ""
|
||||||
|
if m.deleteHost != nil {
|
||||||
|
hostName = m.deleteHost.Name
|
||||||
|
}
|
||||||
|
question := fmt.Sprintf("Are you sure you want to delete host '%s'?", hostName)
|
||||||
action := "This action cannot be undone."
|
action := "This action cannot be undone."
|
||||||
help := "Enter: confirm • Esc: cancel"
|
help := "Enter: confirm • Esc: cancel"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user