SSH connection status indicators
- Add Connected bool field to vfs.Entry and RemoteMount - Track connection status in sshState.connectedHosts - Show status icon (connected/disconnected) in pane header when browsing remote host - Async SSH connection test with cancel support for Add Host dialog - Colored labels and styled help text in SSH dialogs - Confirmation dialog when deleting manually-added SSH hosts
This commit is contained in:
parent
df4df6b8f6
commit
1ed2d3defb
224 changed files with 33447 additions and 236 deletions
209
internal/fs/remote/config.go
Normal file
209
internal/fs/remote/config.go
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
package remote
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseSSHConfig parses ~/.ssh/config and returns a list of SSH hosts.
|
||||
// It handles the most common SSH config directives: Host, HostName, Port, User, IdentityFile.
|
||||
func ParseSSHConfig() []SSHHost {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
configPath := filepath.Join(home, ".ssh", "config")
|
||||
return parseSSHConfigFile(configPath)
|
||||
}
|
||||
|
||||
func parseSSHConfigFile(path string) []SSHHost {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var hosts []SSHHost
|
||||
var current *SSHHost
|
||||
var currentNames []string
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// Skip empty lines and comments
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove inline comments (everything after # that's not in quotes)
|
||||
if idx := strings.Index(line, "#"); idx >= 0 {
|
||||
line = strings.TrimSpace(line[:idx])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
keyword := strings.ToLower(parts[0])
|
||||
value := strings.Join(parts[1:], " ")
|
||||
|
||||
switch keyword {
|
||||
case "host":
|
||||
// Save previous host block
|
||||
if current != nil && len(currentNames) > 0 {
|
||||
for _, name := range currentNames {
|
||||
if !isWildcardPattern(name) {
|
||||
host := *current
|
||||
host.Name = name
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start new host block
|
||||
current = &SSHHost{
|
||||
Port: "22",
|
||||
FromSSHConfig: true,
|
||||
}
|
||||
currentNames = strings.Fields(value)
|
||||
|
||||
case "hostname":
|
||||
if current != nil {
|
||||
current.HostName = value
|
||||
}
|
||||
|
||||
case "port":
|
||||
if current != nil {
|
||||
current.Port = value
|
||||
}
|
||||
|
||||
case "user":
|
||||
if current != nil {
|
||||
current.User = value
|
||||
}
|
||||
|
||||
case "identityfile":
|
||||
if current != nil {
|
||||
// Handle ~ expansion and relative paths
|
||||
resolved := resolveIdentityPath(value)
|
||||
if resolved != "" {
|
||||
current.IdentityFile = resolved
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save last host block
|
||||
if current != nil && len(currentNames) > 0 {
|
||||
for _, name := range currentNames {
|
||||
if !isWildcardPattern(name) {
|
||||
host := *current
|
||||
host.Name = name
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
// isWildcardPattern returns true if the pattern contains wildcard characters.
|
||||
func isWildcardPattern(pattern string) bool {
|
||||
return strings.ContainsAny(pattern, "*?")
|
||||
}
|
||||
|
||||
// resolveIdentityPath resolves a path from SSH config (handles ~ and relative paths).
|
||||
func resolveIdentityPath(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Handle ~/ or $HOME/
|
||||
if strings.HasPrefix(path, "~/") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
path = filepath.Join(home, path[2:])
|
||||
}
|
||||
|
||||
// Handle relative paths (relative to ~/.ssh/)
|
||||
if !filepath.IsAbs(path) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
path = filepath.Join(home, ".ssh", path)
|
||||
}
|
||||
|
||||
return filepath.Clean(path)
|
||||
}
|
||||
|
||||
// SSHConfigPath returns the path to the user's SSH config file.
|
||||
func SSHConfigPath() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".ssh", "config")
|
||||
}
|
||||
|
||||
// HostsFilePath returns the path to the custom hosts data file.
|
||||
func HostsFilePath() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".config", "vcom", "hosts.dat")
|
||||
}
|
||||
|
||||
// GetSSHDir returns the path to the .ssh directory.
|
||||
func GetSSHDir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".ssh")
|
||||
}
|
||||
|
||||
// KnownHostsPath returns the path to known_hosts.
|
||||
func KnownHostsPath() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(home, ".ssh", "known_hosts")
|
||||
}
|
||||
|
||||
// ConfigFileExists checks if the SSH config file exists.
|
||||
func ConfigFileExists() bool {
|
||||
path := SSHConfigPath()
|
||||
if path == "" {
|
||||
return false
|
||||
}
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ValidateHost checks if a host entry has the minimum required fields.
|
||||
func ValidateHost(host SSHHost) error {
|
||||
if strings.TrimSpace(host.Name) == "" {
|
||||
return fmt.Errorf("host name is required")
|
||||
}
|
||||
if strings.TrimSpace(host.HostName) == "" {
|
||||
return fmt.Errorf("hostname/address is required")
|
||||
}
|
||||
if strings.TrimSpace(host.User) == "" {
|
||||
return fmt.Errorf("username is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue