T26: the arrow over the terminal; the settings window drops General, its Shell and Start in leading Terminal, which lists every shortcut from the keymap with a search, and its categories and page are two columns that scroll apart
CI / platforms (ubuntu-24.04) (push) Successful in 1m17s
CI / platforms (windows-latest) (push) Successful in 1m52s
CI / macos (push) Successful in 11m23s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-15 20:08:10 -04:00
1 parent 90a46257a3
commit 7bfb462ffa
9 files changed
+210 -84

No files matched your search

+3 -3
View File
@@ -62,7 +62,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-14: an input method's commit goes to the program as UTF-8 under every kitty flag, as kitty (`keys.c`, `GLFW_IME_COMMIT_TEXT`) and Ghostty (test `kitty: composed text with report all`) send it.
- 2026-10-14: what gpui hides from the kitty protocol: a key alone on a modifier sends nothing (gpui reports no key event for it), and shifted punctuation reports the character it types as its key, since gpui clears Shift for it.
- 2026-10-14: on macOS, which Option key is down and whether a key repeats come from AppKit's current event: gpui rebuilds keys AppKit routes through its commands as fresh presses. As in Ghostty, `ApplePressAndHoldEnabled` is registered off, so held keys repeat instead of opening the accent menu, and a local event monitor hands Command key-ups to the focused view, since AppKit never routes them to the window.
- 2026-10-14: mouse reports follow Ghostty's rules: X10 tracking sends presses alone without modifiers, SGR names the released button, motion inside one cell sends nothing, Shift takes the mouse back for selecting. The tap follows modes 9, 1000, 1002, 1003, 1005 and 1006 as xterm holds them: one tracking and one format at a time, the last set wins, any reset ends it, and RIS clears both. alacritty drops 9 and keeps each mode apart, so its mouse bits go unread. The tap's scanner reads controls inside a sequence as vte does: C0 runs, DEL and high bytes are skipped, CAN and SUB cancel, ESC restarts, subparameters are skipped. Motion reports the first held button: left, right, middle.
- 2026-10-14: mouse reports follow Ghostty's rules: X10 tracking sends presses alone without modifiers, SGR names the released button, motion inside one cell sends nothing, Shift takes the mouse back for selecting. The tap follows modes 9, 1000, 1002, 1003, 1005 and 1006 as xterm holds them: one tracking and one format at a time, the last set wins, any reset ends it, and RIS clears both. alacritty drops 9 and keeps each mode apart, so its mouse bits go unread. The tap's scanner reads controls inside a sequence as vte does: C0 runs, DEL and high bytes are skipped, CAN and SUB cancel, ESC restarts, subparameters are skipped. Motion reports the first held button: left, right, middle. The pointer over the grid is the arrow, as the owner asked, not Ghostty's I-beam.
- 2026-10-14: a wheel notch scrolls 3 lines, as Ghostty's discrete wheel; a trackpad scrolls its pixels. The wheel reports to a program that tracks the mouse, sends arrows on the alternate screen under mode 1007, and scrolls history otherwise.
- 2026-10-14: Cmd-C and Cmd-V copy and paste on macOS, Ctrl-Shift-C and Ctrl-Shift-V elsewhere; on Windows Ctrl-V pastes too, as in Windows Terminal, since Windows shells take it so, while on Linux it stays the program's, as in GNOME Terminal and Ghostty; Cmd-press, Ctrl-press off macOS, opens an OSC 8 target or a URL Ely's `links` finds in the wrapped line. Copies drop the newline alacritty ends a whole line with, as Ghostty's do. Bracketed pastes lose ESC and Ctrl-C, as alacritty's do; plain pastes end lines with CR. Typing and pasting clear the selection. Copy on select and OSC 52 writes to `p` or `s` go to the primary selection on Linux, as Ghostty's do; macOS and Windows have none, so they go to the clipboard.
- 2026-10-14: a key's release goes out only when the terminal saw its press, so a key a binding takes sends no release, as in kitty.
@@ -73,10 +73,10 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-14: Files shows the shown session's folder in Ely's `Tree`, not `FileTree`, which takes every path up front and drops empty folders. A folder is read off the main thread when it first opens; the root and the open folders are read again whenever the session reports a folder, so at each prompt. Git's marks come from `git status --porcelain=v2 -z -- .`, run with `--no-optional-locks`, so it never holds `index.lock` against the user's own git, and under the user's `status.showUntrackedFiles`, so a home folder kept in git stays quick. An untracked folder marks what it holds. On macOS, whose file systems ignore Unicode forms, a name keeps its form on disk and meets Git's mark in composed form, as git's `core.precomposeunicode` writes it. A label built from a path shows each control character as its picture, `␊` for a newline: gpui shapes no newline, and a file may be named with one. A name not in UTF-8 keeps its bytes: its label shows it lossily, and its tree key adds the bytes after a NUL, which no path holds, so no two names share a key. A program without shell integration gets a line in the right column until it reports a folder.
- 2026-10-14: git runs in English with `--literal-pathspecs` and without optional locks. Paths stay git's bytes until shown, so a name not in UTF-8 still names its own file; off Unix git writes UTF-8. Status reads porcelain v2. Paths to stage and unstage go in on stdin, NUL-ended (`--pathspec-from-file`), so any name and any count pass; an empty list is refused, since git would take it as the whole tree. Unstaging is `git reset`, which works before the first commit, and a staged rename unstages with its old path; a copy is a new file, its source no part of it. A diff is git's own, without external tools or text converters, a conflict against HEAD, our side, so a file we deleted reads as new; it is read into Ely's stretches; a binary file has none, and an untracked folder none either. Without an index git exits 1 for a difference and a failure alike; only a difference prints. What git warns of while it works goes to the log. The log needs a commit at HEAD: git refuses it before the first.
- 2026-10-14: the Git panel shows HEAD's branch, or its short commit while detached; Ely's `ChangesList` of conflicted, staged, unstaged and untracked files; Ely's `CommitInput`; Ely's `CommitList` of the last 50 commits. A row's press opens its diff in Ely's fullscreen `Dialog` under the title bar, in Ely's `DiffViewer`, unified or split; a binary file, an untracked folder and a change of no line say so instead. Every control Ely draws works: stage, unstage, stage all, unstage all, amend, and discard, which asks first in Ely's `ConfirmDialog`: an edit goes back to the index (`git restore`); a new file, untracked or only intended to be added, is deleted (`git clean`, `git rm -f`). Git's clean passes over a repository of its own without a word, so a clean that removed nothing is an error. Actions run off the main thread, each after the one asked before it; a failure shows in Ely's `Alert` with git's words until dismissed or the next action, and after any action Files reads its marks again. A folder change drops the open diff, and one still loading, and blanks the view unless the new folder lies inside the shown work tree, so a cd within it does not blink; no action runs until the shown folder has read, so none reaches the repository left behind. A commit empties only the fields that still hold what it committed, and only once the shown folder has read and is in the same repository: a draft typed while it ran, or while a folder read, stays. A path shows on one line and once: as it is, or quoted as git quotes a name with control characters, quotes, backslashes or bytes not in UTF-8; a folder drops git's trailing slash, since no file shares its name. A section lists 500 files at most, and a line says how many are listed: Ely's list draws every row, and gpui copies every row's hitboxes and paint each frame, so 3000 rows halved the terminal's frames. The Git body is a cached view, so a terminal's output does not draw it again; it comes back from the Files tab with its lists scrolled to the top, since a view cached while hidden drops its widgets' state. Files stays uncached, so its tree keeps its open folders: Ely's `Tree` draws only the rows in view.
- 2026-10-14: Cmd-, opens the settings window, or brings the open one forward, and Cmd-W closes it; off macOS Ctrl-, and Ctrl-Shift-W, as Ghostty's. Categories sit in Ely's `SettingsLayout`, rows in its `SettingsRow`; a category joins the list with its rows. Each change goes through `SettingsStore::update`, which checks and saves, and every view reads the global, so it applies at once. A shell saves only when it names a program the running user may run, as the system's `access` answers, ACLs too: empty for the login shell, a file at a full path, or a name found in one of `PATH`'s full folders; a relative folder counts for none, since it would hang on each session's own folder. On Windows a name without an extension means its `.exe`, as `CreateProcess` reads it; a name with one keeps it. A program whose path is not UTF-8 counts for none: a session starts its program by text. A session runs that same full path, so the check and the start agree; else the row says why and the saved one stays, since a bad shell stops the next start. The window takes focus inside when it opens, so its keys find its context. The app quits with its last terminal window: settings alone hold no terminal.
- 2026-10-14: Cmd-, opens the settings window, or brings the open one forward, and Cmd-W closes it; off macOS Ctrl-, and Ctrl-Shift-W, as Ghostty's. Categories sit in the first of two columns of Ely's `ColumnShell`, an Ely `SelectableList` as `SettingsLayout` draws its own, and the open one's rows, Ely's `SettingsRow`, in the second; each column scrolls on its own, which `SettingsLayout`, one box for both, cannot. A category joins the list with its rows. Each change goes through `SettingsStore::update`, which checks and saves, and every view reads the global, so it applies at once. A shell saves only when it names a program the running user may run, as the system's `access` answers, ACLs too: empty for the login shell, a file at a full path, or a name found in one of `PATH`'s full folders; a relative folder counts for none, since it would hang on each session's own folder. On Windows a name without an extension means its `.exe`, as `CreateProcess` reads it; a name with one keeps it. A program whose path is not UTF-8 counts for none: a session starts its program by text. A session runs that same full path, so the check and the start agree; else the row says why and the saved one stays, since a bad shell stops the next start. The window takes focus inside when it opens, so its keys find its context. The app quits with its last terminal window: settings alone hold no terminal.
- 2026-10-14: settings refuse unknown fields; a bad settings file stops the start and is never overwritten.
- 2026-10-14: Appearance shows every built-in theme as Ely's `RadioCard` in a `SimpleGrid`, each over a small window in the theme's colors. Separate dark theme copies the theme into `dark_theme`, so turning it on changes nothing until a dark card is picked. A theme change re-dresses the chrome at once. Font family comes from Ely's `FontPicker`; size, line height and padding from its `NumberInput`, which saves a number on Enter, on leaving the field and on each step. A font saves, and a settings file starts, only when gpui loads it as the terminal draws it: gpui falls back to another font without a word and loads none without an `m`, so the terminal draws with no font but its own. The cursor preview is a prompt in the terminal's font and theme, its cursor drawn as the terminal draws it, blinking on the terminal's beat. Opacity moves by whole percents, so the saved fraction carries no float noise. A change the store refuses leaves the setting as it was, and the page says why at its top; a number field drops its draft, and the font picker shows the saved family again. The settings window opens at 900 by 640, room for three theme columns.
- 2026-10-14: Terminal shows scrollback lines, Option as Alt, on macOS only, since elsewhere Alt is Alt, and copy on select. The engine reads each when it needs it, so a change applies at once to every session; a smaller scrollback drops the history past it.
- 2026-10-14: Terminal shows the shell and where sessions start, which a General page held until the owner dropped it, then scrollback lines, Option as Alt, on macOS only, since elsewhere Alt is Alt, and copy on select, then every shortcut in Ely's `KeyboardShortcutsList`, read from the keymap: the bindings of each listed command that apply in the main window's terminal, matched at any depth of its context stack as gpui matches them, so the list cannot drift from the keys. The engine reads each when it needs it, so a change applies at once to every session; a smaller scrollback drops the history past it.
- 2026-10-14: the main window lets the desktop through while Background opacity is under 1, blurred or clear as Background blur says, through gpui's `Blurred` (macOS's NSVisualEffectView, Windows' acrylic) or `Transparent`; at 1 it is `Opaque`. The workspace sets it as the settings change. Ely's `ColumnShell::glass`, added to Ely for this, grounds the columns in the palette's glass, which the opacity sets, so the title bar and every column let the desktop through alike; the palette's `bg` stays solid, so a dialog, the Git diff among them, hides what lies under it. The terminal paints no ground over the glass, or the two would stack; a program's own background (OSC 11) paints over it. Ely's own themes pass their palette too, Ely's colors with the glass set. The settings window stays opaque.
- 2026-10-15: on Wayland the window asks for `ext_background_effect_v1` beside gpui's `org_kde_kwin_blur`. A guest connection on gpui's own display binds it, takes the effect for the window's surface once, and blurs the window's pane: the surface less Ely's resize margin on each free edge of a client-drawn window, worked out each frame and sent only when it changes; it goes and comes back with Blurred and lands at the surface's next commit, which every frame makes. The guest connection reads the display only while the window opens, before gpui's loop runs, and is never dropped: a drop destroys its proxies through the display, which gpui may close first. The log names what the compositor offers, so which way blur goes: `ext_background_effect_v1`, gpui's `org_kde_kwin_blur`, or none, the desktop clear. Its test runs a `wayland-server` compositor on a socket pair.
- 2026-10-15: on a client-drawn window, so on Linux, Ely's `ResizeBorder` paints its pane solid and shadowed under the content, which kept the desktop from showing at all. `ResizeBorder::glass`, added to Ely for this, leaves the pane to the content while the window is see-through: no ground, nor the shadow gpui draws under the box, which would tint it. So the blur covers the pane alone and the resize margin stays clear. On X11 the window names its pane in `_KDE_NET_WM_BLUR_BEHIND_REGION` (CARDINAL, 32: x, y, width, height in the window's pixels), as Ghostty names its own, and drops the property when Blurred goes; KWin reads it, other compositors pass it by. It goes over a connection of ELYTerminal's own to `$DISPLAY`, which gpui opens too: a property belongs to the window, whoever sets it. gpui's own `org_kde_kwin_blur` on KDE's Wayland still covers the whole surface, margin included, beyond ELYTerminal's reach.
+5 -4
View File
@@ -60,6 +60,7 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
- Ely's `ColumnShell`: sessions column, terminal column, right column. The window opens with blur when the setting is on.
- The sessions column and the right column toggle, each by a button at its end of the title bar or by key, and keep their state in settings, so the window opens as it was left.
- The title shows the active session's title, and no other text.
- The pointer over the terminal is the arrow.
- The title bar is transparent, as Ghostty's: the window's background, opacity and blur run under it, so it wears the terminal's color.
- Every control in the title bar takes its own press on every platform, the window buttons Ely draws on Windows and Linux too.
@@ -88,11 +89,8 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
## 10. Settings window
Cmd-, opens it; on Windows and Linux, which have no menu bar, so does a gear in the title bar. Categories on the left, rows on the right, Ely's settings components. Changes apply at once to every session and save.
Cmd-, opens it; on Windows and Linux, which have no menu bar, so does a gear in the title bar. Categories on the left, rows on the right, each side scrolling on its own, Ely's components. Changes apply at once to every session and save.
- General
- Shell: a path; empty runs the login shell.
- Start in: Home, or the active session's folder.
- Appearance
- Theme: a grid of previews.
- Separate dark theme: a switch; when on, a second grid for dark mode.
@@ -101,9 +99,12 @@ Cmd-, opens it; on Windows and Linux, which have no menu bar, so does a gear in
- Background opacity: a slider. Background blur: a switch.
- Padding: horizontal and vertical.
- Terminal
- Shell: a path; empty runs the login shell.
- Start in: Home, or the active session's folder.
- Scrollback lines.
- Option as Alt: off, left, right, both (macOS only).
- Copy on select.
- Shortcuts: every shortcut, as this platform's keys, with a search.
## 11. Background blur
+2
View File
@@ -52,6 +52,8 @@ The app writes its log to stderr. `RUST_LOG=elyterminal=debug` writes more; `tra
## Keys
Settings › Terminal lists them all, as this platform binds them.
| | macOS | Windows and Linux |
|---|---|---|
| New session | Cmd-T | Ctrl-Shift-T |
+2 -2
View File
@@ -69,6 +69,6 @@ Progress truth. Top to bottom, one at a time. A task is ticked only after its co
- [x] T25 Windows zip with the icon in the executable, Linux tarball with the icon and a desktop entry, the app id on Linux windows, their scripts; release v0.1.0 on Gitea with the three builds.
## 16. Fixes from the first Windows run
## 16. Fixes from the owner's first run
- [ ] T26 On Windows every title bar control takes its press, Ely's window buttons too: in Ely, window buttons and header items occlude the drag region; the Git and Files switch moves below the title bar, centered; on Windows and Linux a gear in the title bar opens Settings; on Windows Ctrl-V pastes, as in Windows Terminal; release v0.1.1 with the three builds.
- [ ] T26 On Windows every title bar control takes its press, Ely's window buttons too: in Ely, window buttons and header items occlude the drag region; the Git and Files switch moves below the title bar, centered; on Windows and Linux a gear in the title bar opens Settings; on Windows Ctrl-V pastes, as in Windows Terminal; the pointer over the terminal is the arrow; the settings window drops General, whose Shell and Start in lead Terminal, which lists every shortcut, and its categories and page scroll apart; release v0.1.1 with the three builds.
+58 -62
View File
@@ -3,11 +3,11 @@ use std::fmt::Debug;
use ely_gpui_component::{
buttons::SegmentedControl,
feedback::InlineMessage,
forms::{InputEvent, PathInput, Switch, TextInput},
primitives::{FocusScope, IconName, Severity},
settings::{SettingsLayout, SettingsRow, SettingsSection},
forms::{InputEvent, Switch, TextInput},
lists::{ListItem, SelectableList},
primitives::{FocusScope, Icon, IconName, Severity},
shell::{ColumnShell, ResizeBorder, ShellColumn},
theme::{ActiveTheme, ControlSize},
theme::{ActiveTheme, ControlSize, IconSize},
};
use gpui::{
App, AppContext, Bounds, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
@@ -16,7 +16,7 @@ use gpui::{
actions, div, prelude::FluentBuilder, px, size,
};
use super::{Settings, SettingsStore, StartIn};
use super::{Settings, SettingsStore};
use crate::{app::APP_ID, menus, term::BLINK};
mod appearance;
@@ -28,8 +28,7 @@ actions!(elyterminal, [OpenSettings, CloseSettings]);
const CONTEXT: &str = "Settings";
/// Each category's key, name and icon.
const SECTIONS: [(&str, &str, IconName); 3] = [
("general", "General", IconName::Settings),
const SECTIONS: [(&str, &str, IconName); 2] = [
("appearance", "Appearance", IconName::Palette),
("terminal", "Terminal", IconName::Terminal),
];
@@ -83,11 +82,13 @@ fn open_window(cx: &mut App) {
}
}
/// The settings window: categories down the side, the open one's rows beside them.
/// The settings window: categories in one column, the open one's rows in the next, each scrolling on its own.
pub struct SettingsWindow {
focus: FocusHandle,
section: SharedString,
shell: Entity<TextInput>,
/// The shortcut list's search.
search: Entity<TextInput>,
/// Why the typed shell is not saved.
refused: Option<SharedString>,
/// Why the last change did not save.
@@ -110,6 +111,7 @@ impl SettingsWindow {
input
});
let font = cx.new(|cx| TextInput::new(window, cx));
let search = cx.new(|cx| TextInput::new(window, cx).placeholder("Search shortcuts"));
let typed = cx.subscribe(&shell, |this, input, event: &InputEvent, cx| {
if let InputEvent::Changed = event {
let shell = input.read(cx).text().to_string();
@@ -123,6 +125,7 @@ impl SettingsWindow {
focus,
section: SECTIONS[0].0.into(),
shell,
search,
refused: None,
failed: None,
drafts: 0,
@@ -167,46 +170,36 @@ impl SettingsWindow {
};
cx.notify();
}
fn general(&self, cx: &Context<Self>) -> SettingsSection {
let start = match SettingsStore::get(cx).start_in {
StartIn::Home => "home",
StartIn::Session => "session",
};
let shell = div()
.w(cx.theme().prose_width())
.flex()
.flex_col()
.gap_1()
.child(PathInput::new("settings-shell", &self.shell))
.children((self.refused.clone()).map(|why| InlineMessage::new(Severity::Danger, why)));
let start = SegmentedControl::new("settings-start-in", start)
.segment("home", "Home", None)
.segment("session", "Session's folder", None)
.size(ControlSize::Sm)
.on_change({
let this = cx.entity();
move |value, _, cx| start_in(&this, value, cx)
});
SettingsSection::new("General")
.row(SettingsRow::new("Shell").control(shell))
.row(SettingsRow::new("Start in").control(start))
}
}
/// Where new sessions start, as the settings keep it.
fn start_in(this: &Entity<SettingsWindow>, value: &SharedString, cx: &mut App) {
let start = match value.as_ref() {
"home" => StartIn::Home,
"session" => StartIn::Session,
other => unreachable!("settings: no start {other}"),
};
set(
this,
format!("new sessions start in {start:?}"),
cx,
|settings| settings.start_in = start,
/// The categories; a press, Up and Down, or Enter opens one.
fn sections(open: &SharedString, cx: &Context<SettingsWindow>) -> SelectableList {
let muted = cx.theme().colors.fg_muted;
let list = SECTIONS.iter().fold(
SelectableList::new("settings-section-list").selected([open.clone()]),
|list, (key, name, icon)| {
let item = ListItem::new(SharedString::from(format!("settings-section-{key}")), *name)
.leading(Icon::new(*icon).size(IconSize::Sm).color(muted));
list.row(*key, item)
},
);
let show = {
let this = cx.entity();
move |key: &SharedString, cx: &mut App| {
log::info!("settings: open {key}");
this.update(cx, |this, cx| {
this.section = key.clone();
cx.notify();
});
}
};
let enter = show.clone();
list.on_change(move |keys, _, cx| {
if let Some(key) = keys.first() {
show(key, cx);
}
})
.on_activate(move |key, _, cx| enter(key, cx))
}
/// A switch that saves through `put`.
@@ -289,9 +282,7 @@ fn set(
impl Render for SettingsWindow {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let pick = cx.entity();
let page = match self.section.as_ref() {
"general" => self.general(cx).into_any_element(),
"appearance" => self.appearance(window, cx).into_any_element(),
"terminal" => self.terminal(cx).into_any_element(),
other => unreachable!("settings: no section {other}"),
@@ -302,26 +293,31 @@ impl Render for SettingsWindow {
.gap_4()
.children((self.failed.clone()).map(|why| InlineMessage::new(Severity::Danger, why)))
.child(page);
let layout = SettingsLayout::new("settings", SECTIONS, self.section.clone(), {
move |key, _, cx| {
pick.update(cx, |this, cx| {
this.section = key.clone();
cx.notify();
})
}
})
.page(page);
let (_, name, _) = SECTIONS
.iter()
.find(|(key, ..)| *key == self.section.as_ref())
.expect("the open section is listed");
let list = div()
.id("settings-list")
.size_full()
.overflow_y_scroll()
.p_2()
.child(sections(&self.section, cx));
let body = div()
.id("settings-body")
.size_full()
.overflow_y_scroll()
.p_6()
.child(layout);
let shell = ColumnShell::new("settings").column(
ShellColumn::new("settings-column")
.title("Settings")
.child(body),
);
.child(page);
// The categories and the page scroll apart.
let shell = ColumnShell::new("settings")
.column(
ShellColumn::new("settings-sections")
.width(cx.theme().label_width() * 1.25)
.title("Settings")
.child(list),
)
.column(ShellColumn::new("settings-page").title(*name).child(body));
// The context sits above the focus root, so its keys work wherever focus is.
div()
.size_full()
+135 -9
View File
@@ -1,12 +1,25 @@
use ely_gpui_component::{
forms::NumberInput,
settings::{SettingsRow, SettingsSection},
theme::ControlSize,
buttons::SegmentedControl,
feedback::InlineMessage,
forms::{Copy, NumberInput, Paste, PathInput, SelectAll},
primitives::Severity,
settings::{KeyboardShortcutsList, SettingsRow, SettingsSection, Shortcut},
theme::{ActiveTheme, ControlSize},
};
use gpui::{
Action, App, Context, Div, Entity, KeyContext, ParentElement, SharedString, Styled, div,
};
use gpui::Context;
use super::{SettingsWindow, choice, set, switch};
use crate::settings::{OptionAsAlt, SCROLLBACK, SettingsStore};
use super::{OpenSettings, SettingsWindow, choice, set, switch};
use crate::{
menus::{Bigger, Minimize, Quit, ResetSize, Smaller},
settings::{OptionAsAlt, SCROLLBACK, SettingsStore, StartIn},
term::input::{self, Find},
workspace::{
self, CloseSession, LastSession, NewSession, NextSession, PreviousSession, SelectSession,
TogglePanel, ToggleSessions,
},
};
/// Each way Option types Alt, its key and its name.
const OPTIONS: [(OptionAsAlt, &str, &str); 4] = [
@@ -16,11 +29,42 @@ const OPTIONS: [(OptionAsAlt, &str, &str); 4] = [
(OptionAsAlt::Both, "both", "Both"),
];
/// The names of the sessions a digit shows.
const PLACES: [&str; 8] = [
"Session 1",
"Session 2",
"Session 3",
"Session 4",
"Session 5",
"Session 6",
"Session 7",
"Session 8",
];
impl SettingsWindow {
/// Scrollback, Option as Alt on macOS, and copy on select.
pub(super) fn terminal(&self, cx: &Context<Self>) -> SettingsSection {
/// Shell, start in, scrollback, Option as Alt on macOS and copy on select, then the shortcuts.
pub(super) fn terminal(&self, cx: &Context<Self>) -> Div {
let settings = SettingsStore::get(cx);
let this = cx.entity();
let shell = div()
.w(cx.theme().prose_width())
.flex()
.flex_col()
.gap_1()
.child(PathInput::new("settings-shell", &self.shell))
.children((self.refused.clone()).map(|why| InlineMessage::new(Severity::Danger, why)));
let start = match settings.start_in {
StartIn::Home => "home",
StartIn::Session => "session",
};
let start = SegmentedControl::new("settings-start-in", start)
.segment("home", "Home", None)
.segment("session", "Session's folder", None)
.size(ControlSize::Sm)
.on_change({
let this = this.clone();
move |value, _, cx| start_in(&this, value, cx)
});
let lines = NumberInput::new(("scrollback", self.drafts), f64::from(settings.scrollback))
.range(f64::from(*SCROLLBACK.start()), f64::from(*SCROLLBACK.end()))
.step(1000.)
@@ -35,6 +79,8 @@ impl SettingsWindow {
}
});
let mut section = SettingsSection::new("Terminal")
.row(SettingsRow::new("Shell").control(shell))
.row(SettingsRow::new("Start in").control(start))
.row(SettingsRow::new("Scrollback lines").control(lines));
// Off macOS, Alt is always Alt.
if cfg!(target_os = "macos") {
@@ -53,6 +99,86 @@ impl SettingsWindow {
settings.copy_on_select,
|settings, on| settings.copy_on_select = on,
);
section.row(SettingsRow::new("Copy on select").control(copy))
let section = section.row(SettingsRow::new("Copy on select").control(copy));
let keys = SettingsSection::new("Shortcuts")
.row(KeyboardShortcutsList::new(shortcuts(cx), &self.search));
div().flex().flex_col().gap_6().child(section).child(keys)
}
}
/// Where new sessions start, as the settings keep it.
fn start_in(this: &Entity<SettingsWindow>, value: &SharedString, cx: &mut App) {
let start = match value.as_ref() {
"home" => StartIn::Home,
"session" => StartIn::Session,
other => unreachable!("settings: no start {other}"),
};
set(
this,
format!("new sessions start in {start:?}"),
cx,
|settings| settings.start_in = start,
);
}
/// The listed commands, in order: group, name and action.
fn listed() -> Vec<(&'static str, &'static str, Box<dyn Action>)> {
let mut listed = vec![
("Sessions", "New session", NewSession.boxed_clone()),
("Sessions", "Close session", CloseSession.boxed_clone()),
("Sessions", "Next session", NextSession.boxed_clone()),
(
"Sessions",
"Previous session",
PreviousSession.boxed_clone(),
),
];
let places = PLACES.iter().enumerate();
listed.extend(places.map(|(ix, name)| ("Sessions", *name, SelectSession(ix).boxed_clone())));
listed.extend([
("Sessions", "Last session", LastSession.boxed_clone()),
("Edit", "Copy", Copy.boxed_clone()),
("Edit", "Paste", Paste.boxed_clone()),
("Edit", "Select all", SelectAll.boxed_clone()),
("Edit", "Find", Find.boxed_clone()),
("View", "Toggle sessions", ToggleSessions.boxed_clone()),
("View", "Toggle right panel", TogglePanel.boxed_clone()),
("View", "Bigger", Bigger.boxed_clone()),
("View", "Smaller", Smaller.boxed_clone()),
("View", "Reset size", ResetSize.boxed_clone()),
("App", "Settings", OpenSettings.boxed_clone()),
("App", "Minimize", Minimize.boxed_clone()),
("App", "Quit", Quit.boxed_clone()),
]);
listed
}
/// Each listed command's keys in the main window's terminal, a line per binding, from the keymap.
fn shortcuts(cx: &App) -> Vec<Shortcut> {
let stack = [workspace::CONTEXT, input::CONTEXT]
.map(|name| KeyContext::parse(name).expect("a context name parses"));
let keymap = cx.key_bindings();
let keymap = keymap.borrow();
let mut shortcuts = Vec::new();
for (group, name, action) in listed() {
for binding in keymap.bindings_for_action(action.as_ref()) {
// gpui matches a binding at any depth of the context stack.
let applies = binding.predicate().is_none_or(|predicate| {
(1..=stack.len()).any(|depth| predicate.eval(&stack[..depth]))
});
if applies {
let strokes: Vec<String> = binding
.keystrokes()
.iter()
.map(|key| key.unparse())
.collect();
shortcuts.push(Shortcut {
group: group.into(),
name: name.into(),
keys: strokes.join(" ").into(),
});
}
}
}
shortcuts
}
-1
View File
@@ -360,7 +360,6 @@ impl Render for TerminalView {
.on_any_mouse_down(cx.listener(Self::mouse_down))
.on_mouse_move(cx.listener(Self::mouse_move))
.on_scroll_wheel(cx.listener(Self::wheel))
.cursor_text()
.size_full()
.px(px(settings.padding_x))
.py(px(settings.padding_y))
+1 -1
View File
@@ -45,7 +45,7 @@ actions!(
pub struct SelectSession(pub usize);
/// The key context of the main window's content.
const CONTEXT: &str = "Workspace";
pub const CONTEXT: &str = "Workspace";
/// The window's title while the program names none.
pub const TITLE: &str = "ELYTerminal";
+4 -2
View File
@@ -69,6 +69,7 @@ What no check reached: no one has looked at the window on Windows, which CI buil
| `ColumnShell` with three columns; blur when the setting is on | T11, T19 | `shot` names a `BlurredView` under the window | Pass |
| Columns toggle and keep their state | T11, T23 | a click on each title bar button hid its column and saved it, and a restart opened the window with both hidden (147 columns against 87); Cmd-B with the app active; Ctrl-Shift-B on a real X11 window | Pass |
| The title shows the session's title, nothing else | T11 | log: "the title is", per session shown | Pass |
| The pointer over the terminal is the arrow | T26 | the terminal sets no pointer, so gpui's arrow shows; on X11 with the pointer over the grid, gpui asked for no I-beam ("text"), which it had asked for, and failed to load under Xvfb, before | Pass |
| The title bar is transparent | T11, T19 | T19 review | Pass |
| Every title bar control takes its press, Ely's window buttons on Windows and Linux too | T26 | Ely `cdc5ce8`: `window_controls_occlude_what_lies_under` (each style; before, the box under Windows' caption buttons took the press), `a_wobbling_press_on_a_slot_moves_no_window` (before, the window moved), `a_wobbling_press_on_a_column_title_moves_the_window`; on the owner's PC, window messages asked the app for Windows' hit test along the title bar: v0.1.0 answered caption from end to end, T26's build answers client over the toggles and +, caption over the titles, min, max and close over Ely's buttons, and a press on close closed the app; the owner has not yet clicked it | Partial |
@@ -105,11 +106,12 @@ What no check reached: no one has looked at the window on Windows, which CI buil
| PRD line | Task | Evidence | Status |
|---|---|---|---|
| Cmd-, opens it; categories left, rows right; changes apply at once and save | T16 | T16 review | Pass |
| Cmd-, opens it; categories left, rows right, each scrolling on its own; changes apply at once and save | T16, T26 | T16 review; on X11, eight wheel notches over the Terminal page carried it down to its Edit and View shortcuts while the category column and both headers stayed | Pass |
| On Windows and Linux, a gear in the title bar opens it too | T26 | on X11 under Xvfb with xfwm4, xdotool's click on the gear, just inside the right toggle, opened the 900×640 Settings window ("settings: the window opens"), after a click on + opened a session; sent through `App::dispatch_action`, the same click had opened nothing; the owner has not yet clicked it on Windows | Partial |
| General: Shell, Start in | T16 | `settings::tests` | Pass |
| Terminal: Shell, Start in | T16, T26 | `settings::tests`; the General page is gone, and a capture on X11 shows its two rows leading Terminal | Pass |
| Appearance: theme grids, dark theme, font, size, line height, ligatures, cursor and preview, opacity, blur, padding | T17 | T17 review | Pass |
| Terminal: scrollback, Option as Alt (macOS), copy on select | T18 | T18 review | Pass |
| Terminal: every shortcut, with a search | T26 | read from the keymap, the bindings that apply in the main window's terminal; on X11 the list showed this platform's keys (Ctrl+Shift+T, Alt+1 and on), and typing "paste" left Edit's Paste, Ctrl+Shift+V | Pass |
## 11. Background blur