T58: the terminal shows a scrollbar over its history, which a user asked for in issue #2 after 0.1.5, since a program that takes the mouse takes the wheel: a thumb along the right edge, in Ely's sizes and colors, stands where the view stands, and a drag moves the view through the history; none shows without history
CI / platforms (windows-latest) (push) Successful in 1m35s
CI / platforms (ubuntu-24.04) (push) Successful in 2m44s
CI / macos (push) Successful in 14m37s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-19 11:04:48 -04:00
1 parent 9e185eab58
commit 8a9941bfd2
9 files changed
+194 -7

No files matched your search

+2 -1
View File
@@ -37,7 +37,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- `src/pet.rs`: Ely, peeking over the main window's top edge from a window of her own while the settings want her, or up from its bottom edge where no room lies above it; she draws at every frame the display gives her window; `pet/rig` draws her from the icon's paths in layers of one color, each moved by a matrix of its own, as the films rig her; `pet/look` reads her mood from the sessions' marks and her pose at each moment of it, `look/curves` the timings of her moves; and `pet/macos`, `pet/windows` and `pet/x11` keep her window on the main window's edge where the platform does not.
- `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 0, 2, 7, 9, 99, 133, 777 and 7501, the title stack's CSI 22 t and 23 t, full resets, the mouse modes and, on Windows, win32-input-mode (`tap/modes`) before the parser; `titles` keeps the title a session shows, read in the output's order; `marks` reads OSC 133's prompt marks; `status` reads OSC 7501 and keeps a terminal's records; `notify` reads OSC 9, 777 and 99, the notifications and OSC 9;4's progress, and gathers OSC 99's chunks; `reports` is the view's side of what programs report: it keeps OSC 7501's records, maps OSC 9;4, answers OSC 99's query and alive, and posts the system notifications its session posts; `prompts` turns each mark into an OSC 8 link the parser keeps in a prompt's cells, and finds the prompt to jump to; `herdr` follows herdr's focused pane and its agents' states through herdr's socket while herdr's client runs in the session, and tells the view herdr leads, so herdr opens its links: `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, the repeat flag and the key code of AppKit's current event, and the key's own character from the layout, as gpui reads key names; it keeps held keys repeating, and hands Command key-ups to the focused view. `win32` sends a key as Windows' own record, win32-input-mode, and finds a key's own character in Windows' layout. `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.
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 0, 2, 7, 9, 99, 133, 777 and 7501, the title stack's CSI 22 t and 23 t, full resets, the mouse modes and, on Windows, win32-input-mode (`tap/modes`) before the parser; `titles` keeps the title a session shows, read in the output's order; `marks` reads OSC 133's prompt marks; `status` reads OSC 7501 and keeps a terminal's records; `notify` reads OSC 9, 777 and 99, the notifications and OSC 9;4's progress, and gathers OSC 99's chunks; `reports` is the view's side of what programs report: it keeps OSC 7501's records, maps OSC 9;4, answers OSC 99's query and alive, and posts the system notifications its session posts; `prompts` turns each mark into an OSC 8 link the parser keeps in a prompt's cells, and finds the prompt to jump to; `herdr` follows herdr's focused pane and its agents' states through herdr's socket while herdr's client runs in the session, and tells the view herdr leads, so herdr opens its links: `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, the repeat flag and the key code of AppKit's current event, and the key's own character from the layout, as gpui reads key names; it keeps held keys repeating, and hands Command key-ups to the focused view. `win32` sends a key as Windows' own record, win32-input-mode, and finds a key's own character in Windows' layout. `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. `scrollbar` is the thumb over the history that a drag moves through it. `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).
@@ -113,3 +113,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-19: Ely's window shows nothing but her, which the owner asked for after a user's 0.1.4 drew a hairline along the top of her box on macOS. gpui opens a popup as a titled panel, style mask 0x8081, titled, full-size content and nonactivating, and macOS draws a titled window's frame, there as a light edge over the clear room above her head. As it is glued her panel turns borderless, nonactivating alone, 0x80, and the log names both masks; a mask that does not take fails the glue, which closes her window, as any failed glue now does, where her window stayed open, unplaced, while the log said she did not show. macOS 27.2 draws nothing of that frame, so the line never showed here, the app active or not; there a real click on her borderless panel still winks her, the front app stays in front, and keys typed after it reach the terminal. On Windows her bare popup, and on Linux X11's override-redirect window and Wayland's xdg_popup, draw no frame, border or shadow.
- 2026-10-19: a drag moves Ely along the edge she stands on, which the owner asked for after 0.1.5. On macOS, Windows and X11 her glue moves her window after the pointer's place on the screen, `NSEvent`'s `mouseLocation`, `GetCursorPos` and `query_pointer`, since her window moves under the pointer, so a place in it would lag; her window keeps the pointer while the button is held, inside it or not, as AppKit, gpui's `SetCapture` and X's implicit grab give it. A Wayland popup cannot move, so there she moves once let go: the settings keep the place, and the twice-a-second check opens her popup there. She moves by whole points inside the pane, and `ely_left` keeps where she stands, 140 by default, where she stood before; a narrower pane holds her at its right end, and the log names where she is let go. Under Xorg's dummy screen xdotool dragged her 200 pixels and her window followed them; under Sway nested in Xorg, whose X input gives its seat a pointer, xdotool's drag reopened her popup 200 points on. Sway's own `seat cursor` commands send no motion while their press is held, and Sway draws no popup opened before its window settled, so a probe there reopens her before it drags.
- 2026-10-19: Settings › Terminal checks for updates, which the owner asked for after 0.1.5: under Ely, a switch, on by default, checks at start and once a day, and Check now checks at once. The check runs `curl`, as git runs through its command line, so no HTTP or TLS crate joins the build; macOS and Windows 10 ship it, and a missing curl shows as a failed check. It asks Gitea's API for the latest published release, compares its tag's three numbers with the running version, and the row says what it found; a newer one brings Download, which opens the release's page in the browser, since replacing a running, signed app is a job of its own. An automatic check that finds one posts a system notification, at most once a day, whose press opens the page. Checks run off the main thread, one at a time, each start and finding logged; a failure shows its error in the row. Under Xorg's dummy screen the start's check found 0.1.5 the latest, Check now and the switch checked again, and a build named 0.1.4 showed 0.1.5 out with Download, which opened its page.
- 2026-10-19: the terminal shows a scrollbar, which a user asked for in issue #2 after 0.1.5: a program that tracks the mouse takes the wheel, so the wheel alone could not reach the history. A thumb rides the view's right edge, over the padding, in Ely's scrollbar sizes and colors, its thickness, its least length and the foreground at 22 and 42 percent; its length is the screen's share of the history, and its place where the view stands. A drag moves the view through the history, whatever mouse mode the program asked for; the bar covers the grid's hitbox, so the terminal sees none of its presses, and a release with no press of its own reports nothing. Ely's `Scrollbar` reads a gpui `ScrollHandle`, and the terminal's place is alacritty's display offset, so the bar is the terminal's own, drawn as Ely draws hers and dragged through gpui's drag as hers is; gpui sets a drag's anchor at its first move past the threshold, so a pointer that jumps far at once loses that jump. The alternate screen holds no history, so it shows none. Under Xorg's dummy screen, 500 lines in, a drag to the top showed the first prompt and lines 1 to 39, and with mouse tracking on, which sent the wheel to the program, the bar still scrolled.
+1
View File
@@ -60,6 +60,7 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
- Clipboard: copy, paste (bracketed when asked), copy on select per setting, OSC 52 writes.
- Links: OSC 8 and plain URLs; Cmd-press (Ctrl-press off macOS) opens them. Inside herdr, while it takes the mouse, herdr opens them, web links only, as herdr does, whole where its pane wraps them, and underlines them while the key is held; with Shift the terminal opens what it sees.
- Focus reporting, title (OSC 0/2), scrollback limit per setting, find in scrollback (Ely's `FindWidget`).
- A scrollbar over the history, along the terminal's right edge: a drag on its thumb scrolls, whatever the program does with the wheel.
- Bell: no sound. The session row marks it until the session is shown.
## 6. Main window and layout
+1
View File
@@ -42,6 +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, and on Windows win32-input-mode, as Windows Terminal sends them, so herdr's shortcuts work; 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; inside herdr, herdr opens them, web links only, whole where its pane wraps them, and with Shift the terminal opens what it sees; find searches the scrollback.
- A scrollbar along the terminal's right edge scrolls its history, even while a program takes the wheel.
- 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.
- Ely peeks over the window's top edge, or up from its bottom left when the window leaves her no room above it, and moves all the time: she breathes, sways and blinks. She follows your sessions: she hops when a program waits on you, shakes at an error, cheers a finish, reads along while a program works, and dozes when all is calm for long. Press her and she winks, waking her from a doze. Drag her along the edge to move her. Settings › Terminal › Ely turns her off.
- Sessions: new, close and switch. A bell marks a hidden session's row and makes no sound. A row's mark shows what its programs report through OSC 7501, OSC 9;4 or herdr: waiting on you, an error, work with its progress, done or idle, else a failed last command. The mark is 3 by 3 dots: work ripples and waiting dips while they last. Hover over the mark lists each report.
+3 -2
View File
@@ -140,11 +140,12 @@ Progress truth. Top to bottom, one at a time. A task is ticked only after its co
- [x] T54 Release v0.1.5 with T51 to T53: version 0.1.5; the macOS disk image, the Windows zip and the Linux tarball, each from its script; the notes list what changed.
## 30. Ely's drag and updates
## 30. Ely's drag, updates and the scrollbar
- [x] T55 A drag moves Ely along the edge she stands on, which the owner asked for after 0.1.5: she follows the pointer sideways inside the pane, on macOS, Windows and X11 as it moves, on Wayland, whose popup keeps its place, once it lets go; where she stands, in points from the pane's left edge, the settings keep as `ely_left`.
- [x] T56 Settings › Terminal checks for updates, which the owner asked for after 0.1.5: under Ely, a switch checks ELYTerminal's latest release at start and once a day, on by default, and a button checks at once; the row says what the check found, and a newer release brings a button that opens its page; a newer release an automatic check finds posts a system notification, whose press opens that page.
- [x] T58 The terminal shows a scrollbar over its history, which a user asked for in issue #2 after 0.1.5, since a program that takes the mouse takes the wheel too: a thumb along its right edge, in Ely's sizes and colors, stands where the view stands in the history, and a drag on it moves the view through it; none shows without history, as on the alternate screen, and a press on it never reaches the terminal.
## 31. Release
- [ ] T57 Release v0.1.6 with T55 and T56: version 0.1.6; the macOS disk image, the Windows zip and the Linux tarball, each from its script; the notes list what changed.
- [ ] T57 Release v0.1.6 with T55, T56 and T58: version 0.1.6; the macOS disk image, the Windows zip and the Linux tarball, each from its script; the notes list what changed.
+1
View File
@@ -16,6 +16,7 @@ mod pointer;
mod prompts;
mod pty;
mod reports;
mod scrollbar;
pub mod shell;
mod status;
mod tap;
+170
View File
@@ -0,0 +1,170 @@
//! The terminal's scrollbar: a thumb over the history, in Ely's sizes and colors, that a drag moves through it, whatever the wheel is bound to.
use std::{cell::Cell, rc::Rc};
use alacritty_terminal::{
grid::{Dimensions, Scroll},
term::Term,
};
use ely_gpui_component::theme::ActiveTheme;
use gpui::{
AnyElement, AppContext as _, Context, DragMoveEvent, EmptyView, EntityId, InteractiveElement,
IntoElement, ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, div, relative,
};
use super::view::TerminalView;
/// A drag of the thumb: its view, how far down the history the view stood as it began, and the drag's first point.
pub(super) struct ThumbDrag {
owner: EntityId,
from: f32,
anchor: Rc<Cell<Option<Pixels>>>,
}
/// Where the view stands in the history: the lines above the screen, the screen's lines, and how far up the view is.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct Span {
history: usize,
screen: usize,
offset: usize,
}
impl Span {
pub(super) fn of<T>(term: &Term<T>) -> Self {
Self {
history: term.grid().history_size(),
screen: term.screen_lines(),
offset: term.grid().display_offset(),
}
}
/// The thumb's share of the track.
fn share(self) -> f32 {
self.screen as f32 / (self.history + self.screen) as f32
}
/// How far down the history the view stands: 0 at its top, 1 at the bottom.
fn ratio(self) -> f32 {
1.0 - self.offset as f32 / self.history as f32
}
/// How far up the view stands with the thumb `ratio` down the track.
fn offset_at(self, ratio: f32) -> usize {
((1.0 - ratio.clamp(0.0, 1.0)) * self.history as f32).round() as usize
}
}
/// The bar along the view's right edge, over the padding there; none without history, as on the alternate screen.
pub(super) fn bar(span: Span, window: &Window, cx: &Context<TerminalView>) -> Option<AnyElement> {
if span.history == 0 {
return None;
}
let theme = cx.theme();
let rem = window.rem_size();
let thickness = theme.scrollbar_thickness().to_pixels(rem);
let min_thumb = theme.scrollbar_min_thumb().to_pixels(rem);
let (idle, busy) = (theme.colors.fg.opacity(0.22), theme.colors.fg.opacity(0.42));
let ratio = span.ratio();
// The room above and below the thumb shares what it leaves as the view stands.
let (mut above, mut below) = (div(), div());
above.style().flex_grow = Some(ratio);
below.style().flex_grow = Some(1.0 - ratio);
let drag = ThumbDrag {
owner: cx.entity_id(),
from: ratio,
anchor: Rc::default(),
};
let thumb = div()
.id("thumb")
.w(thickness)
.h(relative(span.share()))
.min_h(min_thumb)
.flex_shrink_0()
.rounded_full()
.bg(idle)
.hover(|style| style.bg(busy))
.on_drag(drag, |_, _, _, cx| cx.new(|_| EmptyView));
let bar = div()
.id("scrollbar")
.absolute()
.top_0()
.bottom_0()
.right_0()
.w(thickness * 2.0)
.pl(thickness * 0.5)
.flex()
.flex_col()
.occlude()
.on_drag_move(cx.listener(TerminalView::drag_thumb))
.child(above)
.child(thumb)
.child(below);
Some(bar.into_any_element())
}
impl TerminalView {
/// Scrolls the history after the thumb, along its track's bounds.
fn drag_thumb(
&mut self,
event: &DragMoveEvent<ThumbDrag>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let drag = event.drag(cx);
if drag.owner != cx.entity_id() {
return;
}
let min_thumb = cx
.theme()
.scrollbar_min_thumb()
.to_pixels(window.rem_size());
let mut term = self.pty.term.lock();
let span = Span::of(&term);
let height = event.bounds.size.height;
let travel = height - (height * span.share()).max(min_thumb).min(height);
if span.history == 0 || travel <= Pixels::ZERO {
return;
}
let along = event.event.position.y - event.bounds.origin.y;
let anchor = drag.anchor.get().unwrap_or(along);
drag.anchor.set(Some(anchor));
let offset = span.offset_at(drag.from + (along - anchor) / travel);
let delta = offset as i32 - span.offset as i32;
if delta != 0 {
term.scroll_display(Scroll::Delta(delta));
drop(term);
log::trace!(
"terminal {}: the scrollbar shows {offset} lines up",
self.pty.pid
);
cx.notify();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_thumb_stands_where_the_view_stands_in_the_history() {
let span = |offset| Span {
history: 300,
screen: 100,
offset,
};
assert_eq!(span(0).share(), 0.25, "a screen of four");
assert_eq!(span(0).ratio(), 1.0, "at the bottom");
assert_eq!(span(300).ratio(), 0.0, "at the top");
assert_eq!(span(150).ratio(), 0.5);
assert_eq!(
span(0).offset_at(0.0),
300,
"the thumb at the top shows the oldest lines"
);
assert_eq!(span(0).offset_at(1.0), 0);
assert_eq!(span(0).offset_at(0.5), 150);
assert_eq!(span(0).offset_at(-0.2), 300, "past the track's ends");
assert_eq!(span(0).offset_at(1.3), 0);
}
}
+11 -4
View File
@@ -28,6 +28,7 @@ use super::{
pointer::Pointer,
pty::{Events, Pty, PtyEvent, config, fit},
reports::Notices,
scrollbar::{self, Span},
status::{Records, State},
zoom,
};
@@ -413,6 +414,7 @@ impl Render for TerminalView {
});
let shown = frame(&term, &ink, &lit);
let blinks = term.cursor_style().blinking;
let span = Span::of(&term);
drop(term);
let focused = self.focus.is_focused(window) && window.is_window_active();
self.blinking = focused && blinks;
@@ -480,9 +482,14 @@ impl Render for TerminalView {
root.text_color(ink.fg).child(format!("{error:#}"))
}
};
div().relative().size_full().child(root).children(
self.find_box(cx)
.map(|found| div().absolute().top_2().right_4().child(found)),
)
div()
.relative()
.size_full()
.child(root)
.children(scrollbar::bar(span, window, cx))
.children(
self.find_box(cx)
.map(|found| div().absolute().top_2().right_4().child(found)),
)
}
}
+1
View File
@@ -70,6 +70,7 @@ What no check reached: no real KWin drew the blur; of Wayland desktops, only Hyp
| Copy, paste, bracketed paste, copy on select, OSC 52 | T09 | T09 review | Pass |
| OSC 8 and plain links on Cmd-press, Ctrl-press off macOS; inside herdr, herdr opens them whole | T09, T45 | `term::links` tests; on macOS with real herdr 0.9.5 and a stand-in `open` on its PATH, Cmd-press on a URL herdr wraps, on its wrapped tail, on an OSC 8 link and on a URL over three rows opened the whole URL and the OSC 8 target, where before the app opened `.../ELYTermin` and nothing; Cmd-hover underlined the long URL across its rows; outside herdr the app opened the URL itself; an OSC 8 link whose label holds `file:///etc/hosts` and whose target is a web page opened the page through herdr, which keeps the target from the terminal while it takes the mouse; Shift-Cmd-press on a plain `file:///etc/hosts` inside herdr the app opened itself, caught by an `NSWorkspace` stand-in | Pass |
| Focus reporting, title, scrollback limit, find | T10 | `term::find` tests; round 3 failed at the cap, its fix stands on a red-green test | Pass |
| A scrollbar over the history: a drag on its thumb scrolls, whatever the program does with the wheel | T58 | `term::scrollbar` test: the thumb's share and place follow the history and the view's offset, and a place past either end of the track holds at it; under Xorg's dummy screen, after `seq 1 500`, the thumb stood at the bottom right, a drag to the top showed the first prompt and lines 1 to 39, and one back down lines 220 to 259; with mouse tracking on, the wheel went to the program as reports and did not scroll, while a drag on the thumb showed lines 245 to 284 | Pass |
| A bell makes no sound; the row marks it | T10, T12 | T12 review | Pass |
## 6. Main window and layout
+4
View File
@@ -280,3 +280,7 @@ Each task gets at most three codex rounds. Each round: the commit, the verdict,
## T56
- No review: the owner asked for 0.1.6 without one. Checked by scripts/check.sh (243 tests), Linux tests (250) and clippy, Windows gnu clippy, and under Xorg's dummy screen the start's check, Check now, the switch, and a build named 0.1.4 that found 0.1.5 out and opened its page.
## T58
- No review: the owner asked for it at once, in 0.1.6, without one. Checked by scripts/check.sh (244 tests), Linux tests (251) and clippy, Windows gnu clippy, and under Xorg's dummy screen drags of the thumb through 500 lines, with mouse tracking on as well.