This commit is contained in:
+3
-2
@@ -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
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user