This commit is contained in:
+210
-62
@@ -10,8 +10,8 @@ use lumbridge_runtime::{
|
||||
RuntimeCommand, RuntimeEvent, TerminalSize,
|
||||
};
|
||||
use lumbridge_spike_model::{
|
||||
ActionOutcome, FOOTER_RIGHT, FocusDirection, OutputSource, PaneId, PaneState, ShellAction,
|
||||
ShellModel, SurfaceKind, WORKSPACES,
|
||||
ActionOutcome, FOOTER_RIGHT, FocusDirection, INITIAL_ATTACHED_PANEL_COUNT, OutputSource,
|
||||
PaneId, PaneState, ShellAction, ShellModel, SurfaceKind, WORKSPACES,
|
||||
};
|
||||
use lumbridge_terminal::{
|
||||
KeyModifiers, TerminalCellStyle, TerminalColor, TerminalCursorShape, TerminalDimensions,
|
||||
@@ -62,6 +62,8 @@ actions!(
|
||||
SelectPane4,
|
||||
SelectPane5,
|
||||
SelectPane6,
|
||||
AddPanel,
|
||||
DetachSelectedPanel,
|
||||
TerminalTaller,
|
||||
TerminalShorter,
|
||||
TerminalWider,
|
||||
@@ -272,14 +274,18 @@ fn dim_color(color: u32) -> u32 {
|
||||
(dim((color >> 16) & 0xff) << 16) | (dim((color >> 8) & 0xff) << 8) | dim(color & 0xff)
|
||||
}
|
||||
|
||||
fn terminal_dimensions_for_window(window_size: Size<Pixels>) -> TerminalDimensions {
|
||||
fn terminal_dimensions_for_window(
|
||||
window_size: Size<Pixels>,
|
||||
attached_panel_count: usize,
|
||||
) -> TerminalDimensions {
|
||||
let width = f32::from(window_size.width);
|
||||
let height = f32::from(window_size.height);
|
||||
let panel_count = visible_panel_count(window_size);
|
||||
let panel_count = visible_panel_count(window_size)
|
||||
.min(attached_panel_count)
|
||||
.max(1);
|
||||
let workspace_height =
|
||||
(height - APP_HEADER_HEIGHT - TAB_BAR_HEIGHT - APP_FOOTER_HEIGHT).max(0.0);
|
||||
let terminal_height =
|
||||
(workspace_height * 0.60 - TERMINAL_CONTENT_VERTICAL_INSET).max(0.0);
|
||||
let terminal_height = (workspace_height * 0.60 - TERMINAL_CONTENT_VERTICAL_INSET).max(0.0);
|
||||
let workspace_width = (width - SIDEBAR_WIDTH).max(0.0);
|
||||
let panel_gaps = WORK_PANEL_GAP * panel_count.saturating_sub(1) as f32;
|
||||
let panel_width = ((workspace_width - panel_gaps).max(0.0) / panel_count as f32).max(0.0);
|
||||
@@ -401,7 +407,8 @@ impl LumbridgeShell {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let terminal_dimensions = terminal_dimensions_for_window(window.bounds().size);
|
||||
let terminal_dimensions =
|
||||
terminal_dimensions_for_window(window.bounds().size, INITIAL_ATTACHED_PANEL_COUNT);
|
||||
let terminal = TerminalEngine::new(TerminalEngineOptions {
|
||||
dimensions: terminal_dimensions,
|
||||
..TerminalEngineOptions::default()
|
||||
@@ -413,7 +420,10 @@ impl LumbridgeShell {
|
||||
};
|
||||
|
||||
cx.observe_window_bounds(window, |shell, window, cx| {
|
||||
let dimensions = terminal_dimensions_for_window(window.bounds().size);
|
||||
let dimensions = terminal_dimensions_for_window(
|
||||
window.bounds().size,
|
||||
shell.model.attached_panel_count(),
|
||||
);
|
||||
shell.resize_terminal(dimensions.rows(), dimensions.columns(), cx);
|
||||
})
|
||||
.detach();
|
||||
@@ -559,6 +569,50 @@ impl LumbridgeShell {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn resize_terminal_for_workspace(&mut self, window: &Window, cx: &mut Context<Self>) {
|
||||
let dimensions =
|
||||
terminal_dimensions_for_window(window.bounds().size, self.model.attached_panel_count());
|
||||
self.resize_terminal(dimensions.rows(), dimensions.columns(), cx);
|
||||
}
|
||||
|
||||
fn attach_panel(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let outcome = self.dispatch(ShellAction::AttachPanel(pane));
|
||||
if outcome.changed {
|
||||
self.resize_terminal_for_workspace(window, cx);
|
||||
}
|
||||
window.focus(&self.root_focus);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn add_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(pane) = self.model.next_detached_panel() else {
|
||||
return;
|
||||
};
|
||||
self.attach_panel(pane, window, cx);
|
||||
}
|
||||
|
||||
fn detach_panel(&mut self, pane: PaneId, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let outcome = self.dispatch(ShellAction::DetachPanel(pane));
|
||||
if outcome.changed {
|
||||
self.resize_terminal_for_workspace(window, cx);
|
||||
}
|
||||
window.focus(&self.root_focus);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn add_panel_action(&mut self, _: &AddPanel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.add_panel(window, cx);
|
||||
}
|
||||
|
||||
fn detach_selected_panel(
|
||||
&mut self,
|
||||
_: &DetachSelectedPanel,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.detach_panel(self.model.selected_pane(), window, cx);
|
||||
}
|
||||
|
||||
fn move_focus(
|
||||
&mut self,
|
||||
direction: FocusDirection,
|
||||
@@ -641,10 +695,9 @@ impl LumbridgeShell {
|
||||
return;
|
||||
}
|
||||
let modifiers = key_modifiers(event);
|
||||
if let Some(scroll) = terminal_scroll_from_parts(
|
||||
event.keystroke.key.as_str(),
|
||||
modifiers,
|
||||
) {
|
||||
if let Some(scroll) =
|
||||
terminal_scroll_from_parts(event.keystroke.key.as_str(), modifiers)
|
||||
{
|
||||
self.scroll_terminal(scroll, cx);
|
||||
return;
|
||||
}
|
||||
@@ -768,7 +821,14 @@ impl LumbridgeShell {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn pane_context(&self, pane: &PaneState, selected: bool) -> gpui::AnyElement {
|
||||
fn pane_context(
|
||||
&self,
|
||||
pane: &PaneState,
|
||||
selected: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::AnyElement {
|
||||
let pane_id = pane.id();
|
||||
let can_detach = self.model.attached_panel_count() > 1;
|
||||
let external = pane.output_source() == OutputSource::External;
|
||||
let status = if external {
|
||||
self.runtime_status.badge()
|
||||
@@ -859,16 +919,44 @@ impl LumbridgeShell {
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_end()
|
||||
.flex_none()
|
||||
.text_xs()
|
||||
.text_color(rgb(if pane.needs_input() {
|
||||
ATTENTION
|
||||
} else if selected {
|
||||
SUCCESS
|
||||
} else {
|
||||
MUTED
|
||||
}))
|
||||
.child(surface_status),
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(rgb(if pane.needs_input() {
|
||||
ATTENTION
|
||||
} else if selected {
|
||||
SUCCESS
|
||||
} else {
|
||||
MUTED
|
||||
}))
|
||||
.child(surface_status),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id(("detach-panel", pane_id.index()))
|
||||
.cursor_pointer()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded(px(4.0))
|
||||
.border_1()
|
||||
.border_color(rgb(BORDER))
|
||||
.text_xs()
|
||||
.text_color(rgb(if can_detach { MUTED } else { BORDER }))
|
||||
.child(if can_detach {
|
||||
"− DETACH"
|
||||
} else {
|
||||
"LAST PANEL"
|
||||
})
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
shell.detach_panel(pane_id, window, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -990,18 +1078,10 @@ impl LumbridgeShell {
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.text_xs()
|
||||
.child(div().text_color(rgb(MUTED)).child("DECISION SHELF"))
|
||||
.child(
|
||||
div()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("DECISION SHELF"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(rgb(if pane.needs_input() {
|
||||
ATTENTION
|
||||
} else {
|
||||
MUTED
|
||||
}))
|
||||
.text_color(rgb(if pane.needs_input() { ATTENTION } else { MUTED }))
|
||||
.child(if pane.needs_input() {
|
||||
"REVIEW REQUIRED"
|
||||
} else {
|
||||
@@ -1009,16 +1089,9 @@ impl LumbridgeShell {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.mt_2()
|
||||
.children(choices.map(|(label, detail, attention)| {
|
||||
choice(label, detail, attention)
|
||||
})),
|
||||
)
|
||||
.child(div().flex().flex_col().gap_1().mt_2().children(
|
||||
choices.map(|(label, detail, attention)| choice(label, detail, attention)),
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -1048,7 +1121,7 @@ impl LumbridgeShell {
|
||||
.flex_none()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.child(self.pane_context(pane, selected)),
|
||||
.child(self.pane_context(pane, selected, cx)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -1128,7 +1201,26 @@ impl LumbridgeShell {
|
||||
.px_3()
|
||||
.py_2()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("Open attention request"),
|
||||
.child("Add workspace panel")
|
||||
.child(
|
||||
div()
|
||||
.mt_1()
|
||||
.text_xs()
|
||||
.child("Alt+Shift+N · reattach if detached"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("Detach selected panel")
|
||||
.child(
|
||||
div()
|
||||
.mt_1()
|
||||
.text_xs()
|
||||
.child("Alt+Shift+W · session keeps running"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -1158,7 +1250,17 @@ impl LumbridgeShell {
|
||||
impl Render for LumbridgeShell {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let attention = self.model.pane(PaneId::ClaudeUi);
|
||||
let panel_count = visible_panel_count(window.bounds().size);
|
||||
let panel_capacity = visible_panel_count(window.bounds().size);
|
||||
let attached_panes = self.model.attached_pane_ids();
|
||||
let attached_count = attached_panes.len();
|
||||
let detached_entries = self
|
||||
.model
|
||||
.detached_pane_ids()
|
||||
.into_iter()
|
||||
.map(|pane| (pane, self.model.pane(pane).fixture().title))
|
||||
.collect::<Vec<_>>();
|
||||
let detached_count = detached_entries.len();
|
||||
let visible_count = panel_capacity.min(attached_count).max(1);
|
||||
let sidebar = div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -1263,6 +1365,33 @@ impl Render for LumbridgeShell {
|
||||
.text_color(rgb(MUTED))
|
||||
.child("spark-1 · sleeping"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.mt_3()
|
||||
.px_4()
|
||||
.py_2()
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(format!("DETACHED SESSIONS · {detached_count}")),
|
||||
)
|
||||
.children(detached_entries.into_iter().map(|(pane, title)| {
|
||||
div()
|
||||
.id(("detached-session", pane.index()))
|
||||
.cursor_pointer()
|
||||
.mx_2()
|
||||
.mb_1()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.rounded(px(4.0))
|
||||
.border_1()
|
||||
.border_color(rgb(BORDER_QUIET))
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(format!("↪ {title}"))
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
shell.attach_panel(pane, window, cx);
|
||||
}))
|
||||
}))
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
div()
|
||||
@@ -1317,33 +1446,44 @@ impl Render for LumbridgeShell {
|
||||
.child("Review"),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(div().px_3().text_xs().text_color(rgb(MUTED)).child(format!(
|
||||
"{visible_count} shown · {attached_count} attached · {detached_count} detached"
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.id("add-workspace-panel")
|
||||
.cursor_pointer()
|
||||
.ml_2()
|
||||
.px_3()
|
||||
.py_1()
|
||||
.rounded(px(4.0))
|
||||
.border_1()
|
||||
.border_color(rgb(if detached_count > 0 { ACCENT } else { BORDER }))
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(format!(
|
||||
"{panel_count} visible panels · 1 interactive VT · 1 waiting"
|
||||
)),
|
||||
.text_color(rgb(if detached_count > 0 { ACCENT } else { MUTED }))
|
||||
.child(if detached_count > 0 {
|
||||
"+ ADD PANEL"
|
||||
} else {
|
||||
"ALL PANELS ATTACHED"
|
||||
})
|
||||
.on_click(cx.listener(|shell, _, window, cx| {
|
||||
shell.add_panel(window, cx);
|
||||
})),
|
||||
);
|
||||
|
||||
let panel_range = visible_panel_range(
|
||||
PaneId::ALL.len(),
|
||||
self.model.selected_pane().index(),
|
||||
panel_count,
|
||||
);
|
||||
let selected_position = attached_panes
|
||||
.iter()
|
||||
.position(|pane| *pane == self.model.selected_pane())
|
||||
.expect("the selected pane must remain attached");
|
||||
let panel_range = visible_panel_range(attached_count, selected_position, visible_count);
|
||||
let workspace_panels = panel_range
|
||||
.map(|index| {
|
||||
let pane = self.model.pane(PaneId::ALL[index]);
|
||||
let pane = self.model.pane(attached_panes[index]);
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.child(self.workspace_panel(
|
||||
pane,
|
||||
pane.id() == self.model.selected_pane(),
|
||||
cx,
|
||||
))
|
||||
.child(self.workspace_panel(pane, pane.id() == self.model.selected_pane(), cx))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let workspace_row = div()
|
||||
@@ -1380,6 +1520,8 @@ impl Render for LumbridgeShell {
|
||||
.on_action(cx.listener(Self::focus_down))
|
||||
.on_action(cx.listener(Self::open_palette))
|
||||
.on_action(cx.listener(Self::close_palette))
|
||||
.on_action(cx.listener(Self::add_panel_action))
|
||||
.on_action(cx.listener(Self::detach_selected_panel))
|
||||
.on_action(cx.listener(Self::terminal_taller))
|
||||
.on_action(cx.listener(Self::terminal_shorter))
|
||||
.on_action(cx.listener(Self::terminal_wider))
|
||||
@@ -1570,6 +1712,8 @@ fn main() {
|
||||
KeyBinding::new("alt-4", SelectPane4, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-5", SelectPane5, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-6", SelectPane6, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-shift-n", AddPanel, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-shift-w", DetachSelectedPanel, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-shift-up", TerminalTaller, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-shift-down", TerminalShorter, Some("LumbridgeShell")),
|
||||
KeyBinding::new("alt-shift-right", TerminalWider, Some("LumbridgeShell")),
|
||||
@@ -1626,15 +1770,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn terminal_geometry_tracks_middle_sixty_percent_per_panel() {
|
||||
let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0)));
|
||||
let dimensions = terminal_dimensions_for_window(size(px(1500.0), px(960.0)), 5);
|
||||
assert_eq!(dimensions.rows(), 26);
|
||||
assert_eq!(dimensions.columns(), 45);
|
||||
|
||||
let ultrawide = size(px(3440.0), px(1440.0));
|
||||
assert_eq!(visible_panel_count(ultrawide), 5);
|
||||
let dimensions = terminal_dimensions_for_window(ultrawide);
|
||||
let dimensions = terminal_dimensions_for_window(ultrawide, 5);
|
||||
assert_eq!(dimensions.rows(), 42);
|
||||
assert_eq!(dimensions.columns(), 71);
|
||||
|
||||
let two_panels = terminal_dimensions_for_window(ultrawide, 2);
|
||||
assert_eq!(two_panels.rows(), 42);
|
||||
assert_eq!(two_panels.columns(), 185);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user