T34: prompts and commands carry OSC 133 marks: the tap reads every OSC body and hands 133's marks to the session, which keeps its last command's status; zsh, bash, fish and PowerShell mark what the shell leaves unmarked, and the user's prompt still sees the status
This commit is contained in:
18 files changed
+359
-88
No files matched your search
@@ -31,11 +31,11 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
## Layout
|
||||
|
||||
- `build.rs`: on Windows, the icon into the executable as resource 1, through `assets/icons/elyterminal.rc`. `.cargo/config.toml`: Windows builds carry the C runtime. `rust-toolchain.toml`: the toolchain rustup installs.
|
||||
- `src/main.rs`: start. `src/lib.rs`: the modules, public so tests and examples reach them. `src/app.rs`: fonts, settings, integration, then the main window, at the size the settings keep, which it keeps as it settles. `src/menus.rs`: the menu bar, the keys of Quit, the font's size and Minimize, and what they do. `src/blur.rs`: what the window shows of the desktop, from the opacity and blur settings, set as they change; `blur/macos` gives gpui's NSVisualEffectView a material that blurs; `blur/wayland` asks a Wayland compositor for blur behind the window's surface through `ext_background_effect_v1`; `blur/x11` names the window's pane in `_KDE_NET_WM_BLUR_BEHIND_REGION`. `src/workspace.rs`: the window's content, Ely's `ColumnShell` with the sessions, terminal and right columns, their toggles, the right column's Git and Files switch, the title, and the sessions' lifecycle. `src/workspace/widths.rs`: the side columns' widths, which a drag on their border sets and the settings keep once it rests. `src/sessions.rs`: a session and its row. `src/files.rs`: the Files panel, the shown session's folder as Ely's `Tree` with Git's marks. `src/git.rs`: git through its command line: Files' marks, and the Git panel's status, stage, unstage, discard, commit and log; `src/git/diff.rs` reads git's diff into Ely's stretches. `src/git/panel.rs`: the Git panel's state and actions; `src/git/panel/view.rs`: how it draws.
|
||||
- `src/main.rs`: start. `src/lib.rs`: the modules, public so tests and examples reach them. `src/app.rs`: fonts, settings, integration, then the main window, at the size the settings keep, which it keeps as it settles. `src/menus.rs`: the menu bar, the keys of Quit, the font's size and Minimize, and what they do. `src/blur.rs`: what the window shows of the desktop, from the opacity and blur settings, set as they change; `blur/macos` gives gpui's NSVisualEffectView a material that blurs; `blur/wayland` asks a Wayland compositor for blur behind the window's surface through `ext_background_effect_v1`; `blur/x11` names the window's pane in `_KDE_NET_WM_BLUR_BEHIND_REGION`. `src/workspace.rs`: the window's content, Ely's `ColumnShell` with the sessions, terminal and right columns, their toggles, the right column's Git and Files switch, the title, and the sessions' lifecycle. `src/workspace/widths.rs`: the side columns' widths, which a drag on their border sets and the settings keep once it rests. `src/workspace/events.rs`: what a session's terminal reports: title, folder, bell, a command's end, the program's exit. `src/sessions.rs`: a session and its row. `src/files.rs`: the Files panel, the shown session's folder as Ely's `Tree` with Git's marks. `src/git.rs`: git through its command line: Files' marks, and the Git panel's status, stage, unstage, discard, commit and log; `src/git/diff.rs` reads git's diff into Ely's stretches. `src/git/panel.rs`: the Git panel's state and actions; `src/git/panel/view.rs`: how it draws.
|
||||
- `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`; `window` is the settings window, `window/appearance` its Appearance page, `window/terminal` its Terminal page. `tests/fonts.rs` brings its own main: gpui builds its Mac platform only on the main thread.
|
||||
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance; `omarchy`, on Linux, wears Omarchy 4's current theme and follows its switches.
|
||||
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 7 and the mouse modes before the parser; `herdr` follows herdr's focused pane through herdr's socket while herdr's client runs in the session: `herdr/unix` finds the client leading the pty's foreground, `herdr/windows` under the session's shell, and asks its named pipe. `unix` starts the program itself, so a failed `chdir` fails the start; Windows uses alacritty's ConPTY. `shell` writes the integration scripts at start and builds each shell's launch. `frame` reads the grid into styled rows, `boxes` turns box drawing, block elements and powerline arrows into rects and polygons (`boxes/outline` strokes curves and diagonals into polygons), `ink` resolves colors, `paint` draws rows, decorations and the cursor, `view` is the entity: pty events, metrics, blink, resize. `zoom` holds the points Bigger and Smaller add to the Size setting, unsaved. `keys` turns key events into bytes, the kitty protocol and, in `keys/legacy`, xterm's encoding, with no gpui types; `input` is the view's key listeners, the bindings that keep Tab and F6 from Ely's focus moves, and its IME handler; `mac` reads the Option side and repeat flag of AppKit's current event, keeps held keys repeating, and hands Command key-ups to the focused view. `mouse` encodes mouse reports, with no gpui types; `links` finds the link at a cell; `pointer` is the view's mouse listeners: selection, reports, wheel, links, copy and paste. `find` is the find box: Ely's `FindWidget`, and Ely's `find_all` over the grid's shown text.
|
||||
- `assets/shell-integration`: ELYTerminal's own scripts (not Ghostty's, which are GPL): zsh through `ZDOTDIR`, bash through `--rcfile`, fish through `XDG_DATA_DIRS`, PowerShell through `-NoExit -Command`. Each prompt sends OSC 7 and, except in fish, whose `fish_title` stays, a title.
|
||||
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 7, OSC 133 and the mouse modes before the parser; `marks` reads OSC 133's prompt marks; `herdr` follows herdr's focused pane through herdr's socket while herdr's client runs in the session: `herdr/unix` finds the client leading the pty's foreground, `herdr/windows` under the session's shell, and asks its named pipe. `unix` starts the program itself, so a failed `chdir` fails the start; Windows uses alacritty's ConPTY. `shell` writes the integration scripts at start and builds each shell's launch. `frame` reads the grid into styled rows, `boxes` turns box drawing, block elements and powerline arrows into rects and polygons (`boxes/outline` strokes curves and diagonals into polygons), `ink` resolves colors, `paint` draws rows, decorations and the cursor, `view` is the entity: pty events, metrics, blink, resize. `zoom` holds the points Bigger and Smaller add to the Size setting, unsaved. `keys` turns key events into bytes, the kitty protocol and, in `keys/legacy`, xterm's encoding, with no gpui types; `input` is the view's key listeners, the bindings that keep Tab and F6 from Ely's focus moves, and its IME handler; `mac` reads the Option side and repeat flag of AppKit's current event, keeps held keys repeating, and hands Command key-ups to the focused view. `mouse` encodes mouse reports, with no gpui types; `links` finds the link at a cell; `pointer` is the view's mouse listeners: selection, reports, wheel, links, copy and paste. `find` is the find box: Ely's `FindWidget`, and Ely's `find_all` over the grid's shown text.
|
||||
- `assets/shell-integration`: ELYTerminal's own scripts (not Ghostty's, which are GPL): zsh through `ZDOTDIR`, bash through `--rcfile`, fish through `XDG_DATA_DIRS`, PowerShell through `-NoExit -Command`. Each prompt sends OSC 7 and, except in fish, whose `fish_title` stays, a title; each marks prompts and commands with OSC 133 where the shell does not.
|
||||
- `assets/fonts`: JetBrains Mono 2.304's bold and italic faces (OFL; Ely bundles regular), and Nerd Fonts 3.5.1's JetBrains Mono NF Mono Regular, which draws icons a font lacks.
|
||||
- `assets/themes`: Ghostty theme files copied verbatim from iTerm2-Color-Schemes (MIT, its `LICENSE` beside them).
|
||||
- `assets/icons`: the icon, an original figure, Ely, holding a terminal: `elyterminal-macos.svg` on macOS's rounded square, `elyterminal.svg` the figure alone, for Windows and Linux; the macOS file holds a copy of the figure's markup, so an edit to the figure is made in both. `elyterminal.ico` is the figure at 16 to 256 px for Windows, and `elyterminal.rc` names it resource 1.
|
||||
@@ -96,3 +96,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
- 2026-10-16: herdr 0.9 keeps its panes' OSC 7 and writes only a title, notifications, OSC 52 and bells to the terminal it runs in, so no terminal hears a folder from inside it; herdr's server knows each pane's `cwd`, the folder its shell reported, else the shell's own. On macOS and Linux each session asks twice a second which process group leads its pty's foreground (`tcgetpgrp` on a copy of the master) and what executable that group's leader runs: an exec keeps the group and swaps the executable, so `exec herdr` from a shell is a new program, and one that did not read is tried again; on Linux the ` (deleted)` mark an update leaves on a running executable is set aside. When that executable is `herdr` and its arguments start herdr's terminal client (no command, `session attach NAME` or `client`), ELYTerminal reads the client's arguments and variables (`/proc` on Linux; `KERN_PROCARGS2` on macOS, which hides only the variables of Apple's own programs) and finds its socket as herdr 0.9 does: `--session NAME`, then `HERDR_SOCKET_PATH`, then `HERDR_SESSION`, under `$XDG_CONFIG_HOME/herdr`, else `~/.config/herdr`; a relative socket lies in the client's folder, as herdr reads it (`/proc/PID/cwd`; `proc_pidinfo` on macOS). It asks that socket for `session.snapshot` twice a second, a connection each, since herdr closes one after its reply, and reports the focused pane's `cwd` as the session's folder when it changes and again each time herdr takes the foreground; an answer that lands after the shell reported a folder is dropped, since the shell reports only once herdr has left. Focus belongs to herdr's server, so clients of one server follow one focus; a client of another machine's server (`--remote`, `--machine`) is logged, not followed. A failure is logged once while it lasts. On Windows, which has no foreground group, a Toolhelp32 snapshot lists the `herdr.exe` processes at or under the session's shell, none under another herdr, since what a herdr starts, its server too, is its own; when that list changes, `sysinfo` reads their arguments and variables, nearest first, names in upper case, as Windows matches them, and the first terminal client is followed, a server or a command beside it passed by; its folder is found as herdr 0.9 finds it there, `XDG_CONFIG_HOME`, then `APPDATA`, then `USERPROFILE\AppData\Roaming`, then `HOME\.config`; and its server answers on the pipe herdr names `\\.\pipe\` and the socket's path as written, relative or not. A busy pipe is waited for, as herdr's client waits, and the pipe's server may identify ELYTerminal but never act as it. The pipe has no timeout: a herdr that never answers holds that session's follow until it answers or exits.
|
||||
- 2026-10-16: the main window opens at the size it was left at, which the owner asked for on macOS, Windows and Linux: `window_width` and `window_height` in the settings hold the size gpui's `window_bounds` gives 0.5 s after the bounds settle, saved through `SettingsStore::update`, which writes only a change, so a drag saves once and a move saves nothing. A maximized, full-screen or hidden window keeps the size from before; its state and size are read once they settle, since X11 marks a window maximized after its new size, gpui's X11 window then gives a maximized window's own size, and it calls a maximized window unmaximized while minimized. A window that shows again, uncovered, restored or back on its screen, is read again, so a size it took just before it hid is kept; a size it takes in the half second before it closes, or before it hides until the app quits, is lost. The window is no smaller than 100 by 100, the least the settings keep, or a smaller size the platform allows could not be kept. The window opens centered at whole points, since macOS grows a window by a point on a side whose edge falls on a half point; its place is not kept, and a kept side past the primary display's visible area shrinks to it, logged. A size outside 100 to 16384 stops the start, as any value out of range does.
|
||||
- 2026-10-16: the sessions and right columns resize by a drag on their border toward the terminal, which the owner asked for on macOS, Windows and Linux. Ely's `ShellColumn::resizable`, added to Ely for this, puts Ely's `resize_handle` on the border toward the filling column, the sessions column's right and the right column's left: the handle draws that border, a filled hairline, since gpui paints a border no wider than half its box, and the line it drew at first showed half a pixel on macOS and none on X11; it occludes the header under it, so a press there drags the border, not the window, on Windows too, where an unoccluded drag region takes the press as the caption. A drag sets off from the wider of the width drawn and the one kept: a header's parts may hold a column wider, and a window too narrow for both widths squeezes them, where a drag from the squeezed width jumped back against the pointer; a drag that has not moved sideways changes nothing, and one that comes back where it set off gives the width it had. A set width in Ely became a flex basis, so a column holds its header's own parts: at a set width under them, Windows' caption buttons ran out of the window. The widths, 160 to 960 pixels, show at once and are saved in `sessions_width` and `panel_width` through `SettingsStore::update` 0.5 s after a drag rests, as the window's size is; a drag in the half second before the window closes is lost, and a save the store refuses puts the columns back. A window too narrow for both yields the terminal its header's width, as Ely's set widths give way.
|
||||
- 2026-10-16: prompts and commands carry OSC 133 marks, which the owner asked for, read as Ghostty reads them: A or N starts a prompt, B or I ends it, C starts a command's output, and D ends the command with its status, D's first option, none when that is no number. The tap reads any program's marks before the parser, so it never sees the cursor: A's fresh line and L, which need it, are not done, and P adds nothing to A. Each session keeps the status its last command ended with. The integration marks what the shell leaves unmarked, and the user's prompt still sees the command's status. zsh: precmd reads `$?` and sends D after a command ran, preexec sends C, and a last precmd hook wraps PS1 in A and B under `prompt_percent`; it moves to the end of `precmd_functions` at each prompt, since a theme that sets PS1 in a hook of its own loads after it, so such a theme's first prompt goes unmarked. bash: PROMPT_COMMAND's first entry reads `$?`, which each entry resets, and its last wraps PS1; from bash 4.4 C rides PS0, whose subscript notes that a command ran, since bash prints no PS0 for an empty line; macOS's bash 3.2 has no PS0, so a DEBUG trap armed at the prompt sends C, unless the user set one. fish 4.3 marks its own prompts and commands, fish 4.0 all but B, older ones none: the script adds what the running fish lacks, around the prompt shown, and wraps it again when the user's changes. PowerShell: the prompt reads `$?` first and sets it back for the user's prompt through `Write-Error '' -ErrorAction Ignore`, which adds nothing to `$Error`; a command that left a new error ended with 1, any other failure with `$LASTEXITCODE`, read through `Get-Variable`, since strict mode refuses an unset variable and a prompt that fails falls back to `PS>`; C goes out when PSReadLine hands over a line that is not blank. herdr passes no OSC 133 on, so a session running herdr keeps the status from before it.
|
||||
@@ -42,7 +42,7 @@ Chapters run in build order. `TASKS.md` follows them line by line.
|
||||
|
||||
Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `alacritty_terminal`; Ely's `Terminal` is a gallery widget without mouse reporting, IME, kitty keys or style settings.
|
||||
|
||||
- PTY: alacritty's `tty` and event loop. The pty is wrapped so its reader passes bytes through an OSC 7 scanner. The scanner reports the working directory.
|
||||
- PTY: alacritty's `tty` and event loop. The pty is wrapped so its reader passes bytes through an OSC scanner. The scanner reports the working directory and prompt marks.
|
||||
- Shell integration for zsh, bash, fish and PowerShell, injected at launch as Ghostty does: each prompt sends OSC 7 and a title. A shell without integration reports nothing, and the panels say so.
|
||||
- Prompt marks, OSC 133: the shell integration marks where each prompt starts (A) and ends (B), where a command starts (C), and where it ends with its exit status (D); the terminal reads any program's OSC 133. Cmd-Up and Cmd-Down, and Cmd-Shift-Up and Cmd-Shift-Down, on macOS, Ctrl-Shift-Up and Ctrl-Shift-Down elsewhere, scroll to the prompt before or after the view's top, as in Ghostty.
|
||||
- Program status, OSC 7501 revision 0.4: each terminal keeps the records programs report (state, id, kind, progress, app, title, msg), checked as the spec checks them; a new prompt drops working, blocked and idle records, a full reset drops them all; the terminal answers the support query with the states and kinds it takes.
|
||||
@@ -150,4 +150,4 @@ Cmd-, opens it; on Windows and Linux, which have no menu bar, so does a gear in
|
||||
|
||||
## Deferred
|
||||
|
||||
Splits, tabs inside a session, profiles, key rebinding, kitty graphics, sixel, OSC 133 prompt marks, session restore, push and pull, quick terminal.
|
||||
Splits, tabs inside a session, profiles, key rebinding, kitty graphics, sixel, session restore, push and pull, quick terminal.
|
||||
@@ -42,7 +42,7 @@ The app writes its log to stderr. `RUST_LOG=elyterminal=debug` writes more; `tra
|
||||
## What it does
|
||||
|
||||
- The terminal runs on `alacritty_terminal`: 256 colors and truecolor, bold, dim, italic, five underline styles, wide characters and combining marks. Box drawing, block elements and powerline shapes are drawn as geometry, so lines join across cells. Keys follow xterm and the kitty keyboard protocol; input methods compose at the cursor; programs can track the mouse; pastes are bracketed when a program asks; OSC 8 and plain links open with the link press below; find searches the scrollback.
|
||||
- Shell integration for zsh, bash, fish and PowerShell: each prompt reports its folder and title.
|
||||
- Shell integration for zsh, bash, fish and PowerShell: each prompt reports its folder and title, and OSC 133 marks each prompt, each command and its exit status.
|
||||
- Sessions: new, close and switch. A bell marks a hidden session's row and makes no sound.
|
||||
- A button at each end of the title bar hides or shows its side column, and a drag on the border between a side column and the terminal resizes it; the window opens as it was left, at the size and widths it was left at. On Windows and Linux a gear beside the right one opens Settings.
|
||||
- Files shows the session's folder as a tree with Git's marks. Inside herdr the folder is that of herdr's focused pane.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ELYTerminal: load the usual startup files, report folder and title.
|
||||
# ELYTerminal: load the usual startup files, report folder and title, mark prompts and commands.
|
||||
if [ -n "${ELY_BASH_LOGIN-}" ]; then
|
||||
builtin unset ELY_BASH_LOGIN
|
||||
[ -r /etc/profile ] && builtin source /etc/profile
|
||||
@@ -12,8 +12,29 @@ if [ -n "${ELY_BASH_LOGIN-}" ]; then
|
||||
else
|
||||
[ -r ~/.bashrc ] && builtin source ~/.bashrc
|
||||
fi
|
||||
_ely_ran=
|
||||
_ely_armed=
|
||||
_ely_ps0=
|
||||
if (( BASH_VERSINFO[0] * 100 + BASH_VERSINFO[1] >= 404 )); then
|
||||
# PS0 runs in the shell, so its subscript can note that a command ran.
|
||||
_ely_ps0='${_ely_note[_ely_ran=1]-}\e]133;C\a'
|
||||
elif [ -z "$(builtin trap -p DEBUG)" ]; then
|
||||
_ely_debug() {
|
||||
[ -n "$_ely_armed" ] || builtin return 0
|
||||
case $BASH_COMMAND in _ely_*) builtin return 0 ;; esac
|
||||
_ely_armed=
|
||||
_ely_ran=1
|
||||
builtin printf '\e]133;C\a'
|
||||
}
|
||||
builtin trap '_ely_debug' DEBUG
|
||||
fi
|
||||
_ely_prompt() {
|
||||
builtin local ret=$? LC_ALL=C char url= i title
|
||||
_ely_armed=
|
||||
if [ -n "$_ely_ran" ]; then
|
||||
builtin printf '\e]133;D;%s\a' "$ret"
|
||||
_ely_ran=
|
||||
fi
|
||||
for (( i = 0; i < ${#PWD}; i++ )); do
|
||||
char=${PWD:i:1}
|
||||
case $char in
|
||||
@@ -25,4 +46,12 @@ _ely_prompt() {
|
||||
builtin printf '\e]7;file://%s\a\e]2;%s\a' "$url" "${title//[[:cntrl:]]/}"
|
||||
builtin return $ret
|
||||
}
|
||||
PROMPT_COMMAND="_ely_prompt${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
|
||||
# Last, after anything that sets PS1 or PS0.
|
||||
_ely_mark() {
|
||||
[[ $PS1 == *'\e]133;A'* ]] || PS1='\[\e]133;A\a\]'"$PS1"'\[\e]133;B\a\]'
|
||||
if [ -n "$_ely_ps0" ] && builtin shopt -q promptvars && [[ ${PS0-} != *"$_ely_ps0"* ]]; then
|
||||
PS0=$_ely_ps0${PS0-}
|
||||
fi
|
||||
_ely_armed=1
|
||||
}
|
||||
PROMPT_COMMAND="_ely_prompt${PROMPT_COMMAND:+;$PROMPT_COMMAND};_ely_mark"
|
||||
@@ -1,4 +1,4 @@
|
||||
# ELYTerminal: restore XDG_DATA_DIRS, report the folder; fish_title keeps the title.
|
||||
# ELYTerminal: restore XDG_DATA_DIRS, report the folder, mark what this fish leaves unmarked; fish_title keeps the title.
|
||||
if test -n "$ELY_FISH_XDG_DATA_DIRS"
|
||||
set -gx XDG_DATA_DIRS $ELY_FISH_XDG_DATA_DIRS
|
||||
else
|
||||
@@ -9,4 +9,33 @@ if status is-interactive
|
||||
function _ely_prompt --on-event fish_prompt
|
||||
printf '\e]7;file://%s\a' (string escape --style=url -- $PWD)
|
||||
end
|
||||
# fish 4.3 marks its own prompts and commands; 4.0 all but the prompt's end; older ones none.
|
||||
set -l v (string split ' ' -- (string replace -r '^(\d+)\.(\d+).*' '$1 $2' -- $version))
|
||||
if test $v[1] -lt 4
|
||||
set -g _ely_marks A B
|
||||
function _ely_preexec --on-event fish_preexec
|
||||
printf '\e]133;C\a'
|
||||
end
|
||||
function _ely_postexec --on-event fish_postexec
|
||||
printf '\e]133;D;%s\a' $status
|
||||
end
|
||||
else if test $v[1] -eq 4 -a $v[2] -lt 3
|
||||
set -g _ely_marks B
|
||||
end
|
||||
if set -q _ely_marks
|
||||
# The user's prompt loads late and may change; the marks go around the one shown.
|
||||
function _ely_wrap --on-event fish_prompt
|
||||
if functions -q _ely_user_prompt; and string match -q '*_ely_user_prompt*' -- (functions fish_prompt)
|
||||
return
|
||||
end
|
||||
functions -q _ely_user_prompt; and functions -e _ely_user_prompt
|
||||
functions -c fish_prompt _ely_user_prompt
|
||||
function fish_prompt
|
||||
# First, so the user's prompt sees the command's status.
|
||||
set -l text (_ely_user_prompt | string collect -N)
|
||||
contains A $_ely_marks; and printf '\e]133;A\a'
|
||||
printf '%s\e]133;B\a' "$text"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,23 @@
|
||||
# ELYTerminal: wrap the prompt to report folder and title.
|
||||
# ELYTerminal: wrap the prompt to report folder and title and to mark prompts and commands.
|
||||
$global:ElyPrompt = $function:prompt
|
||||
$global:ElyRan = $false
|
||||
function global:prompt {
|
||||
# First: every statement sets $? again.
|
||||
$ok = $?
|
||||
$esc, $bel = [char]27, [char]7
|
||||
if ($global:ElyRan) {
|
||||
$code = 1
|
||||
if ($ok) { $code = 0 }
|
||||
elseif ($Error.Count -eq 0 -or [object]::ReferenceEquals($Error[0], $global:ElyError)) {
|
||||
# No new error: a native program failed. Strict mode refuses an unset variable.
|
||||
$code = Get-Variable LASTEXITCODE -Scope Global -ValueOnly -ErrorAction Ignore
|
||||
}
|
||||
[Console]::Write("$esc]133;D;$code$bel")
|
||||
$global:ElyRan = $false
|
||||
}
|
||||
[Console]::Write("$esc]133;A$bel")
|
||||
# The user's prompt sees a failed command's $? still.
|
||||
if (-not $ok) { Write-Error '' -ErrorAction Ignore }
|
||||
$text = & $global:ElyPrompt
|
||||
$folder = $executionContext.SessionState.Path.CurrentLocation
|
||||
if ($folder.Provider.Name -eq 'FileSystem') {
|
||||
@@ -8,8 +25,20 @@ function global:prompt {
|
||||
$path = ($parts | ForEach-Object { [Uri]::EscapeDataString($_) }) -join '/'
|
||||
if (-not $path.StartsWith('/')) { $path = "/$path" }
|
||||
$leaf = (Split-Path -Leaf $folder.ProviderPath) -replace '\p{Cc}', ''
|
||||
$esc, $bel = [char]27, [char]7
|
||||
[Console]::Write("$esc]7;file://$path$bel$esc]2;$leaf$bel")
|
||||
}
|
||||
$text
|
||||
"$text$esc]133;B$bel"
|
||||
}
|
||||
# A command starts once PSReadLine hands over a line that is not blank.
|
||||
if (Test-Path Function:\PSConsoleHostReadLine) {
|
||||
$global:ElyReadLine = $function:PSConsoleHostReadLine
|
||||
function global:PSConsoleHostReadLine {
|
||||
$line = & $global:ElyReadLine
|
||||
if (-not [string]::IsNullOrWhiteSpace($line)) {
|
||||
$global:ElyRan = $true
|
||||
$global:ElyError = if ($Error.Count) { $Error[0] }
|
||||
[Console]::Write("$([char]27)]133;C$([char]7)")
|
||||
}
|
||||
$line
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# ELYTerminal: restore ZDOTDIR, load the user's .zshenv, report folder and title.
|
||||
# ELYTerminal: restore ZDOTDIR, load the user's .zshenv, report folder and title, mark prompts and commands.
|
||||
if [[ -n "${ELY_ZSH_ZDOTDIR+set}" ]]; then
|
||||
ZDOTDIR=$ELY_ZSH_ZDOTDIR
|
||||
builtin unset ELY_ZSH_ZDOTDIR
|
||||
@@ -7,8 +7,13 @@ else
|
||||
fi
|
||||
[[ -r "${ZDOTDIR:-$HOME}/.zshenv" ]] && builtin source "${ZDOTDIR:-$HOME}/.zshenv"
|
||||
if [[ -o interactive ]]; then
|
||||
_ely_ran=
|
||||
_ely_precmd() {
|
||||
builtin local ret=$? LC_ALL=C char url=
|
||||
if [[ -n $_ely_ran ]]; then
|
||||
builtin printf '\e]133;D;%s\a' $ret
|
||||
_ely_ran=
|
||||
fi
|
||||
for char in ${(s::)PWD}; do
|
||||
case $char in
|
||||
[[:cntrl:]]|%) builtin printf -v char '%%%02X' "'$char" ;;
|
||||
@@ -16,12 +21,21 @@ if [[ -o interactive ]]; then
|
||||
url+=$char
|
||||
done
|
||||
builtin printf '\e]7;file://%s\a\e]2;%s\a' "$url" "${${${PWD:t}:-/}//[[:cntrl:]]/}"
|
||||
# Themes set PS1 in later hooks; from the next prompt the marks go on after them.
|
||||
precmd_functions=(${precmd_functions:#_ely_mark} _ely_mark)
|
||||
builtin return ret
|
||||
}
|
||||
_ely_mark() {
|
||||
if [[ -o prompt_percent && $PS1 != *$'\e]133;A'* ]]; then
|
||||
PS1=$'%{\e]133;A\a%}'$PS1$'%{\e]133;B\a%}'
|
||||
fi
|
||||
}
|
||||
_ely_preexec() {
|
||||
builtin printf '\e]2;%s\a' "${1//[[:cntrl:]]/}"
|
||||
_ely_ran=1
|
||||
builtin printf '\e]133;C\a\e]2;%s\a' "${1//[[:cntrl:]]/}"
|
||||
}
|
||||
builtin autoload -Uz add-zsh-hook
|
||||
add-zsh-hook precmd _ely_precmd
|
||||
add-zsh-hook precmd _ely_mark
|
||||
add-zsh-hook preexec _ely_preexec
|
||||
fi
|
||||
@@ -24,6 +24,8 @@ pub struct Session {
|
||||
pub reports: bool,
|
||||
/// A bell rang while it was hidden.
|
||||
pub bell: bool,
|
||||
/// The last command's exit status, as its shell reported it; none before one, or when it gave none.
|
||||
pub status: Option<i32>,
|
||||
pub _events: Subscription,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/// A semantic prompt mark a program sends, OSC 133.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Mark {
|
||||
/// A prompt starts: A, or N.
|
||||
Prompt,
|
||||
/// The prompt ends and input starts: B, or I.
|
||||
Input,
|
||||
/// Input ends and the command's output starts: C.
|
||||
Output,
|
||||
/// The command ended, with the status it gave: D.
|
||||
Done(Option<i32>),
|
||||
}
|
||||
|
||||
/// The mark in an OSC 133 body past `133;`; none for one the terminal takes no action on.
|
||||
pub(crate) fn read(body: &[u8]) -> Option<Mark> {
|
||||
let shown = || String::from_utf8_lossy(body);
|
||||
let Some((&letter, rest)) = body.split_first() else {
|
||||
log::warn!("pty: OSC 133 without a mark");
|
||||
return None;
|
||||
};
|
||||
let options = match rest {
|
||||
[] => rest,
|
||||
[b';', options @ ..] => options,
|
||||
_ => {
|
||||
log::warn!("pty: OSC 133;{} is no mark", shown());
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match letter {
|
||||
b'A' | b'N' => Some(Mark::Prompt),
|
||||
b'B' | b'I' => Some(Mark::Input),
|
||||
b'C' => Some(Mark::Output),
|
||||
b'D' => Some(Mark::Done(status(options))),
|
||||
// L's fresh line needs the cursor, unseen before the parser; P adds nothing to A.
|
||||
b'L' | b'P' => {
|
||||
log::debug!("pty: OSC 133;{} takes no action here", shown());
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
log::warn!("pty: OSC 133;{} is no mark", shown());
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// D's status, its first option; none when the program gave none or no number.
|
||||
fn status(options: &[u8]) -> Option<i32> {
|
||||
let first = options.split(|&byte| byte == b';').next()?;
|
||||
if first.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let status = std::str::from_utf8(first)
|
||||
.ok()
|
||||
.and_then(|text| text.parse().ok());
|
||||
if status.is_none() {
|
||||
log::warn!(
|
||||
"pty: OSC 133;D with a status that is no number ({})",
|
||||
String::from_utf8_lossy(first)
|
||||
);
|
||||
}
|
||||
status
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn each_mark_reads_with_its_options_as_ghostty_reads_it() {
|
||||
assert_eq!(read(b"A"), Some(Mark::Prompt));
|
||||
assert_eq!(read(b"A;click_events=1"), Some(Mark::Prompt));
|
||||
assert_eq!(read(b"N;aid=7"), Some(Mark::Prompt));
|
||||
assert_eq!(read(b"B"), Some(Mark::Input));
|
||||
assert_eq!(read(b"I"), Some(Mark::Input));
|
||||
assert_eq!(read(b"C;cmdline_url=false"), Some(Mark::Output));
|
||||
assert_eq!(read(b"D;1"), Some(Mark::Done(Some(1))));
|
||||
assert_eq!(read(b"D;127;aid=foo"), Some(Mark::Done(Some(127))));
|
||||
assert_eq!(read(b"D;-1"), Some(Mark::Done(Some(-1))));
|
||||
assert_eq!(read(b"D"), Some(Mark::Done(None)));
|
||||
assert_eq!(read(b"D;"), Some(Mark::Done(None)));
|
||||
assert_eq!(read(b"D;abc"), Some(Mark::Done(None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mark_the_terminal_takes_no_action_on_reads_as_none() {
|
||||
for body in [&b""[..], b"L", b"P;k=s", b"AB", b"Q", b"a"] {
|
||||
assert_eq!(read(body), None, "{}", String::from_utf8_lossy(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod keys;
|
||||
mod links;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod mac;
|
||||
mod marks;
|
||||
pub mod mouse;
|
||||
mod paint;
|
||||
mod pointer;
|
||||
|
||||
+19
-4
@@ -11,7 +11,7 @@ use alacritty_terminal::{
|
||||
};
|
||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded};
|
||||
|
||||
use super::{mouse::MouseMode, tap::Tapped};
|
||||
use super::{marks::Mark, mouse::MouseMode, tap::Tapped};
|
||||
use crate::settings::{CursorStyle, Settings};
|
||||
|
||||
/// What a session hears from its pty.
|
||||
@@ -20,6 +20,7 @@ pub enum PtyEvent {
|
||||
Term(Event),
|
||||
Folder(PathBuf),
|
||||
Mouse(MouseMode),
|
||||
Mark(Mark),
|
||||
}
|
||||
|
||||
/// Hands the grid's events to the session; `pid` names the program in logs.
|
||||
@@ -256,6 +257,7 @@ mod tests {
|
||||
|
||||
struct Ran {
|
||||
folders: Vec<PathBuf>,
|
||||
marks: Vec<Mark>,
|
||||
text: String,
|
||||
success: bool,
|
||||
pid: u32,
|
||||
@@ -272,23 +274,25 @@ mod tests {
|
||||
};
|
||||
let (pty, mut events) =
|
||||
spawn(&launch, size, (8, 16), config(&Settings::default())).expect("sh starts");
|
||||
let (folders, success) = futures::executor::block_on(async {
|
||||
let (mut folders, mut success) = (Vec::new(), false);
|
||||
let (folders, marks, success) = futures::executor::block_on(async {
|
||||
let (mut folders, mut marks, mut success) = (Vec::new(), Vec::new(), false);
|
||||
while let Some(event) = events.next().await {
|
||||
match event {
|
||||
PtyEvent::Folder(folder) => folders.push(folder),
|
||||
PtyEvent::Mark(mark) => marks.push(mark),
|
||||
PtyEvent::Term(Event::ChildExit(status)) => success = status.success(),
|
||||
PtyEvent::Term(Event::Exit) => break,
|
||||
PtyEvent::Term(_) | PtyEvent::Mouse(_) => {}
|
||||
}
|
||||
}
|
||||
(folders, success)
|
||||
(folders, marks, success)
|
||||
});
|
||||
let term = pty.term.lock();
|
||||
let row = &term.grid()[Line(0)];
|
||||
let text: String = (0..24).map(|column| row[Column(column)].c).collect();
|
||||
Ran {
|
||||
folders,
|
||||
marks,
|
||||
text: text.trim_end().to_string(),
|
||||
success,
|
||||
pid: pty.pid,
|
||||
@@ -314,6 +318,17 @@ mod tests {
|
||||
assert_eq!(ran.text, "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn osc_133_reaches_the_session_as_marks() {
|
||||
let ran = run(
|
||||
"printf '\\033]133;A\\007>\\033]133;B\\007\\033]133;C\\033\\\\x\\033]133;D;3\\007'",
|
||||
(40, 4),
|
||||
);
|
||||
let marks = [Mark::Prompt, Mark::Input, Mark::Output, Mark::Done(Some(3))];
|
||||
assert_eq!(ran.marks, marks);
|
||||
assert_eq!(ran.text, ">x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_foreground_is_the_program_s_own_group() {
|
||||
let launch = Launch {
|
||||
|
||||
+29
-5
@@ -6,7 +6,7 @@ use futures::channel::mpsc::TryRecvError;
|
||||
use super::*;
|
||||
use crate::{
|
||||
settings::Settings,
|
||||
term::{Events, Pty, PtyEvent, config, spawn},
|
||||
term::{Events, Pty, PtyEvent, config, marks::Mark, spawn},
|
||||
};
|
||||
|
||||
/// A folder name with a space, a percent, a bell and a newline.
|
||||
@@ -43,6 +43,7 @@ struct Run {
|
||||
start: PathBuf,
|
||||
folders: Vec<PathBuf>,
|
||||
titles: Vec<String>,
|
||||
marks: Vec<Mark>,
|
||||
deadline: Instant,
|
||||
}
|
||||
|
||||
@@ -79,6 +80,7 @@ impl Run {
|
||||
start,
|
||||
folders: Vec::new(),
|
||||
titles: Vec::new(),
|
||||
marks: Vec::new(),
|
||||
deadline: Instant::now() + Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
@@ -88,15 +90,17 @@ impl Run {
|
||||
while !done(self) {
|
||||
assert!(
|
||||
Instant::now() < self.deadline,
|
||||
"{}: no {what}; folders {:?}, titles {:?}, screen\n{}",
|
||||
"{}: no {what}; folders {:?}, titles {:?}, marks {:?}, screen\n{}",
|
||||
self.shell,
|
||||
self.folders,
|
||||
self.titles,
|
||||
self.marks,
|
||||
self.screen()
|
||||
);
|
||||
match self.events.try_recv() {
|
||||
Ok(PtyEvent::Folder(folder)) => self.folders.push(folder),
|
||||
Ok(PtyEvent::Term(Event::Title(title))) => self.titles.push(title),
|
||||
Ok(PtyEvent::Mark(mark)) => self.marks.push(mark),
|
||||
Ok(PtyEvent::Term(Event::PtyWrite(reply))) => self.pty.write(reply.into_bytes()),
|
||||
Ok(PtyEvent::Term(Event::ChildExit(status))) => {
|
||||
panic!("{} ended, {status}:\n{}", self.shell, self.screen())
|
||||
@@ -142,6 +146,13 @@ impl Run {
|
||||
self.until("inner folder", |run| run.folders.contains(&inner));
|
||||
}
|
||||
|
||||
/// Types `line`, the last command sent, which fails with 1; its marks follow its earlier ones.
|
||||
fn fails(&mut self, line: &str) {
|
||||
self.send(line);
|
||||
let after = [Mark::Output, Mark::Done(Some(1)), Mark::Prompt, Mark::Input];
|
||||
self.until("marks of the failure", |run| run.marks.ends_with(&after));
|
||||
}
|
||||
|
||||
fn finish(mut self) {
|
||||
self.send("exit");
|
||||
loop {
|
||||
@@ -193,7 +204,10 @@ fn a_script_it_cannot_read_fails_the_install() {
|
||||
|
||||
#[test]
|
||||
fn zsh_reports_its_folder_byte_for_byte_and_its_titles() {
|
||||
let mut run = Run::start(&installed("zsh"), &[]);
|
||||
// A theme that sets PS1 in its own hook, which runs after ours.
|
||||
let theme =
|
||||
"autoload -Uz add-zsh-hook\n_theme() { PS1='THEME %? > ' }\nadd-zsh-hook precmd _theme";
|
||||
let mut run = Run::start(&installed("zsh"), &[(".zshrc", theme)]);
|
||||
run.walk("(( $+functions[_ely_precmd] )) && echo ELY-LOAD''ED");
|
||||
run.until("title", |run| {
|
||||
run.titles.iter().any(|title| title == "in ner%xy")
|
||||
@@ -203,6 +217,10 @@ fn zsh_reports_its_folder_byte_for_byte_and_its_titles() {
|
||||
"{:?}",
|
||||
run.titles
|
||||
);
|
||||
run.fails("false");
|
||||
run.until("the theme's prompt", |run| {
|
||||
run.shows("THEME 0 > false") && run.shows("THEME 1 >")
|
||||
});
|
||||
run.finish();
|
||||
}
|
||||
|
||||
@@ -215,8 +233,9 @@ fn bash_reports_its_folder_byte_for_byte_and_keeps_the_status() {
|
||||
run.until("title", |run| {
|
||||
run.titles.iter().any(|title| title == "in ner%xy")
|
||||
});
|
||||
run.send("false");
|
||||
run.fails("false");
|
||||
run.until("kept status", |run| run.shows("USER_STATUS=1"));
|
||||
assert!(run.shows("$ false"), "{}", run.screen());
|
||||
run.finish();
|
||||
}
|
||||
|
||||
@@ -228,6 +247,10 @@ fn fish_loads_its_script_and_restores_its_data_dirs() {
|
||||
run.send(r#"echo "DIRS[$ELY_FISH_XDG_DATA_DIRS][$XDG_DATA_DIRS]""#);
|
||||
let restored = format!("DIRS[][{previous}]");
|
||||
run.until("restored dirs", |run| run.shows(&restored));
|
||||
// The prompt hears each command's own status, none failed so far.
|
||||
assert!(!run.shows("[1]"), "{}", run.screen());
|
||||
run.fails("false");
|
||||
run.until("the failed status", |run| run.shows("[1]>"));
|
||||
run.finish();
|
||||
}
|
||||
|
||||
@@ -244,8 +267,9 @@ fn powershell_reports_its_folder_byte_for_byte_and_keeps_the_status() {
|
||||
run.until("title", |run| {
|
||||
run.titles.iter().any(|title| title == "in ner%xy")
|
||||
});
|
||||
run.send("/usr/bin/false");
|
||||
run.fails("/usr/bin/false");
|
||||
run.until("kept status", |run| run.shows("USER_SUCCESS=False"));
|
||||
assert!(run.shows("> /usr/bin/false"), "{}", run.screen());
|
||||
run.finish();
|
||||
}
|
||||
|
||||
|
||||
+16
-9
@@ -7,7 +7,7 @@ use alacritty_terminal::{
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use polling::{Event, PollMode, Poller};
|
||||
|
||||
use super::{mouse::MouseMode, pty::PtyEvent};
|
||||
use super::{marks, mouse::MouseMode, pty::PtyEvent};
|
||||
|
||||
/// The longest OSC body kept; longer ones are dropped whole.
|
||||
const MOST: usize = 4096;
|
||||
@@ -15,7 +15,7 @@ const MOST: usize = 4096;
|
||||
/// A pty whose reader shows each byte to its scanners before the parser.
|
||||
pub(crate) struct Tapped<P> {
|
||||
pub inner: P,
|
||||
scanner: Osc7,
|
||||
scanner: Osc,
|
||||
mouse: MouseModes,
|
||||
events: UnboundedSender<PtyEvent>,
|
||||
pid: u32,
|
||||
@@ -26,7 +26,7 @@ impl<P> Tapped<P> {
|
||||
pub fn new(inner: P, events: UnboundedSender<PtyEvent>, pid: u32) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
scanner: Osc7::default(),
|
||||
scanner: Osc::default(),
|
||||
mouse: MouseModes::default(),
|
||||
events,
|
||||
pid,
|
||||
@@ -40,7 +40,14 @@ impl<P: EventedReadWrite> io::Read for Tapped<P> {
|
||||
let count = self.inner.reader().read(buf)?;
|
||||
let (events, pid, last) = (&self.events, self.pid, &mut self.last);
|
||||
self.scanner.feed(&buf[..count], &mut |body| {
|
||||
let Some(folder) = folder(body) else {
|
||||
if let Some(mark) = body.strip_prefix(b"133;").and_then(marks::read) {
|
||||
log::debug!("pty {pid}: {mark:?}");
|
||||
if events.unbounded_send(PtyEvent::Mark(mark)).is_err() {
|
||||
log::debug!("pty {pid}: a mark after its session closed");
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(folder) = body.strip_prefix(b"7;").and_then(folder) else {
|
||||
return;
|
||||
};
|
||||
if last.as_ref() == Some(&folder) {
|
||||
@@ -114,15 +121,15 @@ enum State {
|
||||
OscEscape,
|
||||
}
|
||||
|
||||
/// Finds `ESC ] 7 ; body` ended by BEL or `ESC \`, across reads.
|
||||
/// Finds each OSC body, `ESC ] body` ended by BEL or `ESC \`, across reads.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Osc7 {
|
||||
pub(crate) struct Osc {
|
||||
state: State,
|
||||
body: Vec<u8>,
|
||||
overflow: bool,
|
||||
}
|
||||
|
||||
impl Osc7 {
|
||||
impl Osc {
|
||||
pub fn feed(&mut self, bytes: &[u8], found: &mut impl FnMut(&[u8])) {
|
||||
let mut rest = bytes;
|
||||
while !rest.is_empty() {
|
||||
@@ -175,8 +182,8 @@ impl Osc7 {
|
||||
fn finish(&mut self, found: &mut impl FnMut(&[u8])) {
|
||||
if self.overflow {
|
||||
log::warn!("pty: an OSC over {MOST} bytes dropped");
|
||||
} else if let Some(body) = self.body.strip_prefix(b"7;") {
|
||||
found(body);
|
||||
} else {
|
||||
found(&self.body);
|
||||
}
|
||||
self.body.clear();
|
||||
}
|
||||
|
||||
+14
-8
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
fn scan(chunks: &[&[u8]]) -> Vec<String> {
|
||||
let mut scanner = Osc7::default();
|
||||
let mut scanner = Osc::default();
|
||||
let mut found = Vec::new();
|
||||
for chunk in chunks {
|
||||
scanner.feed(chunk, &mut |body| {
|
||||
@@ -12,17 +12,23 @@ fn scan(chunks: &[&[u8]]) -> Vec<String> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_osc_7_ended_by_bel_or_st_across_reads() {
|
||||
assert_eq!(scan(&[b"ab\x1b]7;file:///a\x07cd"]), ["file:///a"]);
|
||||
assert_eq!(scan(&[b"\x1b]7;file:", b"///b\x1b", b"\\"]), ["file:///b"]);
|
||||
assert_eq!(scan(&[b"\x1b", b"]7;x\x07\x1b]7;y\x1b\\"]), ["x", "y"]);
|
||||
fn finds_each_osc_body_ended_by_bel_or_st_across_reads() {
|
||||
assert_eq!(scan(&[b"ab\x1b]7;file:///a\x07cd"]), ["7;file:///a"]);
|
||||
assert_eq!(
|
||||
scan(&[b"\x1b]7;file:", b"///b\x1b", b"\\"]),
|
||||
["7;file:///b"]
|
||||
);
|
||||
assert_eq!(
|
||||
scan(&[b"\x1b", b"]133;A\x07\x1b]0;t\x1b\\"]),
|
||||
["133;A", "0;t"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_sequences_and_cancels_pass() {
|
||||
assert!(scan(&[b"\x1b]0;title\x07\x1b[31m"]).is_empty());
|
||||
assert!(scan(&[b"\x1b[31m\x1bc"]).is_empty());
|
||||
assert!(scan(&[b"\x1b]7;file:///a\x18\x07"]).is_empty());
|
||||
assert_eq!(scan(&[b"\x1b]7;lost\x1b]7;kept\x07"]), ["kept"]);
|
||||
assert_eq!(scan(&[b"\x1b]7;lost\x1b]133;D;1\x07"]), ["133;D;1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -31,7 +37,7 @@ fn an_overlong_body_is_dropped() {
|
||||
long.extend(std::iter::repeat_n(b'a', MOST + 1));
|
||||
long.push(0x07);
|
||||
assert!(scan(&[&long]).is_empty());
|
||||
assert_eq!(scan(&[&long, b"\x1b]7;b\x07"]), ["b"]);
|
||||
assert_eq!(scan(&[&long, b"\x1b]7;b\x07"]), ["7;b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,6 +22,7 @@ use super::{
|
||||
frame::frame,
|
||||
ink::Ink,
|
||||
input::{self, Find},
|
||||
marks::Mark,
|
||||
mouse::MouseMode,
|
||||
paint::{self, Look},
|
||||
pointer::Pointer,
|
||||
@@ -48,6 +49,8 @@ pub enum TerminalEvent {
|
||||
Title(String),
|
||||
Folder(PathBuf),
|
||||
Bell,
|
||||
/// A command the shell ran ended, with the status it gave.
|
||||
Finished(Option<i32>),
|
||||
Exited(ExitStatus),
|
||||
}
|
||||
|
||||
@@ -218,6 +221,8 @@ impl TerminalView {
|
||||
}
|
||||
PtyEvent::Term(event) => self.term_event(event, cx),
|
||||
PtyEvent::Mouse(mode) => self.mouse = mode,
|
||||
PtyEvent::Mark(Mark::Done(status)) => cx.emit(TerminalEvent::Finished(status)),
|
||||
PtyEvent::Mark(_) => {}
|
||||
}
|
||||
}
|
||||
// ponytail: a full search per output batch while find is open; search only new lines if it lags.
|
||||
|
||||
+4
-47
@@ -7,7 +7,7 @@ use ely_gpui_component::{
|
||||
typography::Caption,
|
||||
};
|
||||
use gpui::{
|
||||
Action, App, AppContext, Context, Entity, EntityId, FocusHandle, Focusable, InteractiveElement,
|
||||
Action, App, AppContext, Context, Entity, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, KeyBinding, ParentElement, Render, SharedString, StatefulInteractiveElement,
|
||||
StyleRefinement, Styled, Subscription, Window, actions, div, prelude::FluentBuilder,
|
||||
};
|
||||
@@ -20,12 +20,13 @@ use crate::{
|
||||
sessions::{self, Session},
|
||||
settings::{PanelTab, Settings, SettingsStore, StartIn, window::OpenSettings},
|
||||
term::{
|
||||
Events, Launch, Pty, TerminalEvent, TerminalView, config,
|
||||
Events, Launch, Pty, TerminalView, config,
|
||||
shell::{ShellIntegration, launch},
|
||||
spawn,
|
||||
},
|
||||
};
|
||||
|
||||
mod events;
|
||||
mod widths;
|
||||
|
||||
use widths::{Dragged, Side};
|
||||
@@ -153,6 +154,7 @@ impl Workspace {
|
||||
folder: launch.cwd,
|
||||
reports: launch.reports,
|
||||
bell: false,
|
||||
status: None,
|
||||
_events: events,
|
||||
});
|
||||
}
|
||||
@@ -242,51 +244,6 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn session_event(
|
||||
&mut self,
|
||||
id: EntityId,
|
||||
event: &TerminalEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(index) = self
|
||||
.sessions
|
||||
.iter()
|
||||
.position(|session| session.terminal.entity_id() == id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let session = &mut self.sessions[index];
|
||||
match event {
|
||||
TerminalEvent::Title(title) => session.title.clone_from(title),
|
||||
TerminalEvent::Folder(folder) => {
|
||||
session.folder.clone_from(folder);
|
||||
session.reports = true;
|
||||
if index == self.active {
|
||||
self.files.update(cx, |files, cx| files.show(folder, cx));
|
||||
self.git.update(cx, |git, cx| git.show(folder, cx));
|
||||
}
|
||||
}
|
||||
TerminalEvent::Bell if index != self.active => {
|
||||
if !session.bell {
|
||||
log::debug!("app: a bell in hidden session {}", index + 1);
|
||||
}
|
||||
session.bell = true;
|
||||
}
|
||||
TerminalEvent::Bell => {}
|
||||
TerminalEvent::Exited(status) => {
|
||||
let pid = session.terminal.read(cx).pid();
|
||||
log::info!("app: session {pid}'s program ended, {status}");
|
||||
self.close(index, window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if index == self.active {
|
||||
self.retitle(window);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Shows or hides a side column, as the settings keep it.
|
||||
fn toggle(&mut self, flip: fn(&mut Settings), cx: &mut Context<Self>) {
|
||||
match SettingsStore::update(cx, flip) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
use gpui::{Context, EntityId, Window};
|
||||
|
||||
use super::Workspace;
|
||||
use crate::term::TerminalEvent;
|
||||
|
||||
impl Workspace {
|
||||
/// What session `id`'s terminal reported.
|
||||
pub(super) fn session_event(
|
||||
&mut self,
|
||||
id: EntityId,
|
||||
event: &TerminalEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(index) = self
|
||||
.sessions
|
||||
.iter()
|
||||
.position(|session| session.terminal.entity_id() == id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let session = &mut self.sessions[index];
|
||||
match event {
|
||||
TerminalEvent::Title(title) => session.title.clone_from(title),
|
||||
TerminalEvent::Folder(folder) => {
|
||||
session.folder.clone_from(folder);
|
||||
session.reports = true;
|
||||
if index == self.active {
|
||||
self.files.update(cx, |files, cx| files.show(folder, cx));
|
||||
self.git.update(cx, |git, cx| git.show(folder, cx));
|
||||
}
|
||||
}
|
||||
TerminalEvent::Bell if index != self.active => {
|
||||
if !session.bell {
|
||||
log::debug!("app: a bell in hidden session {}", index + 1);
|
||||
}
|
||||
session.bell = true;
|
||||
}
|
||||
TerminalEvent::Bell => {}
|
||||
TerminalEvent::Finished(status) => {
|
||||
if session.status != *status {
|
||||
let pid = session.terminal.read(cx).pid();
|
||||
let ended =
|
||||
status.map_or("with no status".into(), |code| format!("with {code}"));
|
||||
log::info!("app: session {pid}'s last command ended {ended}");
|
||||
}
|
||||
session.status = *status;
|
||||
}
|
||||
TerminalEvent::Exited(status) => {
|
||||
let pid = session.terminal.read(cx).pid();
|
||||
log::info!("app: session {pid}'s program ended, {status}");
|
||||
self.close(index, window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if index == self.active {
|
||||
self.retitle(window);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -49,8 +49,9 @@ What no check reached: no real KWin drew the blur; of Wayland desktops, only Hyp
|
||||
|
||||
| PRD line | Task | Evidence | Status |
|
||||
|---|---|---|---|
|
||||
| PTY through alacritty's event loop; OSC 7 scanned on the reader | T04 | `term::pty`, `term::tap` tests; round 3 failed at the cap, its fix stands on a red-green test | Pass |
|
||||
| PTY through alacritty's event loop; OSC 7 and OSC 133 scanned on the reader | T04, T34 | `term::pty`, `term::tap` tests; round 3 failed at the cap, its fix stands on a red-green test | Pass |
|
||||
| Shell integration for zsh, bash, fish, PowerShell; a shell without it reports nothing and the panels say so | T05 | `term::shell` tests on macOS and Linux | Pass |
|
||||
| Prompt marks, OSC 133, from the shell integration and any program; each session keeps its last command's status | T34 | `term::marks` tests: each letter and option as Ghostty reads them; `term::tap` and `term::pty` tests: marks split across reads reach the session in order; `term::shell` tests: zsh, bash, fish and PowerShell send C, D;1, A and B around a failing command, and the user's prompt still shows the failure, a zsh theme's that sets PS1 in its own hook too; on macOS fish 4.9 marks itself and bash 3.2 marks C through its DEBUG trap, in the Linux container fish 4.0 gets B and bash 5.2 marks C through PS0, CI's Ubuntu runs fish 3.7, which the script marks whole; by hand in a pty, PowerShell under strict mode, and a cmdlet's failure after a native one's reads 1 | Pass |
|
||||
| Inside herdr, the folder of herdr's focused pane, read from its socket, on macOS, Linux and Windows | T29, T32 | `term::herdr` tests: the socket found as herdr 0.9 finds it, only the terminal client followed and a remote one refused, the snapshot read, a socket round trip, a process's path, arguments and variables; `the_foreground_is_the_program_s_own_group`; on X11 with herdr 0.9.5 run in the session: its start showed the focused pane's folder, a cd in the pane moved the panel, the focus moved to a pane in `/etc` and back moved it each time, and the client's end gave the panel back to bash's report, then `exec herdr` from that bash, with `HERDR_SOCKET_PATH` relative to its folder, was followed through the socket there; on macOS under `shot`, herdr 0.9.5 as the program and again by `exec` from `sh` with a relative socket, its pane's cd moved the panel; on Windows, `term::herdr` tests on the CI runner: the folder under `APPDATA` and `USERPROFILE`, the `herdr.exe` nearest under the shell, this test's name, arguments and variables read through the live process list and `sysinfo`, a round trip over a named pipe; no real herdr has run on Windows yet | Partial |
|
||||
| Colors, styles, five underlines, wide characters, combining marks | T06 | `term::frame`, `term::ink`, `term::paint` tests | Pass |
|
||||
| Box drawing, blocks and powerline shapes as geometry | T07 | `term::boxes` tests | Pass |
|
||||
|
||||
Reference in new issue
Block a user