T11: the window is Ely's ColumnShell: sessions, terminal and right columns under a transparent title bar, the terminal's header wearing the program's title; Cmd-B and Cmd-Alt-B toggle the side columns, kept in the settings; Linux asks for client decorations inside Ely's ResizeBorder
This commit is contained in:
5 files changed
+155
-55
No files matched your search
@@ -21,7 +21,7 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
|
||||
## Layout
|
||||
|
||||
- `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.
|
||||
- `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. `src/workspace.rs`: the window's content, Ely's `ColumnShell` with the sessions, terminal and right columns, their toggles and the title.
|
||||
- `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`.
|
||||
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance.
|
||||
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 7 and the mouse modes before the parser. `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. `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 and repeat flag of AppKit's current event, keeps held keys repeating, and hands Command key-ups to the focused view. `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.
|
||||
@@ -59,4 +59,5 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
- 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.
|
||||
- 2026-10-14: find opens on Cmd-F (Ctrl-Shift-F off macOS) beside the grid, outside the `Terminal` key context, so its keys never reach the program. Matches wash in Ely's `warning` color, the current one stronger; the newest is current first, Enter steps older and Shift-Enter newer. Shift-Enter binds under `TerminalFind > ElyInput`, the context of Ely's text input, whose own Shift-Enter binding takes the key. Find reads the grid as it shows: combining marks ride their cells, hidden cells are blank, wrapped rows join, words are Unicode; alacritty's own search reads no marks and knows only ASCII word edges. New output, a resize and a settings change search again; the current match stays the same occurrence: its line kept, grown or cut at its end, held under the same line above or in its place, before such a line elsewhere, nearest where scrolling moved it; else the nearest by line, then column. Searching and following take one pass over the text and the matches.
|
||||
- 2026-10-14: the program hears focus in and out under mode 1004 when the terminal's focus or the window's activation changes. The window wears the program's title, the app's name while it names none. A bell is logged and makes no sound. A full reset, `ESC c`, empties alacritty's title and sends only `CursorBlinkingChange`, in the parser's order, synchronized updates too; on that event the view replays the title through `set_options`, which sends it.
|
||||
- 2026-10-14: the title bar is transparent, as Ghostty's: the content runs under it, Ely's column headers drag the window, and the terminal's header shows the program's title. macOS draws its traffic lights at Ely's origin; elsewhere Ely draws the controls, so windows ask for client decorations, which only Linux reads, and Ely's `ResizeBorder` gives the edges. Cmd-B and Cmd-Alt-B (Ctrl-Shift-B and Ctrl-Shift-Alt-B off macOS) toggle the sessions and right columns, kept in the settings.
|
||||
- 2026-10-14: settings refuse unknown fields; a bad settings file stops the start and is never overwritten.
|
||||
+2
-1
@@ -143,8 +143,9 @@ mod typing {
|
||||
};
|
||||
|
||||
/// US layout key codes and what each key types.
|
||||
const KEYS: [(&str, u16, &str); 19] = [
|
||||
const KEYS: [(&str, u16, &str); 20] = [
|
||||
("a", 0, "a"),
|
||||
("b", 11, "b"),
|
||||
("c", 8, "c"),
|
||||
("e", 14, "e"),
|
||||
("f", 3, "f"),
|
||||
|
||||
+11
-53
@@ -1,63 +1,21 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use ely_gpui_component::{primitives::FocusScope, theme::ActiveTheme};
|
||||
use ely_gpui_component::theme::ActiveTheme;
|
||||
use gpui::{
|
||||
App, AppContext, Bounds, Context, Entity, FocusHandle, Focusable, IntoElement, ParentElement,
|
||||
Render, Styled, Subscription, TitlebarOptions, Window, WindowBounds, WindowHandle,
|
||||
WindowOptions, px, size,
|
||||
App, AppContext, Bounds, Focusable, TitlebarOptions, WindowBounds, WindowDecorations,
|
||||
WindowHandle, WindowOptions, px, size,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::term::mac;
|
||||
use crate::{
|
||||
settings::SettingsStore,
|
||||
term::{
|
||||
ICONS, Launch, MONO, TerminalEvent, TerminalView, config, input, shell::ShellIntegration,
|
||||
spawn,
|
||||
},
|
||||
term::{ICONS, Launch, MONO, TerminalView, config, input, shell::ShellIntegration, spawn},
|
||||
themes,
|
||||
workspace::{self, TITLE, Workspace},
|
||||
};
|
||||
|
||||
/// The window title while the program names none.
|
||||
const TITLE: &str = "ELYTerminal";
|
||||
|
||||
/// The main window's content.
|
||||
pub struct Workspace {
|
||||
focus: FocusHandle,
|
||||
terminal: Entity<TerminalView>,
|
||||
_events: Subscription,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
/// The window wears the terminal's title.
|
||||
fn terminal_event(
|
||||
&mut self,
|
||||
_: &Entity<TerminalView>,
|
||||
event: &TerminalEvent,
|
||||
window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
if let TerminalEvent::Title(title) = event {
|
||||
let title = if title.is_empty() { TITLE } else { title };
|
||||
if window.window_title() != title {
|
||||
log::debug!("app: the window title is {title:?}");
|
||||
window.set_window_title(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Workspace {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
FocusScope::new(&self.focus)
|
||||
.root()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors.bg)
|
||||
.child(self.terminal.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fonts, settings, integration scripts and the chosen font, each checked before a window opens.
|
||||
pub fn start(cx: &mut App) -> anyhow::Result<()> {
|
||||
cx.text_system()
|
||||
@@ -82,6 +40,7 @@ pub fn start(cx: &mut App) -> anyhow::Result<()> {
|
||||
SettingsStore::init(cx)?;
|
||||
ShellIntegration::init(cx)?;
|
||||
input::bind_keys(cx);
|
||||
workspace::bind_keys(cx);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
mac::repeat_held_keys();
|
||||
@@ -103,8 +62,11 @@ pub fn open(launch: Launch, cx: &mut App) -> anyhow::Result<WindowHandle<Workspa
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some(TITLE.into()),
|
||||
..Default::default()
|
||||
appears_transparent: true,
|
||||
traffic_light_position: Some(cx.theme().traffic_light_origin()),
|
||||
}),
|
||||
// Ely's columns draw the controls; only Linux reads this, and must not draw its own.
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
..Default::default()
|
||||
};
|
||||
let (pty, events) = spawn(&launch, (80, 24), (8, 16), config(SettingsStore::get(cx)))?;
|
||||
@@ -114,11 +76,7 @@ pub fn open(launch: Launch, cx: &mut App) -> anyhow::Result<WindowHandle<Workspa
|
||||
.detach();
|
||||
let terminal = cx.new(|cx| TerminalView::new(pty, events, window, cx));
|
||||
window.focus(&terminal.focus_handle(cx), cx);
|
||||
cx.new(|cx| Workspace {
|
||||
focus: cx.focus_handle(),
|
||||
_events: cx.subscribe_in(&terminal, window, Workspace::terminal_event),
|
||||
terminal,
|
||||
})
|
||||
cx.new(|cx| Workspace::new(terminal, window, cx))
|
||||
})?;
|
||||
log::info!("app: main window open");
|
||||
Ok(window)
|
||||
|
||||
@@ -4,3 +4,4 @@ pub mod term;
|
||||
#[cfg(test)]
|
||||
mod testlog;
|
||||
pub mod themes;
|
||||
pub mod workspace;
|
||||
@@ -0,0 +1,139 @@
|
||||
use ely_gpui_component::{
|
||||
primitives::FocusScope,
|
||||
shell::{ColumnShell, ResizeBorder, ShellColumn},
|
||||
theme::ActiveTheme,
|
||||
};
|
||||
use gpui::{
|
||||
App, Context, Entity, FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement,
|
||||
Render, SharedString, Styled, Subscription, Window, actions, div,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
settings::{Settings, SettingsStore},
|
||||
term::{TerminalEvent, TerminalView},
|
||||
};
|
||||
|
||||
actions!(elyterminal, [ToggleSessions, TogglePanel]);
|
||||
|
||||
/// The key context of the main window's content.
|
||||
const CONTEXT: &str = "Workspace";
|
||||
|
||||
/// The window's title while the program names none.
|
||||
pub const TITLE: &str = "ELYTerminal";
|
||||
|
||||
/// Cmd-B and Cmd-Alt-B toggle the side columns; Ctrl-Shift-B and Ctrl-Shift-Alt-B off macOS.
|
||||
pub fn bind_keys(cx: &mut App) {
|
||||
let (sessions, panel) = if cfg!(target_os = "macos") {
|
||||
("cmd-b", "cmd-alt-b")
|
||||
} else {
|
||||
("ctrl-shift-b", "ctrl-shift-alt-b")
|
||||
};
|
||||
cx.bind_keys([
|
||||
KeyBinding::new(sessions, ToggleSessions, Some(CONTEXT)),
|
||||
KeyBinding::new(panel, TogglePanel, Some(CONTEXT)),
|
||||
]);
|
||||
}
|
||||
|
||||
/// The main window's content: sessions, the terminal, and Git or Files, in Ely's columns.
|
||||
pub struct Workspace {
|
||||
focus: FocusHandle,
|
||||
terminal: Entity<TerminalView>,
|
||||
/// The program's title, or the app's name while it names none.
|
||||
title: SharedString,
|
||||
_events: Subscription,
|
||||
_settings: Subscription,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn new(
|
||||
terminal: Entity<TerminalView>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
focus: cx.focus_handle(),
|
||||
title: TITLE.into(),
|
||||
_events: cx.subscribe_in(&terminal, window, Self::terminal_event),
|
||||
_settings: cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
|
||||
terminal,
|
||||
}
|
||||
}
|
||||
|
||||
/// The window and the terminal's header wear the program's title.
|
||||
fn terminal_event(
|
||||
&mut self,
|
||||
_: &Entity<TerminalView>,
|
||||
event: &TerminalEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let TerminalEvent::Title(title) = event {
|
||||
let title = if title.is_empty() { TITLE } else { title };
|
||||
if self.title.as_ref() != title {
|
||||
log::debug!("app: the title is {title:?}");
|
||||
self.title = SharedString::from(title.to_string());
|
||||
window.set_window_title(title);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows or hides a side column, as the settings keep it.
|
||||
fn toggle(&mut self, flip: fn(&mut Settings), cx: &mut Context<Self>) {
|
||||
match SettingsStore::update(cx, flip) {
|
||||
Ok(()) => {
|
||||
let settings = SettingsStore::get(cx);
|
||||
log::info!(
|
||||
"app: sessions {}, right column {}",
|
||||
if settings.show_sessions {
|
||||
"shown"
|
||||
} else {
|
||||
"hidden"
|
||||
},
|
||||
if settings.show_panel {
|
||||
"shown"
|
||||
} else {
|
||||
"hidden"
|
||||
}
|
||||
);
|
||||
}
|
||||
Err(error) => log::error!("app: the columns stay: {error:#}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Workspace {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = SettingsStore::get(cx);
|
||||
let width = cx.theme().sidebar_width(false);
|
||||
let mut shell = ColumnShell::new("workspace");
|
||||
if settings.show_sessions {
|
||||
shell = shell.column(ShellColumn::new("sessions").width(width));
|
||||
}
|
||||
shell = shell.column(
|
||||
ShellColumn::new("terminal")
|
||||
.title(self.title.clone())
|
||||
.child(self.terminal.clone()),
|
||||
);
|
||||
if settings.show_panel {
|
||||
shell = shell.column(ShellColumn::new("panel").width(width));
|
||||
}
|
||||
ResizeBorder::new().child(
|
||||
FocusScope::new(&self.focus).root().size_full().child(
|
||||
div()
|
||||
.size_full()
|
||||
.key_context(CONTEXT)
|
||||
.on_action(cx.listener(|this, _: &ToggleSessions, _, cx| {
|
||||
this.toggle(
|
||||
|settings| settings.show_sessions = !settings.show_sessions,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &TogglePanel, _, cx| {
|
||||
this.toggle(|settings| settings.show_panel = !settings.show_panel, cx)
|
||||
}))
|
||||
.child(shell),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user