When some tools spawn subshells, PROMPT_COMMAND may be inherited as an
environment variable while the __ghostty_hook function is not (bash
doesn't export functions by default). This causes "command not found"
errors on every prompt in the subshell.
Add 2>/dev/null to the __ghostty_hook entry in PROMPT_COMMAND so that it
silently no-ops in subshells where the function isn't defined. This also
silences any errors from inside __ghostty_hook itself, but those are all
terminal escape sequences and non-actionable.
See: #11245
We previously used a readonly variable (__ghostty_ps0) to define the
best __ghostty_preexec_hook expansion for the current bash version.
This works pretty well, but it had the downside of managing another
variable (#11258).
We can instead simplify this a bit by moving this into __ghostty_hook. I
didn't take that approach originally because I wanted to avoid the bash
version check on each command, but slightly loosening our guard check to
just look for "__ghostty_preexec_hook" (rather than the full expansion
expression) means we can bury the bash version check to the cold path.
One small gap here is that we may not update PS0 to the correct syntax
if we start switching between significantly different bash versions in
interactive subshells, but that seems like a pretty rare case to handle
given the benefits of this approach.
ble.sh performs its own cursor positioning so we get multiple newlines
with 133;A's fresh-line behavior. ble.sh is a large enough project to
justify this additional, unambiguous conditional.
See: akinomyoga/ble.sh#684
See: wezterm/wezterm#5072
We need to handle on more case: when an existing PROMPT_COMMAND ends in
a newline, we don't want to append a ; because that already counts as a
command separator.
We now handle all of these PROMPT_COMMAND cases:
- Ends with ; — no ; added
- Ends with \n or other whitespace — no ; added
- Ends with a command name — ; added as separator
See: #11245
Use OSC 133;P (prompt mark) instead of 133;A (fresh line + prompt mark)
inside PS1 and PS2. Readline redraws the prompt on vi mode switches,
Ctrl-L, and other events, and 133;A's fresh-line behavior would emit a
CR+LF whenever the cursor wasn't at column 0, causing visible extra
newlines.
The one-time 133;A is now emitted via printf in __ghostty_precmd, which
only runs once per prompt cycle via PROMPT_COMMAND. On SIGWINCH, bash
redraws PS1 (firing the 133;P marks) but doesn't re-run PROMPT_COMMAND,
so there's no unwanted fresh-line on resize either. The redraw=last flag
persists from the initial printf.
This is a little less optimal than our previous approach, in terms of
number of prompt marks we emit, but it produces an overall more correct
result, which is the important thing.
Because readline prints its output outside the scope of PS1, those
characters "inherit" the surrounded prompt scope. This is usually fine,
but it can sometimes get out of sync (especially during redraws). This
is inherently a limitation of the fact that it's a separate output
channel, so we just have to accept that can happen.
See: #11267
Only replace the \n prompt escape when inserting secondary prompt marks,
not literal newlines ($'\n'). Literal newlines may appear inside $(...)
or `...` command substitutions, and inserting escape sequences there
breaks the shell syntax. For example:
PS1='$(if [ $? -eq 0 ]; then echo -e "P";
else echo -e "F";
fi) $ '
The literal newlines between the if/else/fi are part of the shell syntax
inside the command substitution. The previous code replaced all literal
newlines in PS1 with newline + OSC 133 escape sequences, which injected
terminal escapes into the middle of the command substitution and caused
bash to report a syntax error when evaluating it.
The \n prompt escape is PS1-specific and safe to replace globally. This
means prompts using literal newlines for line breaks (rather than \n)
won't get per-line secondary marks, but this is the conventional form
and avoids the need for complex shell parsing.
Fixes: #11267
This fixes an error if the script was sourced a second time:
bash: __ghostty_ps0: readonly variable
Because this is a non-exported variable, this would only happen if the
script was sourced multiple times in the same bash session.
If an existing PROMPT_COMMAND was a string ending in ; (and maybe some
spaces), we'd add a redundant ;, resulting in a syntax error. Now we
strip any trailing `;[[:space:]]*` characters from the original string
before add ours.
This fixes an error if the script was sourced a second time:
bash: __ghostty_ps0: readonly variable
Because this is a non-exported variable, this would only happen if the
script was sourced multiple times in the same bash session.
The `cursor` shell feature always used a blinking bar (beam), often to
the surprise of users who set `cursor-style-blink = false`.
This change extends our GHOSTTY_SHELL_FEATURES format to include either
`cursor:blink` (default) or `cursor:steady` based on cursor-style-blink
when the `cursor` feature is enabled, and all shell integrations have
been updated to use that additional information to choose the DECSCUSR
cursor value (5=blinking bar, 6=steady bar).
I also considered passing a DECSCUSR value in GHOSTTY_SHELL_FEATURES
(e.g. `cursor:5`). This mostly worked well, but zsh also needs the blink
state on its own for its block cursor. We also don't support any other
shell feature cursor configurability (e.g. the shape), so this was an
over generalization.
This does change the behavior for users who like the blinking bar in the
shell but have `cursor-blink-style = false` for other reasons. We could
provide additional `cursor` shell feature configurability (e.g.
`cursor:blink` in `shell-integration-features`), but I'll propose that
as its own change.
See: #2812Closes: #8681
---
**AI Disclosure:** I did a lot of rubber ducking with Claude Code while
trying out various ideas. It was particularly useful for this kind of
feature because I could try out one thing and have it evaluate the
impact on all of the shell integration scripts at once.
The printf was part of the original script (9d6121245), and at the time,
this was the only place we'd emit the 133;A mark.
A PS1-based 133;P;k=i mark was introduced in 2bf1f80f7, and then it
become a full 133;A mark in aa47047a6, making the original printf line
redundant (because bash will also redraw PS1 on SIGWINCH).
The PS1-based 133;A was only missing the aid= option, and with that
added, it handles all of our cases (prompts, initial draw, and resizes).
We were previously overwriting PS0 on every PROMPT_COMMAND. We now
append to PS0, but only if it doesn't already contain our hook.
This is also more consistent with the bash-preexec behavior we maintain
for older bash versions.
The `cursor` shell feature always used a blinking bar (beam), often to
the surprise of users who set `cursor-style-blink = false`.
This change extends our GHOSTTY_SHELL_FEATURES format to include either
`cursor:blink` (default) or `cursor:steady` based on cursor-style-blink
when the `cursor` feature is enabled, and all shell integrations have
been updated to use that additional information to choose the DECSCUSR
cursor value (5=blinking bar, 6=steady bar).
I also considered passing a DECSCUSR value in GHOSTTY_SHELL_FEATURES
(e.g. `cursor:5`). This mostly worked well, but zsh also needs the blink
state on its own for its block cursor. We also don't support any other
shell feature cursor configurability (e.g. the shape), so this was an
over generalization.
This does change the behavior for users who like the blinking bar in the
shell but have `cursor-blink-style = false` for other reasons. We could
provide additional `cursor` shell feature configurability, but I think
that's best left to a separate change.
PROMPT_COMMAND array support for introduced in bash 5.1, and it's the
preferred format moving forward. Using the string form is also fine, but
it's easy to be a modern bash citizen here, so let's do so.
bash-preexec implements support for its "precmd" and "preexec" hooks
using a combination of PROMPT_COMMAND and a DEBUG trap. The latter is
unfortunately quick slow (in a relative sense), and the overall system
is a bit more generalized than what we need for our shell integration
(e.g. supporting multiple function hooks, subshells, etc.).
Bash 4.4 introduced the PS0 variable, which is expanded and displayed by
interactive shells after reading a complete command but before executing
it. This is all we need to implement our own shell integration hooks.
In Bash 5.1, PROMPT_COMMAND can be an array variable, each element of
which can contain a command to be executed like a string PROMPT_COMMAND
variable. When adding our hook to PROMPT_COMMAND, we preserve its type
(string or array) to be minimally intrusive. This also matches direnv's
approach.
Bash 5.3 introduced support for function substitution, which is an even
more efficient way to run code from PS0, so we use that when available.
Otherwise, we use the more traditional command substitution approach.
Earlier versions of bash (such as 3.2, which still ships with macOS)
continue to use the bash-preexec path. This gives us two code paths to
maintain, but I think that's preferable to fully maintaining our own
DEBUG trap-based system for older bash versions given that bash-preexec
has proven to work reliably in those environments. We also wouldn't
unlock any other user benefits aside from removing the bash-preexec
script dependency.
See: #3724, #7734
Insert OSC 133 A k=s marks after each newline in PS1, so that all lines
following the first are marked as secondary prompts. This prevents ghostty
from erasing leading lines during terminal resize.
Due to security issues, `sudo` implementations may not preserve
environment variables unless appended with `--preserve-env=list`.
Signed-off-by: definfo <hjsdbb1@gmail.com>
Our bash shell integration code uses ENV (in POSIX mode) to bootstrap
our shell integration script. This had the side effect of overwriting an
existing ENV value.
This change preserves ENV by storing it temporarily in GHOSTTY_BASH_ENV.
Note that this doesn't enable --posix mode support for automatic shell
integration. (--posix does work; we just skip shell integration when
that flag is specified.) We can reconsider implementing full --posix
support separately.
This value is always set to a non-empty string, and we only need this
value after we've determined that 'ssh_hostname' is non-empty.
In bash and zsh, we also don't need to check for the 'ghostty' command
before we attempt to add the target to the cache. That command will
safely fail silently if it's not available.
- Default ssh_term to xterm-256color to eliminate fallback assignments
- Remove base64 and replace infocmp -Q2 with standard -0 -x options for
compatibility
- Use process substitution instead of intermediate ssh_config variable
- Always set TERM explicitly since ssh_term is always defined
- Simplify feature detection to use single wildcard check
- Replace ssh_env array with simple ssh_term string variable
- Use TERM environment prefix instead of save/restore pattern
- Remove unnecessary backgrounded subshell for cache operations
- Remove complex ssh_exported_vars tracking and local environment
modification in favor of trusting Ghostty's local environment
- Replace regex patterns with glob-based feature detection for better
performance
- Fix local variable declaration consistency throughout
- Streamline logic while maintaining all functionality
- Replace dual-loop SSH config parsing with efficient single-pass case
statement
- Remove overly cautious timeout logic from cache checks for simplicity
- Add base64 availability check with xterm-256color fallback when
missing
- Include hostname in terminfo setup messages for better UX
- Maintain SendEnv/SetEnv dual approach for maximum OpenSSH
compatibility (relying on SetEnv alone seems to drop some vars during my
tests, despite them being explicitly included in AcceptEnv on the remote
host)
Our use of PS0 (which bash runs before command execution) was causing
raw command sequences to be printed between multiple commands in a
sequence.
$ alias garbage='echo start
> echo end'
$ garbage
start
�\���dend
I wasn't able to definitely track down all of the reasons for why this
only happens in the command sequence case, but I suspect it's related to
the way that __ghostty_preexec runs from within the bash DEBUG trap (by
way of bash-preexec).
This problem occurs when PS0 is set to _any_ string (even "") inside of
__ghostty_preexec, which also rules out most/any Ghostty-specific code.
PS1 and PS2 appear to be safe to (re)set in this context.
Fortunately, we can avoid using PS0 entirely by instead printing the
cursor reset escape sequence directly from __ghostty_precmd because it
also runs just before command execution.
- Eliminates standalone bash dependency
- Consolidates `+list-ssh-cache` and `+clear-ssh-cache` actions into
single `+ssh-cache` action with args
- Structured cache format with timestamps and expiration support
- Memory-safe entry handling with proper file locking
- Comprehensive hostname validation (IPv4/IPv6/domains)
- Atomic updates via temp file + rename
- Updated shell integrations for improved cross-platform support and
reliability
- Cache operations are now unit-testable
GHOSTTY_VERSION was mistakenly referenced but is never set. Use
TERM_PROGRAM_VERSION which is actually provided by Exec.zig from
build_config.version_string.
- Add +list-ssh-cache and +clear-ssh-cache CLI actions
- Remove ghostty() wrapper functions from all shell integrations
- Improve variable naming in shell scripts for readability
Addresses @00-kat's feedback about CLI discoverability and naming
consistency. The new CLI actions follow established Ghostty patterns
and are discoverable via `ghostty --help`, while maintaining clean
separation of concerns between shell logic and cache management.
Addresses feedback about separation of concerns in shell integration
scripts.
Extracts host caching logic to
`src/shell-integration/shared/ghostty-ssh-cache` and updates all four
shell integrations to use the shared script. The `shared/` subdirectory
preserves the existing organizational pattern where all shell-specific
code lives in subdirectories. This cleanly separates SSH transport logic
from cache management while reducing code duplication by ~25%.
All existing SSH integration behavior remains identical.
Rewrote shell functions to support the two new flags for
shell-integration-features:
- ssh-env: TERM compatibility + best effort environment variable
propagation (anything beyond TERM will depend on what the remote host
allows)
- ssh-terminfo: automatic terminfo installation with control socket
orchestration
- Flags work independently or combined
Implementation optimizations:
- ~65% code reduction through unified execution path
- Eliminated GHOSTTY_SSH_INTEGRATION environment variable system
- Replaced complex function dispatch with direct flag detection
- Consolidated 4 cache helper functions into single _ghst_cache()
utility
- Simplified control socket management (removed multi-step
orchestration)
- Subsequent connections to cached hosts are now directly executed and
more reliable
New additions:
- If ssh-terminfo is enabled, ghostty will be wrapped to provide users
with convenient commands to invoke either of the two utility functions:
`ghostty ssh-cache-list` and `ghostty ssh-cache-clear`
Need a sanity check on this new approach for "full" to help determine if
it's worth additional iteration/refinement.
It solves the double auth issue, successfully propagates env vars, and
avoids output noise for connections that happen after terminfo is
installed. The only issue I don't have time to fix tonight is the fact
that it drops the MOTD for cached (re)connections.
- Cache known hosts with terminfo in
$GHOSTTY_RESOURCES_DIR/terminfo_hosts
- Skip installation step for cached hosts (single connection instead of
two)
- Use secure file permissions (600) and atomic writes
- Extract SSH target safely from command arguments
- Maintains full functionality while improving user experience on
repeated connections
- Fix elvish function name mismatch and use conj for list operations
- Simplify terminfo installation command per ghostty docs (tic -x -)
- Fix conditional structure to ensure error messages always print
- Remove redundant checks and optimize array initialization
- Use consistent patterns across bash, fish, elvish, and zsh
implementations
Keeps only functional additions for SSH integration wrapper,
preserving original line breaks and indentation to minimize
diff noise per maintainer feedback.