wraith/internal/ssh/cwd.go
Vantz Stockwell 8a096d7f7b
Some checks failed
Build & Sign Wraith / Build Windows + Sign (push) Has been cancelled
Wraith v0.1.0 — Desktop SSH + RDP + SFTP Client
Go + Wails v3 + Vue 3 + SQLite + FreeRDP3 (purego)
183 tests, 76 source files, 9,910 lines of code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 08:19:29 -04:00

129 lines
3.3 KiB
Go

package ssh
import (
"bytes"
"fmt"
"net/url"
"sync"
)
// CWDTracker parses OSC 7 escape sequences from terminal output to track the
// remote working directory.
type CWDTracker struct {
currentPath string
mu sync.RWMutex
}
// NewCWDTracker creates a new CWDTracker.
func NewCWDTracker() *CWDTracker {
return &CWDTracker{}
}
// osc7Prefix is the escape sequence that starts an OSC 7 directive.
var osc7Prefix = []byte("\033]7;")
// stTerminator is the ST (String Terminator) escape: ESC + backslash.
var stTerminator = []byte("\033\\")
// belTerminator is the BEL character, an alternative OSC terminator.
var belTerminator = []byte{0x07}
// ProcessOutput scans data for OSC 7 sequences of the form:
//
// \033]7;file://hostname/path\033\\ (ST terminator)
// \033]7;file://hostname/path\007 (BEL terminator)
//
// It returns cleaned output with all OSC 7 sequences stripped and the new CWD
// path (or "" if no OSC 7 was found).
func (t *CWDTracker) ProcessOutput(data []byte) (cleaned []byte, newCWD string) {
var result []byte
remaining := data
var lastCWD string
for {
idx := bytes.Index(remaining, osc7Prefix)
if idx == -1 {
result = append(result, remaining...)
break
}
// Append everything before the OSC 7 sequence.
result = append(result, remaining[:idx]...)
// Find the end of the OSC 7 payload (after the prefix).
afterPrefix := remaining[idx+len(osc7Prefix):]
// Try ST terminator first (\033\\), then BEL (\007).
endIdx := -1
terminatorLen := 0
if stIdx := bytes.Index(afterPrefix, stTerminator); stIdx != -1 {
endIdx = stIdx
terminatorLen = len(stTerminator)
}
if belIdx := bytes.Index(afterPrefix, belTerminator); belIdx != -1 {
if endIdx == -1 || belIdx < endIdx {
endIdx = belIdx
terminatorLen = len(belTerminator)
}
}
if endIdx == -1 {
// No terminator found; treat the rest as literal output.
result = append(result, remaining[idx:]...)
break
}
// Extract the URI payload between prefix and terminator.
payload := string(afterPrefix[:endIdx])
if path := extractPathFromOSC7(payload); path != "" {
lastCWD = path
}
remaining = afterPrefix[endIdx+terminatorLen:]
}
if lastCWD != "" {
t.mu.Lock()
t.currentPath = lastCWD
t.mu.Unlock()
}
return result, lastCWD
}
// GetCWD returns the current tracked working directory.
func (t *CWDTracker) GetCWD() string {
t.mu.RLock()
defer t.mu.RUnlock()
return t.currentPath
}
// extractPathFromOSC7 parses a file:// URI and returns the path component.
func extractPathFromOSC7(uri string) string {
u, err := url.Parse(uri)
if err != nil {
return ""
}
if u.Scheme != "file" {
return ""
}
return u.Path
}
// ShellIntegrationCommand returns the shell command to inject for CWD tracking
// via OSC 7. The returned command sets up a prompt hook that emits the OSC 7
// escape sequence after every command.
func ShellIntegrationCommand(shellType string) string {
switch shellType {
case "bash":
return fmt.Sprintf(`PROMPT_COMMAND='printf "\033]7;file://%%s%%s\033\\" "$HOSTNAME" "$PWD"'`)
case "zsh":
return fmt.Sprintf(`precmd() { printf '\033]7;file://%%s%%s\033\\' "$HOST" "$PWD" }`)
case "fish":
return `function __wraith_osc7 --on-event fish_prompt; printf '\033]7;file://%s%s\033\\' (hostname) (pwd); end`
default:
return ""
}
}