Dragging a selection above the top (or below the bottom) of the pane
used to pin at the visible edge: pos_to_cell clamps the row and
on_select_update never moves the display offset, so scrollback content
just out of view was unreachable by mouse selection.
Now a left-drag that crosses the pane's vertical edge arms a repeating
50ms task that scrolls the display and re-anchors the selection's
moving end to the edge row, so the scroll continues while the pointer
holds still past the edge (mouse-move events alone stop the moment the
hand does). Speed scales with the overshoot distance — 1 line/tick when
grazing the edge up to a capped 8 lines/tick (~160 lines/s) — and the
task stops when the pointer re-enters, the drag ends, or a newer task
supersedes it (epoch guard, so a quick leave/re-enter/leave can't
double the speed). On the alt screen scroll_display no-ops (no
scrollback), leaving TUIs untouched.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Windows: a shell that exits on its own (exit / Ctrl-D / crash) left the pane
wedged open because ConPTY's output pipe never EOFs on a natural child exit.
Add a Windows-only exit monitor that waits on the child handle directly and
reports through a shared, run-once DeathReporter latch — the same Exited /
on_dead path the reader's EOF drives on Unix. Unix behavior is unchanged.
Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF
* fix(terminal): give solo prompt glyphs a two-cell paint window
Nerd Font prompt icons and the multiline `➜`/`❯` rendered sliced off on
the right (#17). tty7 paints any single-column glyph clipped to exactly
one cell, but a symbol face routinely draws ink past that cell: a
non-Mono Nerd Font sets a one-cell *advance* on its icons yet fills up to
~1.9 cells (measured across Hasklug / Meslo / JetBrainsMono NF), and the
OS cascade serves a proportional `➜` the same way when nothing in the
font list covers it. The per-cell content mask then severed the overflow
— the incomplete icons and cut-off arrow in the report.
Advance is no signal here (it reads one cell for those overflowing
icons), so widen every solo glyph's clip to two cells. A glyph that
already fits is untouched — it has no ink to spill — while a symbol that
overflows renders whole, bleeding into a trailing blank the way iTerm2
and Terminal.app do with non-Mono faces; the two-cell bound keeps a
pathological face from smearing a lone glyph across the row. Batched
Run/Wide segments keep their exact column-span clip, and Mono Nerd Fonts
are pixel-identical. Pairs with the native powerline separators from #19.
Verified in-app on Apple Silicon with a non-Mono "Hasklug Nerd Font"
primary: icons and a powerline prompt that were severed now render whole,
with no change to ASCII, CJK, or the Mono-font path.
* test(terminal): cover both clip-width branches, not just the solo constant
Fold the solo/batched clip-width decision into one pure `seg_clip_width`
so the branch itself is testable (per PR review): the test now asserts a
batched Run/Wide segment clips to its exact column span while a solo
glyph gets the two-cell window, instead of only checking the standalone
constant. No behavior change.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
gpui-component's MenuItemElement highlights the hovered/selected row from
`tokens.accent` + `accent_foreground`. tty7's theme never overrode those, so
menu rows fell back to the stock saturated accent — a hard highlight out of
step with the app's soft mix-based palette.
Point the accent tokens (and the plain `accent`/`accent_foreground` fields the
input completion / code-action popovers read) at `list_active` (mix 0.17), the
same soft fill the command palette uses for its selected row, and keep the
hover text at `foreground` so it stays legible on the low-contrast fill. Right-
click menu, "+" dropdown and command palette now share one hover language.
Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Opening a new tab or split stalled for seconds while a zsh plugin manager
reinstalled itself from scratch. tty7 launches zsh through a per-pane throwaway
ZDOTDIR so it can layer its OSC 133 shell integration on top of the user's
config, but it left ZDOTDIR aimed at that empty temp dir for the whole shell
lifetime. Tools that resolve their own state via ${ZDOTDIR:-$HOME} — Zim
(.zimrc/.zim), oh-my-zsh, compinit's .zcompdump — therefore looked inside the
empty throwaway dir and rebuilt on every pane: Zim redownloads/recompiles every
module (the ~3s stall and "modules/…: Installed" spam of #15), compinit rewrites
its dump into the temp dir, etc.
Each redirector now stashes our dir, points ZDOTDIR at the user's real config
dir while their startup file runs, then restores ours so zsh still reaches the
next redirector; the integration body restores the real dir for the live
session via a one-shot precmd hook. When the user never set a ZDOTDIR we unset
it (rather than force $HOME) while sourcing, so their file sees exactly what a
real launch gives it and the `: ${ZDOTDIR:=~/.config/zsh}` relocate idiom still
fires. .zshenv recaptures a user-relocated ZDOTDIR so the classic tiny-~/.zshenv
layout keeps both its config and tty7's integration (previously it silently lost
the integration).
Verified end-to-end against real zsh for three layouts (config in $HOME,
unconditional relocate, and `-z`-guarded relocate): a second pane now hits the
plugin manager's cache instead of reinstalling, and runtime ZDOTDIR resolves to
the real dir. Adds three unit tests covering the redirector ordering, the
.zshenv recapture, and the runtime restore hook.
Fixes#15
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
A remote shell that emits its own OSC 133 prompt marks — fish 4.x over
ssh, most visibly, which ships them on by default — made the daemon
report at_prompt=true, so tty7 engaged its local line editor and Tab ran
completion against the local filesystem instead of reaching the remote
shell.
The shell tty7 spawned only emits its OSC 133 marks while it is itself
the PTY's foreground process group (idle at its prompt). Reject a prompt
mark whenever a foreground command (ssh, a TUI, a nested shell) owns the
PTY — comparing the PTY's foreground pgid against the shell pid — so
keystrokes pass raw to whatever is really reading them. The normal local
case (shell idle ⇒ it is the foreground group) is untouched.
Fixes#26.
Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF
Promote the [Unreleased] changelog to [0.6.0]: the "+" shell picker and the
Windows PowerShell 7 default, plus this cycle's fixes — native powerline
separators + bundled Hack fallback (#17), URL detection stopping at full-width
brackets (#20), PATH-probed shell detection for brew/nix installs (#18), and
stale-daemon restart-and-retry on upgrade (#22). Bump the crate version
0.5.0 → 0.6.0 so the built binary reports its real version to the update check.
The Unix shell picker only enumerated $SHELL and /etc/shells, but
Homebrew and nix don't register what they install there (Homebrew only
suggests adding fish to /etc/shells, and few users do). A brew-installed
fish therefore never appeared in the "+" dropdown, even though the app
can spawn and shell-integrate it fine once selected.
Probe a curated set of well-known shells (fish, nu, pwsh, elvish, xonsh)
on PATH as a catch-all, after the /etc/shells entries. Registered shells
keep their /etc/shells paths via the existing basename dedupe; only the
unregistered leftovers are picked up from PATH. Uses the login-shell-
enriched PATH so Dock launches see Homebrew's prefix too.
Closes#18
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
A URL followed by a full-width parenthetical with no ASCII space, e.g.
`…/pull/343(fix/… → dev)`, leaked the bracket text into the link: the
whitespace split ran past the URL and trailing-punctuation trimming only
touches the token's end. URL characters are all ASCII (RFC 3986), so after
locating the scheme, truncate at the first non-URL char — a CJK character,
full-width bracket, arrow or emoji — which marks where the link ends. Interior
ASCII parens (Wikipedia-style URLs) are preserved. Adds regression tests.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Prompt themes (powerlevel10k, oh-my-posh, oh-my-zsh) render their
segment separators and status glyphs through whatever face the font
fallback chain resolves, and two failure modes followed (#17):
- Powerline separators (U+E0B0-E0BE) resolved from a fallback face
render at *that face's* advance, not the cell width — narrow wedges
that no longer meet their segment backgrounds, or tofu with no Nerd
Font installed at all.
- A glyph nothing in the configured chain covers falls through to the
OS cascade, which can serve a proportional glyph wider than the
cell; paint_glyphs' per-cell clip then truncates it — the severed
U+279C arrow in the report (pixel forensics: the cut is a zero-
antialiasing vertical edge exactly one cell after the glyph origin).
Fix, the approach Warp (stretchable SVGs) and kitty (programmatic
glyphs) settled on:
- The eight solid powerline separators — sharp triangles E0B0/E0B2,
round caps E0B4/E0B6, slants E0B8/BA/BC/BE — are now drawn as gpui
fill paths sized to the exact cell: pixel-perfect for every
font/size/line-height combination, no font dependency at all. The
thin/outline variants stay on the font path (hairline strokes can't
be a fill, and the bundled Hack covers the common ones).
- fallback_chain() appends the bundled "Hack" to every font fallback
list, so prompt symbols (U+279C, U+276F, box drawing, the sharp
wedges) resolve against a monospace-fitting face before ever
reaching the OS cascade lottery.
Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
The background detect_shells() update set detected_shells without
cx.notify(), so an idle window kept rendering the pre-probe (empty)
list until something else forced a redraw. Flagged by CodeRabbit.
Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF
The "+" button now opens a shell picker listing every shell found on
this machine — the login shell plus /etc/shells on Unix; PowerShell 7,
Windows PowerShell, cmd, Git Bash and WSL distributions on Windows — so
opening a tab in a different shell no longer requires hand-editing
config.json. The default entry leads the menu (Cmd+T still opens a
default tab in one keystroke), and splits inherit the pane's shell.
The Windows default shell now prefers PowerShell 7 when installed,
probed the way Warp does it (Program Files x64/x86/ARM, the Microsoft
Store shim, scoop, dotnet tools, then PATH), falling back to the
Windows PowerShell that ships with the OS.
Spawn precedence in the daemon is now: explicit per-spawn pick >
config.json `shell` > platform default. Wire compat is preserved across
GUI/daemon version skew: a default spawn keeps the legacy SPAWN frame
byte-for-byte, so an old daemon still serves a new GUI; only an
explicit pick uses the new SPAWN_SHELL kind, locked by a round-trip +
legacy-frame test.
Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF
Add a Discord badge to the README badge row and a chat invite in the
Contributing section (both English and Simplified Chinese). Remove
SECURITY.md and its references; private vulnerability reporting stays
available through GitHub's repository setting.
Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF
macOS gives the Option key two jobs a terminal can't serve at once: the
OS composes a special character (Option+B types ∫), while Meta bindings
need an ESC-prefixed chord (Option+B → ESC b, readline's backward-word).
A new `macos_option_as_alt` config (Settings → Terminal → Keyboard, off
by default) picks which one wins, matching Ghostty's `macos-option-as-alt`
and iTerm2's "Option as Meta".
The keystroke is reshaped once at the top of `on_key_down` — on: the
composed `key_char` is replaced with the base key so the encoder emits
`ESC` + the plain character (uppercased under Shift, like xterm's
metaSendsEscape); off: the alt bit is dropped so the composed character
goes out bare. Every consumer downstream (the ⌘ dispatcher, the prompt
editor, the PTY encoder, the Kitty encoder) then sees one consistent
story.
This also fixes two existing defects, both settings:
* The raw path used to emit `ESC ∫` for Option+B — an ESC prefix bolted
onto the composed character, wrong under either reading of Option.
* The prompt editor swallowed Option+letter chords entirely: no composed
character, no motion. Off now inserts the composed character; on maps
M-b / M-f / M-d to the readline word motions (which also lights them
up on Linux, where Alt is natively Meta but the chords were no-ops).
Known limitation: with the setting on and a non-ASCII IME input source
active (e.g. Pinyin), gpui routes printable keys — Option chords
included — to the IME first, which commits the composed character before
the key event reaches the view; Meta chords need an ASCII-capable source
(e.g. ABC). No regression: that state behaved identically before.
Closes#13
Promote the [Unreleased] changelog to [0.5.0] (Windows installer, startup
update check, ⌘K clear, ⌘⏎ fullscreen, right-click menu shortcut hints) and
add this release's fixes (right-click Select All matching ⌘A, Ctrl+R ASCII
keystrokes). Bump the crate version 0.3.0 → 0.5.0 so the built binary reports
its real version to the update check, and backfill the missing [0.3.0]/[0.4.0]
changelog compare links.
Copy / Paste / Select All / Find are dispatched inline in
`handle_cmd_shortcut` with no registered `KeyBinding`, so gpui-component's
PopupMenu couldn't auto-derive their hints the way it does for the
keybinding-backed items (Clear, Split, Maximize, New Tab, Close Pane) — the
menu showed shortcuts on some rows and nothing on others. Render the hints
ourselves via `menu_row_with_hint`, styled to match the auto-rendered ones,
instead of registering real bindings (which would risk the Ctrl+C SIGINT
fall-through on Windows/Linux). ⌘A / ⌘F are macOS-only, so those hints are
gated behind `mac_only`.
Also fix the "Select All" menu item diverging from ⌘A: both now go through
`select_all_contextual` (select the edited command line at the prompt,
otherwise the whole terminal buffer) so click and shortcut stay in sync.
The reverse-search query only took text from the IME commit path
(input_text -> push_query), so a CJK input source could type into it but a
plain ASCII source — and Linux, where prefers_ime_for_printable_keys is
false — delivered printable keys via on_key_down, where handle_key ignored
them. Ctrl+R opened, but the search box swallowed every keystroke.
Forward printable key_char events from handle_reverse_search_key into
push_query, mirroring the editor's existing key_char path; control/Cmd/Alt
chords and non-printable keys still fall through to the control-key handling.
Fixes ASCII typing on macOS and all typing on Linux. Reported on V2EX.
Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF
Two CodeRabbit findings on core::update:
- parse_version now rejects malformed cores instead of truncating them:
a doubled prefix (`vv0.3.1`) and extra components (`0.3.1.1`) no longer
collapse to a valid triple, so an unrecognizable tag can't surface a
bogus "update available". Uses strip_prefix('v') (not trim_start_matches)
and rejects a 4th dot component. Added tests for both cases.
- The check now races an explicit 15s timeout (ReqwestClient only sets a
connect timeout), so a connected-but-stalled response fails soft
deterministically instead of sitting pending for the session.
bundle-windows.ps1 now compiles windows-installer.iss (ISCC is preinstalled
on windows-latest) from the same staged payload as the zip, producing
tty7-<version>-windows-x86_64-setup.exe: per-user install by default with an
all-users option, Start Menu shortcut, Apps uninstall entry, optional desktop
icon. Release workflow uploads the new artifact; READMEs and CHANGELOG updated.
Set `windows_subsystem = "windows"` on release builds so launching the
packaged .exe doesn't flash a console window, while keeping it in debug
builds so println!/eprintln! stay visible during development.
Build on the notify-only check: when a newer release is found, pop a
native modal dialog ("Update available — Download / Later") in addition
to the passive Settings -> About prompt. Download opens the Releases
page; tty7 still never self-updates.
- Pops exactly once per version: the shown version is recorded in a small
update.json (mirroring session.json's load/save) and re-checked on the
next launch, so we never nag twice for the same release.
- wait_for_window handles the startup race where the network check
finishes before the window opens — poll briefly, then host the modal.
- open_releases_page moves to core::update as a shared free fn; the
Settings -> About button now delegates to it (one opener, two callers).
- Fails soft as before: no window / disabled check / offline all skip the
modal silently; the About prompt still appears whenever we're outdated.
Ask the GitHub releases API once, in the background, on GUI startup
whether a newer version has shipped and — if so — surface a "Download"
prompt in Settings → About that links to the Releases page. tty7 never
downloads or self-updates; the user installs by hand, exactly as the
README's Install section describes.
- core::update: UpdateStatus global, version parse/compare, and the
GitHub fetch via reqwest_client. That reqwest+rustls stack is already
compiled into the tree through gpui-component-assets, so this pins no
new native code — it only exposes what we already build.
- config: check_for_updates toggle (default true) to skip the network
call entirely for offline / privacy.
- Settings -> About: an "Updates" section with the version prompt, a
Download button (default style, matching the sibling actions), and the
toggle.
- Fails soft: no network, a rate-limit, a private/renamed repo, or an
unparseable tag all collapse to "no prompt", logged at debug and never
surfaced. The terminal opens the same whether or not GitHub is reachable.
Typing `exit` (or Ctrl-D) left a dead "process exited" pane behind: the
view marked itself exited, but nothing ever closed the pane or tab.
The reader thread now records whether the pane ended with a *genuine*
child exit (`DaemonMsg::Exited`) as opposed to a daemon disconnect, and
the view surfaces that as a `ChildExited` gpui event. The app subscribes
at the single place all panes are built and closes the emitting leaf:
collapsing its split, or closing the tab when it was the only pane (the
last tab falls back to the home page). The daemon pane is killed so the
session drops it; panes that died while detached clean themselves up on
the next attach, when the daemon replays their exit.
A disconnect deliberately keeps the dead pane visible -- auto-closing
would silently discard (and kill) sessions that may still be alive
daemon-side.
An ssh session dropping mid-TUI (htop, vim, ...) leaves the pane wedged:
the remote app hid the cursor (DECTCEM), entered the alt screen and
enabled mouse reporting, and the restore sequences can never arrive. The
host shell's next prompt then renders with no cursor anywhere, mouse
clicks echo `0;19;42M`-style junk, and scrollback stays broken.
The client's reader thread now scrubs this residue when the daemon
reports a fresh prompt (OSC 133): at that position in the byte stream no
full-screen program owns the pane, so a stranded alt screen, hidden
cursor, mouse/focus reporting, or kitty keyboard flags are provably
stale. Each reset is emitted only when its mode is actually set
(`?1049l` performs a cursor restore when idle, so no blanket resets),
and bracketed paste / application cursor keys are left to the line
editor, which re-arms them on every read. Reattach self-heals the same
way -- the daemon replays the prompt state after the ring.
tty7 is split into two Rust processes: a persistent daemon that owns the
shells and a GPU-rendered client that talks to it over a local socket.
Because the shells live in the daemon, quitting and reopening the app
leaves the session intact — detach and reattach, no tmux required.
- Persistent sessions — the daemon holds the PTYs and child processes, so
closing a window or swapping in a new build never takes a shell down.
- Performance — an 11 MB `cat` completes in 95 ms and DOOM-fire renders at
888 fps; the daemon drains the PTY at device speed off the render path.
- Shell-aware — new tabs and splits open in the current working directory;
zsh, bash, fish, and PowerShell are set up automatically.
- Enhanced prompt — inline completion, syntax highlighting, history, and
in-terminal search, with rich flag/subcommand signatures for common tools.
- Tabs, resizable splits, a command palette, click-to-open links, desktop
notifications, eight themes, and CJK/IME input.
Native builds for macOS, Windows, and Linux.
Built on Zed's gpui and Alacritty's VT core.