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 "" } }