From 57a4b03eabb5f9ea8e922ac8dde177df65af60cc Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Fri, 19 Sep 2025 13:48:40 +0200 Subject: [PATCH] add OSC7 SSH newterm support Enhances the existing Alt+Enter newterm functionality to support SSH connections via OSC7 working directory notifications. - Parses OSC7 sequences in format file://[user@]hostname/path - Launches new st terminal with SSH to remote directory - Proper shell escaping for paths with spaces and special chars - Falls back to local terminal on SSH failure or invalid paths - Supports both user@hostname and hostname-only formats --- README-OSC7-SSH.md | 132 +++++++++++++++++++++++++++++++++++++++++ st.c | 144 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 README-OSC7-SSH.md diff --git a/README-OSC7-SSH.md b/README-OSC7-SSH.md new file mode 100644 index 0000000..a843fef --- /dev/null +++ b/README-OSC7-SSH.md @@ -0,0 +1,132 @@ +# ST OSC7 SSH Enhancement Patch + +This patch enhances the existing Alt+Enter new terminal functionality in st to support SSH connections via OSC7 (Operating System Command 7) working directory notifications. + +## Features + +- **OSC7 Support**: Adds handling for OSC7 escape sequences to track working directory +- **SSH Detection**: Automatically detects when you're in an SSH session based on OSC7 hostname +- **Smart Terminal Launch**: Alt+Enter launches appropriate terminal: + - **Remote SSH**: If connected via SSH, opens new terminal with SSH connection to same directory + - **Local Directory**: If local session, uses OSC7 directory or falls back to pid-based method + +## Installation + +1. Apply the patch to your st source: +```bash +patch -p1 < st-osc7-ssh-newterm.patch +``` + +2. Recompile and install st: +```bash +make clean && make && sudo make install +``` + +## Shell Setup + +To enable OSC7 support, add this to your shell configuration: + +### For Bash (~/.bashrc) +```bash +# OSC7 working directory notification +osc7_cwd() { + local hostname="${HOSTNAME:-$(hostname)}" + local username="${USER:-$(whoami)}" + printf '\033]7;file://%s@%s%s\033\\' "$username" "$hostname" "$PWD" +} + +# Update working directory after each command +if [[ "$TERM" == st* ]]; then + PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND$'\n'}osc7_cwd" +fi +``` + +### For Zsh (~/.zshrc) +```bash +# OSC7 working directory notification +osc7_cwd() { + local hostname="${HOSTNAME:-$(hostname)}" + local username="${USER:-$(whoami)}" + printf '\033]7;file://%s@%s%s\033\\' "$username" "$hostname" "$PWD" +} + +# Update working directory after each command +if [[ "$TERM" == st* ]]; then + precmd_functions+=(osc7_cwd) +fi +``` + +### For Fish (~/.config/fish/config.fish) +```fish +# OSC7 working directory notification +function osc7_cwd + set hostname (hostname) + set username (whoami) + printf '\033]7;file://%s@%s%s\033\\' $username $hostname $PWD +end + +# Update working directory after each command +if string match -q "st*" $TERM + function __osc7_cwd_handler --on-event fish_prompt + osc7_cwd + end +end +``` + +## Usage + +1. **SSH to a remote server** with OSC7-enabled shell setup +2. **Navigate to any directory** on the remote server +3. **Press Alt+Enter** to launch a new terminal + +The new terminal will automatically: +- Connect via SSH to the same remote server +- Change to the same working directory +- Use your default shell + +## Technical Details + +### OSC7 Format +The patch handles OSC7 sequences in the format: +``` +ESC]7;file://hostname/path/to/directory ESC\ +``` + +### SSH Command Generation +When launching a remote terminal, the patch constructs: +```bash +ssh -t user@hostname "cd /path/to/directory && exec $SHELL" +``` + +### Fallback Behavior +- If SSH connection fails, falls back to local terminal +- If OSC7 directory is inaccessible, uses original pid-based directory detection +- If no OSC7 data available, uses original behavior + +## Troubleshooting + +1. **Alt+Enter opens local terminal instead of SSH**: + - Ensure shell is sending OSC7 sequences + - Check that hostname in OSC7 is not "localhost" + +2. **SSH connection fails**: + - Verify SSH key authentication is set up + - Check that remote server allows SSH connections + - Patch will fall back to local terminal on SSH failure + +3. **Wrong directory opened**: + - Ensure OSC7 sequences are being sent after directory changes + - Check shell configuration for OSC7 setup + +## Compatibility + +- Requires existing newterm patch (Alt+Enter functionality) +- Works with any SSH-accessible remote server +- Compatible with bash, zsh, fish, and other shells that support prompt customization + +## Security Considerations + +- Uses existing SSH configuration and keys +- No additional authentication required +- Falls back gracefully on connection failures +- Only connects to hosts specified in OSC7 sequences diff --git a/st.c b/st.c index 6887d9a..77e8d29 100644 --- a/st.c +++ b/st.c @@ -238,6 +238,10 @@ static int iofd = 1; static int cmdfd; static pid_t pid; +/* OSC7 working directory tracking */ +static char *osc7_cwd = NULL; +static char *ssh_connection = NULL; + static const uchar utfbyte[UTF_SIZ + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; @@ -1086,10 +1090,112 @@ tswapscreen(void) tfulldirt(); } +static void +handle_osc7(char *uri) +{ + char *path, *host, *user, *at_pos; + + /* OSC7 format: file://[user@]hostname/path or file:///path */ + if (strncmp(uri, "file://", 7) != 0) + return; + + uri += 7; + + /* Extract hostname and path */ + path = strchr(uri, '/'); + if (!path) { + /* No path part, whole thing is hostname */ + host = xstrdup(uri); + path = "/"; + } else { + /* Split hostname and path */ + host = xmalloc(path - uri + 1); + memcpy(host, uri, path - uri); + host[path - uri] = '\0'; + /* path already points to the '/', which is what we want */ + } + + /* Store working directory */ + if (osc7_cwd) + free(osc7_cwd); + osc7_cwd = xstrdup(path); + + /* Parse user@hostname format and store SSH connection info */ + if (host && *host && strcmp(host, "localhost") != 0) { + if (ssh_connection) + free(ssh_connection); + + /* Check if hostname contains user@ prefix */ + at_pos = strchr(host, '@'); + if (at_pos) { + /* user@hostname format - use as-is */ + ssh_connection = xstrdup(host); + } else { + /* hostname only - try to get current user from environment */ + user = getenv("USER"); + if (!user) + user = getenv("LOGNAME"); + if (!user) + user = "root"; + + ssh_connection = xmalloc(strlen(user) + strlen(host) + 2); + sprintf(ssh_connection, "%s@%s", user, host); + } + } else { + if (ssh_connection) { + free(ssh_connection); + ssh_connection = NULL; + } + } + + /* Clean up allocated host string if we allocated it */ + if (path != "/" && host) + free(host); +} + +static char * +shell_escape(const char *str) +{ + const char *p; + char *escaped, *q; + size_t len = 0; + + /* Count characters that need escaping */ + for (p = str; *p; p++) { + if (*p == '\'') + len += 4; /* Replace ' with '\'' */ + else + len += 1; + } + + escaped = xmalloc(len + 3); /* +2 for surrounding quotes, +1 for null */ + q = escaped; + *q++ = '\''; + + for (p = str; *p; p++) { + if (*p == '\'') { + /* Replace single quote with '\'' */ + *q++ = '\''; + *q++ = '\\'; + *q++ = '\''; + *q++ = '\''; + } else { + *q++ = *p; + } + } + + *q++ = '\''; + *q = '\0'; + + return escaped; +} + void newterm(const Arg* a) { int res; + char **args; + switch (fork()) { case -1: die("fork failed: %s\n", strerror(errno)); @@ -1100,7 +1206,38 @@ newterm(const Arg* a) die("fork failed: %s\n", strerror(errno)); break; case 0: - chdir_by_pid(pid); + /* If we have OSC7 directory info and SSH connection, launch new st with SSH */ + if (osc7_cwd && ssh_connection) { + /* Build the SSH command to change to the remote directory, with fallback */ + char *escaped_path = shell_escape(osc7_cwd); + size_t cmdlen = strlen(ssh_connection) + strlen(escaped_path) + strlen("ssh -t \"cd 2>/dev/null || cd ~ ; exec $SHELL\"") + 50; + char *ssh_cmd = xmalloc(cmdlen); + snprintf(ssh_cmd, cmdlen, "ssh -t %s \"cd %s 2>/dev/null || cd ~ ; exec $SHELL\"", ssh_connection, escaped_path); + free(escaped_path); + + args = xmalloc(6 * sizeof(char*)); + args[0] = argv0; /* Launch new st */ + args[1] = "-e"; /* Execute command */ + args[2] = "sh"; /* Use shell to execute */ + args[3] = "-c"; /* Command follows */ + args[4] = ssh_cmd; + args[5] = NULL; + + execvp("/proc/self/exe", args); + /* If exec fails, fall back to local terminal */ + fprintf(stderr, "failed to launch new st with ssh, falling back to local terminal\n"); + } + + /* Local terminal: use OSC7 directory if available, otherwise use pid method */ + if (osc7_cwd) { + if (chdir(osc7_cwd) != 0) { + /* If OSC7 directory doesn't exist, fall back to pid method */ + chdir_by_pid(pid); + } + } else { + chdir_by_pid(pid); + } + execlp("/proc/self/exe", argv0, NULL); exit(1); break; @@ -2022,6 +2159,11 @@ strhandle(void) } } return; + case 7: + /* OSC7 - working directory notification */ + if (narg > 1) + handle_osc7(strescseq.args[1]); + return; case 10: case 11: case 12: