T63: a double press on a session's row renames it, which a user asked for after 0.1.6: Ely's ListItem::editor, added for this, draws a field in the title's place; Enter keeps the name, Escape drops it, a blur keeps it; the name stands over the program's title in the row, the header and the window's title until renamed again, and an empty one gives the title back; Ely pinned to 7f7a715

This commit is contained in:
ZacharyZhang-NY committed 2026-10-19 19:19:09 -04:00
1 parent 76c48dce2e
commit 3085a940ad
9 files changed
+136 -17

No files matched your search

+2 -1
View File
@@ -34,7 +34,7 @@ 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/workspace/events.rs`: what a session's terminal reports: title, folder, bell, the state its row shows, the program's exit; and a press on a notification a session posted. `src/sessions.rs`: a session and its row, with the mark of its status and what hover shows of it; `sessions/orb` draws the mark, 3 by 3 dots on one clock. `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/updates.rs`: checks for a newer release through `curl` and Gitea's API, at start and once a day while the settings ask, and when Settings › Terminal asks.
- `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/rename.rs`: a session renamed in place. `src/workspace/events.rs`: what a session's terminal reports: title, folder, bell, the state its row shows, the program's exit; and a press on a notification a session posted. `src/sessions.rs`: a session and its row, with the mark of its status and what hover shows of it; `sessions/orb` draws the mark, 3 by 3 dots on one clock. `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/updates.rs`: checks for a newer release through `curl` and Gitea's API, at start and once a day while the settings ask, and when Settings › Terminal asks.
- `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.
@@ -120,3 +120,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
- 2026-10-19: fallback fonts, which a user asked for after 0.1.6, since the Chinese and Latin fonts people like seldom come as one: `font_fallbacks` in the settings lists installed families, tried in order for a character Font family lacks, through gpui's `Font::fallbacks`, which CoreText's cascade list, DirectWrite's fallback builder and cosmic-text's chain each honor; the system's own fallback follows the last. On Linux gpui's cosmic-text path keeps the fallback the last character took while it covers the next, so shaping clusters stay whole: with two fallbacks, a character only the second covers carries the next one, which the first covers too, into the second; macOS and Windows choose per character. The owner chose to leave that so. Each family must load as the terminal draws it, as Font family must, so a family gpui would pass over without a word stops the start or the save, the error naming `font_fallbacks`; an empty name is refused. The field is new beside `font_family`, so a settings file from before loads as it was. Settings › Appearance lists them under Family: each an Ely `FontPicker` whose pick replaces it and a button that removes it, then Add fallback, which opens an empty picker whose pick adds one, its button closing it; a refused pick shows the saved family again, as Family's does. Each row keeps an input of its own, and a removal drops the removed row's input and takes the focus away, since Ely's combobox keeps a focused input's text, and an input kept by place would show, and on Enter save, the family removed. Each picker's id is its input's entity, not its place: Ely's combobox keeps its state by id and listens to the input it first met, so a picker keyed by place, after a removal moved the inputs, wrote another row's family into its new input at the next blur. Ligatures and the bundled Nerd Font for private-use icons stand as before; the cursor preview keeps the family alone.
- 2026-10-19: a user found the IME's candidate box jumping on Windows after 0.1.6. gpui places it, through `ImmSetCompositionWindow` and `ImmSetCandidateWindow`, only when a composition starts, at the caret the terminal reports, the grid cursor's cell from the last frame, hidden or not. On the owner's PC, WeType under powershell.exe and under a status line redrawn every 100 ms put the box at the caret in 23 compositions out of 23, so the owner closed it unreproduced. The terminal logs each caret the input method asks for, at debug, and each move of the caret, at trace, so the next report carries where the box was told to go.
- 2026-10-19: Ely's shirt and back hair run on below the edge she holds, which a user asked for after 0.1.6, where a hop lifted her whole figure off the window and showed a gap under her collar and hands: both continue down to 340 in the icon's units, 54 below the edge at 286, past her highest lift, a hop of 31 with a press's 0.6 of one on it, so a hop shows more of her and never the gap; at rest the part below the edge lies outside her window and does not show. The shirt moves with her body, the hair with her head, as before; the hair behind her hands fills under them as she lifts.
- 2026-10-19: a double press on a session's row renames it, which a user asked for after 0.1.6. Ely's `ListItem::editor`, added to Ely for this, draws Ely's text field in the title's place; the row's first press selects the session, as ever, and its second opens the field, holding what the session is called, selected, with the focus. Enter keeps what it holds and gives the focus back to the shown terminal, Escape drops it the same way, and a blur keeps it, as Windows Terminal's tab rename does. A press inside the open field stays the field's: the row would select its session, which takes the focus to the terminal and ends the rename. The name, its ends trimmed, stands over the program's title in the row, the terminal's header and the window's title until renamed again, a program's later titles passing under it; an empty one gives the title back to the program. It lives as long as the session, and notifications keep the program's title. Each program title, each rename and its end are logged.
Generated
+1 -1
View File
@@ -1701,7 +1701,7 @@ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
[[package]]
name = "ely-gpui-component"
version = "0.1.1"
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git?rev=da3fdcaf39f2fa85ce1dd9cad07c3c81cf1dd721#da3fdcaf39f2fa85ce1dd9cad07c3c81cf1dd721"
source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git?rev=7f7a715d797d8450eed4f17ae8002b5427d01d28#7f7a715d797d8450eed4f17ae8002b5427d01d28"
dependencies = [
"alacritty_terminal",
"anyhow",
+1 -1
View File
@@ -13,7 +13,7 @@ alacritty_terminal = { version = "=0.26.0", default-features = false }
anyhow = "1"
base64 = "0.22"
dirs = "7"
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git", rev = "da3fdcaf39f2fa85ce1dd9cad07c3c81cf1dd721" }
ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git", rev = "7f7a715d797d8450eed4f17ae8002b5427d01d28" }
env_logger = "0.11"
futures = "0.3"
gethostname = "1.1.0"
+1
View File
@@ -81,6 +81,7 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
- A list of sessions. Each row: title, then the working directory, home as `~`.
- New session (+ and Cmd-T), close (row's ×, Cmd-W), select (press, Cmd-1…9, Ctrl-Tab and Ctrl-Shift-Tab).
- Rename: a double press on a row's title opens a field in its place; Enter keeps the name, Escape drops it. The name shows in the row, the terminal's header and the window's title over the program's own until renamed again; an empty name gives the title back to the program.
- A session whose program ends closes its row. The last row closing closes the window.
- A bell in a hidden session marks its row.
- A row shows its session's status: working, with its progress where known; waiting on the user, for a permission, a question or a login; done; error; and a mark when the last command failed. Hover shows each record's program, title and message. A hidden session that starts waiting marks its row until it is shown.
+44 -7
View File
@@ -5,6 +5,7 @@ use std::{
use ely_gpui_component::{
buttons::{ButtonVariant, IconButton},
forms::{Input, TextInput},
lists::ListItem,
primitives::{Icon, IconName, Tooltip},
theme::{ActiveTheme, ControlSize, IconSize},
@@ -25,6 +26,10 @@ pub struct Session {
pub terminal: Entity<TerminalView>,
/// The program's title; empty while it names none.
pub title: String,
/// The name the user gave it; empty leaves the title to its program.
pub name: String,
/// The field its row shows while the user renames it.
pub editing: Option<(Entity<TextInput>, Subscription)>,
/// The folder it reports, else the one it started in.
pub folder: PathBuf,
/// Its shell has integration, or it reported a folder anyway.
@@ -39,6 +44,14 @@ pub struct Session {
}
impl Session {
/// What it is called: the user's name, else its program's title, else the app's name.
pub fn named(&self) -> &str {
[self.name.as_str(), self.title.as_str()]
.into_iter()
.find(|title| !title.is_empty())
.unwrap_or(TITLE)
}
/// The look its mark takes from its terminal now.
pub fn looks(&self, cx: &App) -> Option<Look> {
let view = self.terminal.read(cx);
@@ -103,17 +116,20 @@ pub fn row(
) -> ListItem {
let terminal = session.terminal.read(cx);
let pid = terminal.pid() as usize;
let title = if session.title.is_empty() {
TITLE
} else {
&session.title
};
let (select, close) = (workspace.clone(), workspace.clone());
let item = ListItem::new(("session", pid), title.to_string())
let editing = session.editing.is_some();
let item = ListItem::new(("session", pid), session.named().to_string())
.description(shown(&session.folder, home))
.selected(active)
.strong(session.bell || session.waiting)
.on_click(move |_, window, cx| select.update(cx, |this, cx| this.select(index, window, cx)))
// A press in the field it renames with stays the field's.
.on_click(
move |event, window, cx| match (editing, event.click_count()) {
(true, _) => {}
(false, 2..) => select.update(cx, |this, cx| this.rename(index, window, cx)),
(false, _) => select.update(cx, |this, cx| this.select(index, window, cx)),
},
)
.trailing(
IconButton::new(("close-session", pid), IconName::X)
.variant(ButtonVariant::Ghost)
@@ -124,6 +140,10 @@ pub fn row(
close.update(cx, |this, cx| this.close(index, window, cx))
}),
);
let item = match &session.editing {
Some((input, _)) => item.editor(editor(workspace, session, input)),
None => item,
};
let mark = mark(session, now, cx);
let bell = session
.bell
@@ -141,6 +161,23 @@ pub fn row(
)
}
/// The field a session's row renames it with; Escape drops what it holds.
fn editor(
workspace: &Entity<Workspace>,
session: &Session,
input: &Entity<TextInput>,
) -> impl IntoElement {
let (workspace, id) = (workspace.clone(), session.terminal.entity_id());
div()
.on_key_down(move |event, window, cx| {
if event.keystroke.key == "escape" {
cx.stop_propagation();
workspace.update(cx, |this, cx| this.end_rename(id, false, true, window, cx));
}
})
.child(Input::new(input).size(ControlSize::Sm))
}
/// What a mark shows: the most pressing record's state, else a failed last command.
pub fn look(record: Option<&Record>, failed: bool) -> Option<Look> {
Some(
+5 -6
View File
@@ -29,6 +29,7 @@ use crate::{
};
mod events;
mod rename;
mod widths;
use widths::{Dragged, Side};
@@ -156,6 +157,8 @@ impl Workspace {
self.sessions.push(Session {
terminal,
title: String::new(),
name: String::new(),
editing: None,
folder: launch.cwd,
reports: launch.reports,
bell: false,
@@ -268,13 +271,9 @@ impl Workspace {
&self.sessions
}
/// The shown session's title, or the app's name while its program names none.
/// What the shown session is called.
fn title(&self) -> &str {
self.sessions
.get(self.active)
.map(|session| session.title.as_str())
.filter(|title| !title.is_empty())
.unwrap_or(TITLE)
self.sessions.get(self.active).map_or(TITLE, Session::named)
}
fn retitle(&self, window: &mut Window) {
+4 -1
View File
@@ -21,7 +21,10 @@ impl Workspace {
};
let session = &mut self.sessions[index];
match event {
TerminalEvent::Title(title) => session.title.clone_from(title),
TerminalEvent::Title(title) => {
log::debug!("app: session {}'s program names it {title:?}", index + 1);
session.title.clone_from(title);
}
TerminalEvent::Folder(folder) => {
session.folder.clone_from(folder);
session.reports = true;
+77
View File
@@ -0,0 +1,77 @@
//! A session renamed in place, as a double-click on its row asks.
use ely_gpui_component::forms::{InputEvent, TextInput};
use gpui::{AppContext, Context, EntityId, Focusable, Window};
use super::Workspace;
impl Workspace {
/// Opens a field over session `index`'s title, what it is called selected.
pub fn rename(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
let Some(session) = self.sessions.get(index) else {
return;
};
if session.editing.is_some() {
return;
}
let named = session.named().to_string();
let id = session.terminal.entity_id();
let input = cx.new(|cx| {
let mut input = TextInput::new(window, cx);
input.set_text(named.clone(), cx);
input.select(0..named.len(), cx);
input
});
let events = cx.subscribe_in(
&input,
window,
move |this, _, event, window, cx| match event {
InputEvent::Submit => this.end_rename(id, true, true, window, cx),
InputEvent::Blur => this.end_rename(id, true, false, window, cx),
InputEvent::Changed | InputEvent::Focus => {}
},
);
window.focus(&input.focus_handle(cx), cx);
log::debug!("app: session {} is renamed in place", index + 1);
self.sessions[index].editing = Some((input, events));
cx.notify();
}
/// Closes the field renaming the session `id` names, `keep`ing what it holds or not, the shown terminal `focused` again or not.
pub fn end_rename(
&mut self,
id: EntityId,
keep: bool,
focused: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(session) = self
.sessions
.iter_mut()
.find(|session| session.terminal.entity_id() == id)
else {
log::debug!("app: the session renamed is gone");
return;
};
let Some((input, _)) = session.editing.take() else {
return;
};
let pid = session.terminal.read(cx).pid();
if !keep {
log::debug!("app: session {pid} keeps its name");
} else {
session.name = input.read(cx).text().trim().to_string();
if session.name.is_empty() {
log::info!("app: session {pid} takes its program's title again");
} else {
log::info!("app: session {pid} is named {:?}", session.name);
}
}
if focused && let Some(shown) = self.sessions.get(self.active) {
window.focus(&shown.terminal.focus_handle(cx), cx);
}
self.retitle(window);
cx.notify();
}
}
+1
View File
@@ -63,6 +63,7 @@ What no check reached: no real KWin drew the blur; of Wayland desktops, only Hyp
| 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 |
| JetBrains Mono, any installed font, Nerd Font icons | T06, T17 | `tests/fonts.rs` | Pass |
| Rename: a double press on a row's title opens a field; Enter keeps the name, Escape drops it, a blur keeps it; the name shows in the row, the header and the window's title over the program's own; an empty one gives the title back | T63 | Ely test: `ListItem::editor` draws its field in the row, above the description; on macOS `shot` over a program titled `prog`: a double press on the row opened a field holding `prog`, selected; `Build box` and Enter showed it in the row, the header and the window's title, `app: session … is named "Build box"`, and the focus came back to the terminal; the program's later title `prog2`, logged four seconds after, left the window's title `Build box`; Escape logged `keeps its name` and left `prog2`; a blur from a press on the terminal while the app was active kept `Blurred`; emptied, the field gave the title back, `takes its program's title again`; a second session's rename named that row alone | Pass |
| Fallback fonts: installed families tried in order for a character the family lacks, listed under Family in Settings › Appearance | T60 | `settings` test: a file without `font_fallbacks` loads with none, named families keep their order, and an empty one is refused; on macOS with Menlo and `"font_fallbacks": ["Kaiti SC"]` the terminal drew `中文字体 終端` in Kaiti and Latin in Menlo, where without it the system drew them in PingFang; a fallback gpui cannot load, `No Such Font`, stopped the start with `font_fallbacks: the terminal cannot load "No Such Font"`, and an empty one with `font_fallbacks holds an empty family`; in Settings › Appearance, Add fallback opened an empty picker, picking Kaiti SC saved `["Kaiti SC"]` and the terminal took it at once, the row's X saved `[]`, and Apple Color Emoji, which gpui loads no font for, was refused and nothing saved; round 1 of T60's review found that removing the row whose input held the focus left the removed family in it, and Enter saved it over the next: a removal drops that row's input and the focus, so after removing Kaiti SC from `[Kaiti SC, Songti SC, PingFang SC]` with its input focused the rows showed Songti SC and PingFang SC and Enter saved nothing new; round 2 found the pickers keyed by place, so after that removal a blur wrote PingFang SC into the first row and Enter saved it twice: each picker is keyed by its input, and the same removal, a click, the app activated and deactivated, and Enter, which in the build before saved `[PingFang SC, PingFang SC]`, kept `[Songti SC, PingFang SC]`; on Linux, gpui's cosmic-text path carries a character a second fallback took into the next, as AGENTS records | Pass |
| Cursor shapes, blink by setting and DECSCUSR, hollow unfocused | T06 | T06 review | Pass |
| xterm keys, cursor mode, Option as Alt, kitty protocol, keypad; a shifted symbol as its own key with Shift on macOS and Windows; win32-input-mode on Windows | T08, T44 | `term::keys` tests, `kitty_reports_the_key_shift_turned_into_a_symbol` red at `CSI 123;13u` before the fix; on macOS herdr 0.9.5, its `[keys]` `previous_workspace = "cmd+shift+ctrl+["` and `next_workspace = "cmd+shift+ctrl+]"`, run by `shot` in a fresh HOME: before, ⌘⇧⌃] sent `CSI 125;13u` and herdr stayed on its workspace; after, `CSI 93:125;14u` moved it to the next and ⌘⇧⌃[, `CSI 91:123;14u`, back; `term::win32` and `term::tap` tests on Windows | Pass |