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
This commit is contained in:
132
README-OSC7-SSH.md
Normal file
132
README-OSC7-SSH.md
Normal file
@@ -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
|
144
st.c
144
st.c
@@ -238,6 +238,10 @@ static int iofd = 1;
|
|||||||
static int cmdfd;
|
static int cmdfd;
|
||||||
static pid_t pid;
|
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 utfbyte[UTF_SIZ + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0};
|
||||||
static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
|
static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
|
||||||
static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000};
|
static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000};
|
||||||
@@ -1086,10 +1090,112 @@ tswapscreen(void)
|
|||||||
tfulldirt();
|
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
|
void
|
||||||
newterm(const Arg* a)
|
newterm(const Arg* a)
|
||||||
{
|
{
|
||||||
int res;
|
int res;
|
||||||
|
char **args;
|
||||||
|
|
||||||
switch (fork()) {
|
switch (fork()) {
|
||||||
case -1:
|
case -1:
|
||||||
die("fork failed: %s\n", strerror(errno));
|
die("fork failed: %s\n", strerror(errno));
|
||||||
@@ -1100,7 +1206,38 @@ newterm(const Arg* a)
|
|||||||
die("fork failed: %s\n", strerror(errno));
|
die("fork failed: %s\n", strerror(errno));
|
||||||
break;
|
break;
|
||||||
case 0:
|
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);
|
execlp("/proc/self/exe", argv0, NULL);
|
||||||
exit(1);
|
exit(1);
|
||||||
break;
|
break;
|
||||||
@@ -2022,6 +2159,11 @@ strhandle(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case 7:
|
||||||
|
/* OSC7 - working directory notification */
|
||||||
|
if (narg > 1)
|
||||||
|
handle_osc7(strescseq.args[1]);
|
||||||
|
return;
|
||||||
case 10:
|
case 10:
|
||||||
case 11:
|
case 11:
|
||||||
case 12:
|
case 12:
|
||||||
|
Reference in New Issue
Block a user