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
This commit is contained in:
15 files changed
+770
-495
No files matched your search
+2
-1
@@ -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();
|
||||
|
||||
@@ -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
@@ -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();
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user