T22: the macOS menu bar holds the PRD's menus, each item showing its key; Quit, Select All, Bigger, Smaller, Reset Size and Minimize take Ghostty's keys, Ctrl-Shift off macOS where Ctrl with a letter types a control byte; the font's size moves for every session, unsaved; Select All runs from the first character in the history to the last, as Ghostty's; shot lists the menus and says whether the app was active at each key
CI / platforms (windows-latest) (push) Failing after 8s
CI / macos (push) Failing after 8m29s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-15 01:51:46 -04:00
1 parent 149c1b5f02
commit 5ee67bd406
15 files changed
+770 -495

No files matched your search

+2 -1
View File
@@ -10,7 +10,7 @@ use gpui::{
#[cfg(target_os = "macos")]
use crate::term::mac;
use crate::{
blur,
blur, menus,
settings::{self, SettingsStore},
term::{ICONS, Launch, MONO, config, input, shell::ShellIntegration, spawn},
themes,
@@ -43,6 +43,7 @@ pub fn start(cx: &mut App) -> anyhow::Result<()> {
input::bind_keys(cx);
workspace::bind_keys(cx);
settings::window::bind_keys(cx);
menus::init(cx);
// Settings alone hold no terminal, so they go with the last one.
cx.on_window_closed(|cx, _| {
let windows = cx.windows();
+1
View File
@@ -2,6 +2,7 @@ pub mod app;
pub mod blur;
pub mod files;
pub mod git;
pub mod menus;
pub mod sessions;
pub mod settings;
pub mod term;
+110
View File
@@ -0,0 +1,110 @@
use gpui::{App, InteractiveElement, KeyBinding, Menu, MenuItem, OsAction, actions};
use crate::{
settings::window::OpenSettings,
term::{
input::{Copy, Find, Paste, SelectAll},
zoom,
},
workspace::{CloseSession, NewSession, TogglePanel, ToggleSessions},
};
actions!(
elyterminal,
[Quit, Bigger, Smaller, ResetSize, Minimize, Zoom]
);
/// The menu bar the PRD lists; macOS draws it and names the first menu after the app.
pub fn menus() -> Vec<Menu> {
let menu = |name: &str, items| Menu {
name: name.to_string().into(),
items,
disabled: false,
};
vec![
menu(
"ELYTerminal",
vec![
MenuItem::action("Settings…", OpenSettings),
MenuItem::separator(),
MenuItem::action("Quit ELYTerminal", Quit),
],
),
menu(
"Shell",
vec![
MenuItem::action("New Session", NewSession),
MenuItem::action("Close Session", CloseSession),
],
),
menu(
"Edit",
vec![
MenuItem::os_action("Copy", Copy, OsAction::Copy),
MenuItem::os_action("Paste", Paste, OsAction::Paste),
MenuItem::os_action("Select All", SelectAll, OsAction::SelectAll),
MenuItem::separator(),
MenuItem::action("Find", Find),
],
),
menu(
"View",
vec![
MenuItem::action("Toggle Sessions", ToggleSessions),
MenuItem::action("Toggle Right Panel", TogglePanel),
MenuItem::separator(),
MenuItem::action("Bigger", Bigger),
MenuItem::action("Smaller", Smaller),
MenuItem::action("Reset Size", ResetSize),
],
),
// macOS lists the open windows here itself.
menu(
"Window",
vec![
MenuItem::action("Minimize", Minimize),
MenuItem::action("Zoom", Zoom),
],
),
]
}
/// Ghostty's keys for Quit and the font's size, Cmd on macOS and Ctrl elsewhere, Cmd-M to minimize; what they do; then the menus, which show the keys bound by now.
pub fn init(cx: &mut App) {
let (quit, modifier) = if cfg!(target_os = "macos") {
("cmd-q", "cmd")
} else {
("ctrl-shift-q", "ctrl")
};
cx.bind_keys([
KeyBinding::new(quit, Quit, None),
KeyBinding::new(&format!("{modifier}-="), Bigger, None),
KeyBinding::new(&format!("{modifier}-+"), Bigger, None),
KeyBinding::new(&format!("{modifier}--"), Smaller, None),
KeyBinding::new(&format!("{modifier}-0"), ResetSize, None),
]);
if cfg!(target_os = "macos") {
cx.bind_keys([KeyBinding::new("cmd-m", Minimize, None)]);
}
cx.set_global(zoom::Enlarged::default());
cx.on_action(|_: &Quit, cx| {
log::info!("app: quit asked");
cx.quit();
});
cx.on_action(|_: &Bigger, cx| zoom::step(Some(1.0), cx));
cx.on_action(|_: &Smaller, cx| zoom::step(Some(-1.0), cx));
cx.on_action(|_: &ResetSize, cx| zoom::step(None, cx));
cx.set_menus(menus());
}
/// Minimize and Zoom on a window's root: only a window answers them.
pub fn window_actions<E: InteractiveElement>(root: E) -> E {
root.on_action(|_: &Minimize, window, _| {
log::info!("window: minimized");
window.minimize_window();
})
.on_action(|_: &Zoom, window, _| {
log::info!("window: zoomed");
window.zoom_window();
})
}
+3 -2
View File
@@ -13,11 +13,11 @@ use gpui::{
App, AppContext, Bounds, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
KeyBinding, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled,
Subscription, Task, TitlebarOptions, Window, WindowBounds, WindowDecorations, WindowOptions,
actions, div, px, size,
actions, div, prelude::FluentBuilder, px, size,
};
use super::{Settings, SettingsStore, StartIn};
use crate::term::BLINK;
use crate::{menus, term::BLINK};
mod appearance;
mod terminal;
@@ -325,6 +325,7 @@ impl Render for SettingsWindow {
div()
.size_full()
.key_context(CONTEXT)
.map(menus::window_actions)
.on_action(|_: &CloseSettings, window, _| {
log::info!("settings: the window closes");
window.remove_window();
+11 -5
View File
@@ -11,7 +11,7 @@ use super::{
view::TerminalView,
};
actions!(elyterminal, [Copy, Paste, Find, FindNewer]);
actions!(elyterminal, [Copy, Paste, Find, FindNewer, SelectAll]);
/// The key context a terminal's root sets.
pub const CONTEXT: &str = "Terminal";
@@ -19,16 +19,22 @@ pub const CONTEXT: &str = "Terminal";
/// The key context of a terminal's find box.
pub const FIND: &str = "TerminalFind";
/// Copy, paste and find; Ely's focus keys go to the terminal's listeners instead.
/// Copy, paste, select all and find; Ely's focus keys go to the terminal's listeners instead.
pub fn bind_keys(cx: &mut App) {
let (copy, paste, find) = if cfg!(target_os = "macos") {
("cmd-c", "cmd-v", "cmd-f")
let (copy, paste, all, find) = if cfg!(target_os = "macos") {
("cmd-c", "cmd-v", "cmd-a", "cmd-f")
} else {
("ctrl-shift-c", "ctrl-shift-v", "ctrl-shift-f")
(
"ctrl-shift-c",
"ctrl-shift-v",
"ctrl-shift-a",
"ctrl-shift-f",
)
};
cx.bind_keys([
KeyBinding::new(copy, Copy, Some(CONTEXT)),
KeyBinding::new(paste, Paste, Some(CONTEXT)),
KeyBinding::new(all, SelectAll, Some(CONTEXT)),
KeyBinding::new(find, Find, Some(CONTEXT)),
KeyBinding::new(
"shift-enter",
+1
View File
@@ -16,6 +16,7 @@ mod tap;
#[cfg(unix)]
mod unix;
mod view;
pub mod zoom;
pub use pty::{Events, Launch, Listener, Pty, PtyEvent, config, fit, spawn};
pub use view::{BLINK, ICONS, MONO, TerminalEvent, TerminalView, load_font};
+57 -2
View File
@@ -1,8 +1,8 @@
use alacritty_terminal::{
grid::Scroll,
grid::{Dimensions, Scroll},
index::{Column, Line, Point as Cell, Side},
selection::{Selection, SelectionType},
term::TermMode,
term::{Term, TermMode},
};
use gpui::{
ClipboardItem, Context, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
@@ -298,6 +298,25 @@ impl TerminalView {
(!text.is_empty()).then_some(text)
}
/// Selects the history and the screen, blanks around them left out; copy on select takes the text.
pub(super) fn select_all(&mut self, cx: &mut Context<Self>) {
{
let mut term = self.pty.term.lock();
let Some(all) = everything(&*term) else {
log::debug!("terminal {}: nothing to select", self.pty.pid);
return;
};
term.selection = Some(all);
}
let selected = self.selected();
let count = selected.as_ref().map_or(0, |text| text.chars().count());
log::debug!("terminal {}: all {count} characters selected", self.pty.pid);
if let Some(text) = selected.filter(|_| SettingsStore::get(cx).copy_on_select) {
self.copy_text(text, true, cx);
}
cx.notify();
}
/// The selection to the clipboard.
pub(super) fn copy(&mut self, cx: &mut Context<Self>) {
match self.selected() {
@@ -347,3 +366,39 @@ impl TerminalView {
self.input(keys::paste(&text, bracketed), cx);
}
}
/// Ghostty's select all: the first cell holding more than a space or tab, through history and screen, to the last.
fn everything<T>(term: &Term<T>) -> Option<Selection> {
let grid = term.grid();
let cells = (grid.topmost_line().0..=grid.bottommost_line().0).flat_map(|line| {
(0..grid.columns()).map(move |column| Cell::new(Line(line), Column(column)))
});
let inked = |cell: &Cell| !matches!(grid[*cell].c, ' ' | '\t');
let first = cells.clone().find(inked)?;
let last = cells.rev().find(inked)?;
let mut all = Selection::new(SelectionType::Simple, first, Side::Left);
all.update(last, Side::Right);
Some(all)
}
#[cfg(test)]
mod tests {
use alacritty_terminal::{
event::VoidListener,
term::{Config, test::TermSize},
vte::ansi::Processor,
};
use super::*;
#[test]
fn everything_runs_from_the_first_character_in_the_history_to_the_last() {
let mut term = Term::new(Config::default(), &TermSize::new(10, 3), VoidListener);
assert!(everything(&term).is_none(), "a blank grid selects nothing");
let mut parser: Processor = Processor::new();
parser.advance(&mut term, b"\r\n one\r\n\r\ntwo six\t\r\n\r\n");
term.selection = everything(&term);
let text = term.selection_to_string().expect("a selection");
assert_eq!(text, "one\n\ntwo six");
}
}
+9 -5
View File
@@ -18,11 +18,12 @@ use super::{
find::Finder,
frame::frame,
ink::Ink,
input::{self, Copy, Find, Paste},
input::{self, Copy, Find, Paste, SelectAll},
mouse::MouseMode,
paint::{self, Look},
pointer::Pointer,
pty::{Events, Pty, PtyEvent, config, fit},
zoom,
};
use crate::{
settings::{Settings, SettingsStore},
@@ -71,6 +72,7 @@ pub struct TerminalView {
_blink: Task<()>,
_focus: [Subscription; 3],
_settings: Subscription,
_zoom: Subscription,
}
impl EventEmitter<TerminalEvent> for TerminalView {}
@@ -120,6 +122,7 @@ impl TerminalView {
view.search(false, cx);
cx.notify();
}),
_zoom: cx.observe_global::<zoom::Enlarged>(|_, cx| cx.notify()),
}
}
@@ -266,8 +269,8 @@ pub fn load_font(font: &Font, system: &TextSystem) -> anyhow::Result<FontId> {
Ok(id)
}
/// The font, cell and metrics the settings give at this window's scale.
fn look(settings: &Settings, ink: &Ink, window: &Window) -> anyhow::Result<Look> {
/// The font, cell and metrics the settings give at `points` and this window's scale.
fn look(settings: &Settings, points: f32, ink: &Ink, window: &Window) -> anyhow::Result<Look> {
let font = Font {
family: settings.font_family.clone().into(),
features: if settings.ligatures {
@@ -279,7 +282,7 @@ fn look(settings: &Settings, ink: &Ink, window: &Window) -> anyhow::Result<Look>
weight: FontWeight::NORMAL,
style: FontStyle::Normal,
};
let text = px(settings.font_size);
let text = px(points);
let system = window.text_system();
let id = load_font(&font, system)?;
let advance = system
@@ -349,6 +352,7 @@ impl Render for TerminalView {
.on_key_up(cx.listener(Self::key_up))
.on_action(cx.listener(|view, _: &Copy, _, cx| view.copy(cx)))
.on_action(cx.listener(|view, _: &Paste, _, cx| view.paste(cx)))
.on_action(cx.listener(|view, _: &SelectAll, _, cx| view.select_all(cx)))
.on_action(cx.listener(|view, _: &Find, window, cx| view.find(window, cx)))
.on_any_mouse_down(cx.listener(Self::mouse_down))
.on_mouse_move(cx.listener(Self::mouse_move))
@@ -367,7 +371,7 @@ impl Render for TerminalView {
root.on_mouse_up(button, cx.listener(Self::mouse_up))
.on_mouse_up_out(button, cx.listener(Self::mouse_up))
});
let root = match look(&settings, &ink, window) {
let root = match look(&settings, zoom::size(cx), &ink, window) {
Ok(look) => {
self.cell = (
f32::from(look.cell.width).round() as u16,
+54
View File
@@ -0,0 +1,54 @@
use gpui::{App, Global};
use crate::settings::{FONT_SIZE, SettingsStore};
/// Points the terminal's font stands above the Size setting, below it when negative; Bigger and Smaller move it, nothing saves it.
#[derive(Default)]
pub struct Enlarged(f32);
impl Global for Enlarged {}
/// The size the terminal draws its font at: the setting, moved, inside the range.
pub fn size(cx: &App) -> f32 {
shown(SettingsStore::get(cx).font_size, cx.global::<Enlarged>().0)
}
/// Moves the font `by` points, or back to the setting when `by` is none.
pub fn step(by: Option<f32>, cx: &mut App) {
let setting = SettingsStore::get(cx).font_size;
let enlarged = by.map_or(0.0, |by| moved(setting, cx.global::<Enlarged>().0, by));
cx.set_global(Enlarged(enlarged));
log::info!(
"zoom: the terminal's font is {} points, {enlarged:+} on the setting",
shown(setting, enlarged)
);
}
fn shown(setting: f32, enlarged: f32) -> f32 {
(setting + enlarged).clamp(*FONT_SIZE.start(), *FONT_SIZE.end())
}
/// What `enlarged` becomes once the font moves `by` points from where it shows.
fn moved(setting: f32, enlarged: f32, by: f32) -> f32 {
shown(shown(setting, enlarged) + by, 0.0) - setting
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_step_moves_the_font_inside_the_range() {
assert_eq!(moved(13.0, 0.0, 1.0), 1.0);
assert_eq!(moved(13.0, 2.0, -1.0), 1.0);
assert_eq!(moved(71.0, 0.0, 1.0), 1.0);
assert_eq!(moved(72.0, 0.0, 1.0), 0.0, "the top holds");
assert_eq!(moved(6.0, 0.0, -1.0), 0.0, "the bottom holds");
assert_eq!(
shown(70.0, 5.0),
72.0,
"a larger setting under a zoom stays in range"
);
assert_eq!(moved(70.0, 5.0, -1.0), 1.0, "a step starts from what shows");
}
}
+2
View File
@@ -16,6 +16,7 @@ use crate::{
blur::Blur,
files::Files,
git::panel::{Acted, GitPanel},
menus,
sessions::{self, Session},
settings::{PanelTab, Settings, SettingsStore, StartIn},
term::{
@@ -404,6 +405,7 @@ impl Render for Workspace {
div()
.size_full()
.key_context(CONTEXT)
.map(menus::window_actions)
.on_action(cx.listener(|this, _: &ToggleSessions, _, cx| {
this.toggle(
|settings| settings.show_sessions = !settings.show_sessions,