feat: add responsive ultrawide work lanes
CI / rust (push) Successful in 2m3s

This commit is contained in:
2026-08-31 17:46:18 -07:00
parent e7ca1d0a73
commit 3f15b13312
11 changed files with 304 additions and 74 deletions
+3 -2
View File
@@ -24,8 +24,9 @@ shared model retain the all-deterministic mode for like-for-like framework
comparison. GPUI feeds raw output through `lumbridge-terminal` and sends encoded
keyboard input and terminal protocol replies through the bounded runtime actor.
Its visual adapter coalesces VT cells into native styled runs, paints cursor
shapes, and resizes the engine and PTY from the middle 60% work band. Selection,
scrollback navigation, and mouse input remain intentionally unfinished.
shapes, and resizes the engine and PTY from a responsive one/three/five-lane
middle 60% work band. Retained-history navigation is wired; text selection and
mouse input remain intentionally unfinished.
Build independently:
+153 -17
View File
@@ -16,7 +16,7 @@ use lumbridge_spike_model::{
use lumbridge_terminal::{
KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions,
TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalKeyEvent, TerminalNamedColor,
TerminalSnapshot,
TerminalScroll, TerminalSnapshot,
};
const BG: u32 = 0x090c12;
@@ -43,6 +43,7 @@ const TERMINAL_CHROME_HEIGHT: f32 = 72.0;
const TERMINAL_HORIZONTAL_INSET: f32 = 32.0;
const TERMINAL_CELL_WIDTH: f32 = 8.4;
const TERMINAL_CELL_HEIGHT: f32 = 18.0;
const WORK_LANE_GAP: f32 = 4.0;
const TERMINAL_ROW_STEP: u16 = 2;
const TERMINAL_COLUMN_STEP: u16 = 10;
@@ -279,10 +280,14 @@ fn dim_color(color: u32) -> u32 {
fn terminal_dimensions_for_window(window_size: Size<Pixels>) -> TerminalDimensions {
let width = f32::from(window_size.width);
let height = f32::from(window_size.height);
let lane_count = visible_lane_count(window_size);
let workspace_height =
(height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0);
let terminal_height = (workspace_height * 0.60 - TERMINAL_CHROME_HEIGHT).max(0.0);
let terminal_width = (width - SIDEBAR_WIDTH - TERMINAL_HORIZONTAL_INSET).max(0.0);
let workspace_width = (width - SIDEBAR_WIDTH).max(0.0);
let lane_gaps = WORK_LANE_GAP * lane_count.saturating_sub(1) as f32;
let lane_width = ((workspace_width - lane_gaps).max(0.0) / lane_count as f32).max(0.0);
let terminal_width = (lane_width - TERMINAL_HORIZONTAL_INSET).max(0.0);
let rows = (terminal_height / TERMINAL_CELL_HEIGHT)
.floor()
.clamp(2.0, f32::from(u16::MAX));
@@ -298,6 +303,25 @@ fn terminal_dimensions_for_window(window_size: Size<Pixels>) -> TerminalDimensio
.expect("geometry clamps terminal dimensions above zero")
}
fn visible_lane_count(window_size: Size<Pixels>) -> usize {
let workspace_width = (f32::from(window_size.width) - SIDEBAR_WIDTH).max(0.0);
if workspace_width >= 2_800.0 {
5
} else if workspace_width >= 1_100.0 {
3
} else {
1
}
}
fn visible_lane_range(total: usize, selected: usize, lane_count: usize) -> std::ops::Range<usize> {
let count = lane_count.clamp(1, total.max(1)).min(total);
let start = selected
.saturating_sub(count / 2)
.min(total.saturating_sub(count));
start..start + count
}
impl RenderTiming {
fn mark_dispatch(&mut self) {
self.pending_dispatch = Some(Instant::now());
@@ -527,6 +551,14 @@ impl LumbridgeShell {
cx.notify();
}
fn scroll_terminal(&mut self, scroll: TerminalScroll, cx: &mut Context<Self>) {
if !self.terminal.scroll_display(scroll) {
return;
}
self.publish_terminal_snapshot();
cx.notify();
}
fn select_pane(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
self.dispatch(ShellAction::SelectPane(pane));
window.focus(&self.root_focus);
@@ -615,6 +647,13 @@ impl LumbridgeShell {
return;
}
let modifiers = key_modifiers(event);
if let Some(scroll) = terminal_scroll_from_parts(
event.keystroke.key.as_str(),
modifiers,
) {
self.scroll_terminal(scroll, cx);
return;
}
let Some(event) = terminal_key_from_parts(
event.keystroke.key.as_str(),
event.keystroke.key_char.as_deref(),
@@ -629,6 +668,11 @@ impl LumbridgeShell {
if let Err(error) = self.send_runtime_command(RuntimeCommand::Input(bytes)) {
self.runtime_status = LiveRuntimeStatus::Fault(error.to_string());
}
if self.terminal.display_offset() > 0
&& self.terminal.scroll_display(TerminalScroll::Bottom)
{
self.publish_terminal_snapshot();
}
cx.notify();
return;
}
@@ -807,8 +851,13 @@ impl LumbridgeShell {
.into_any_element()
}
fn work_surface(&self) -> gpui::AnyElement {
let pane = self.model.pane(self.model.selected_pane());
fn work_surface(
&self,
pane: &PaneState,
selected: bool,
cx: &mut Context<Self>,
) -> gpui::AnyElement {
let pane_id = pane.id();
let external = pane.output_source() == OutputSource::External;
let status = if external {
self.runtime_status.badge()
@@ -820,9 +869,17 @@ impl LumbridgeShell {
} else {
pane.fixture().target.to_owned()
};
let surface_status = if external {
let surface_status = if external && self.terminal_snapshot.display_offset > 0 {
format!(
"{} · {}×{} · auto-fit",
"{} · {} · {}×{}",
status,
self.terminal_snapshot.display_offset,
self.terminal.dimensions().columns(),
self.terminal.dimensions().rows()
)
} else if external {
format!(
"{} · {}×{}",
status,
self.terminal.dimensions().columns(),
self.terminal.dimensions().rows()
@@ -870,14 +927,19 @@ impl LumbridgeShell {
};
div()
.id(("work-lane", pane_id.index()))
.on_click(cx.listener(move |shell, _, window, cx| {
shell.select_pane(pane_id, window, cx);
}))
.flex()
.flex_col()
.size_full()
.min_w_0()
.min_h_0()
.overflow_hidden()
.bg(rgb(PANEL))
.border_y_1()
.border_color(rgb(BORDER))
.border_1()
.border_color(rgb(if selected { ACCENT } else { BORDER }))
.child(
div()
.flex()
@@ -891,15 +953,23 @@ impl LumbridgeShell {
div()
.flex()
.items_center()
.gap_3()
.gap_2()
.min_w_0()
.text_sm()
.child(pane.fixture().title)
.child(div().text_xs().text_color(rgb(MUTED)).child(detail)),
.child(div().flex_none().child(pane.fixture().title))
.child(
div()
.truncate()
.text_xs()
.text_color(rgb(MUTED))
.child(detail),
),
)
.child(
div()
.flex_none()
.text_xs()
.text_color(rgb(SUCCESS))
.text_color(rgb(if selected { SUCCESS } else { MUTED }))
.child(surface_status),
),
)
@@ -1068,7 +1138,7 @@ impl LumbridgeShell {
}
impl Render for LumbridgeShell {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let attention = self.model.pane(PaneId::ClaudeUi);
let sidebar = div()
.flex()
@@ -1251,6 +1321,26 @@ impl Render for LumbridgeShell {
})
.collect::<Vec<_>>();
let selected = self.model.pane(self.model.selected_pane());
let lane_count = visible_lane_count(window.bounds().size);
let lane_range = visible_lane_range(
PaneId::ALL.len(),
self.model.selected_pane().index(),
lane_count,
);
let work_lanes = lane_range
.map(|index| {
let pane = self.model.pane(PaneId::ALL[index]);
div()
.flex_1()
.min_w_0()
.min_h_0()
.child(self.work_surface(
pane,
pane.id() == self.model.selected_pane(),
cx,
))
})
.collect::<Vec<_>>();
let workspace_stack = div()
.flex()
.flex_col()
@@ -1310,9 +1400,11 @@ impl Render for LumbridgeShell {
div()
.h(relative(0.60))
.flex_none()
.flex()
.gap_1()
.min_h_0()
.overflow_hidden()
.child(self.work_surface()),
.children(work_lanes),
)
.child(
div()
@@ -1498,6 +1590,19 @@ fn terminal_key_from_parts(
Some(TerminalKeyEvent { key, modifiers })
}
fn terminal_scroll_from_parts(key: &str, modifiers: KeyModifiers) -> Option<TerminalScroll> {
if !modifiers.contains(KeyModifiers::SHIFT) {
return None;
}
match key {
"pageup" => Some(TerminalScroll::PageUp),
"pagedown" => Some(TerminalScroll::PageDown),
"home" => Some(TerminalScroll::Top),
"end" => Some(TerminalScroll::Bottom),
_ => None,
}
}
fn start_live_runtime(dimensions: TerminalDimensions) -> Result<RuntimeActor, RuntimeActorError> {
let command = CommandConfig::new("/bin/sh")
.map_err(RuntimeActorError::Start)?
@@ -1551,8 +1656,9 @@ fn main() {
#[cfg(test)]
mod tests {
use super::{
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, indexed_terminal_color,
terminal_dimensions_for_window, terminal_key_from_parts, terminal_paint_rows,
KeyModifiers, TerminalEngine, TerminalEngineOptions, TerminalKey, TerminalScroll,
indexed_terminal_color, terminal_dimensions_for_window, terminal_key_from_parts,
terminal_paint_rows, terminal_scroll_from_parts, visible_lane_count, visible_lane_range,
};
use gpui::{px, size};
@@ -1582,7 +1688,37 @@ mod tests {
fn terminal_geometry_tracks_the_middle_sixty_percent() {
let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0)));
assert_eq!(dimensions.rows(), 24);
assert_eq!(dimensions.columns(), 145);
assert_eq!(dimensions.columns(), 45);
let ultrawide = size(px(3440.0), px(1440.0));
assert_eq!(visible_lane_count(ultrawide), 5);
let dimensions = terminal_dimensions_for_window(ultrawide);
assert_eq!(dimensions.rows(), 40);
assert_eq!(dimensions.columns(), 71);
}
#[test]
fn lane_window_keeps_the_selected_pane_visible() {
assert_eq!(visible_lane_range(6, 0, 5), 0..5);
assert_eq!(visible_lane_range(6, 5, 5), 1..6);
assert_eq!(visible_lane_range(6, 3, 3), 2..5);
assert_eq!(visible_lane_range(6, 4, 1), 4..5);
}
#[test]
fn shift_navigation_controls_scrollback_without_reaching_the_pty() {
assert_eq!(
terminal_scroll_from_parts("pageup", KeyModifiers::SHIFT),
Some(TerminalScroll::PageUp)
);
assert_eq!(
terminal_scroll_from_parts("end", KeyModifiers::SHIFT),
Some(TerminalScroll::Bottom)
);
assert_eq!(
terminal_scroll_from_parts("pageup", KeyModifiers::default()),
None
);
}
#[test]