aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-17 01:41:16 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-17 01:41:16 +0300
commita36da363f14639085bec91d7a378d6c2eeaa6c58 (patch)
tree2c86155b115f8c79db2b8826fd8b2199d153f7bf /src/app/input
parente4de58b37f58d59d146158239ed218a8accd5c3c (diff)
refactor: extract terminal state and runtime from panes
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/mod.rs21
-rw-r--r--src/app/input/modal.rs29
-rw-r--r--src/app/input/mouse.rs67
-rw-r--r--src/app/input/navigate.rs37
-rw-r--r--src/app/input/sidebar.rs183
-rw-r--r--src/app/input/terminal.rs58
6 files changed, 246 insertions, 149 deletions
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs
index 575b373d..fea88b55 100644
--- a/src/app/input/mod.rs
+++ b/src/app/input/mod.rs
@@ -82,8 +82,8 @@ impl App {
if self.state.mode != Mode::Terminal {
return;
}
- if let Some(ws) = self.state.active.and_then(|i| self.state.workspaces.get(i)) {
- if let Some(rt) = ws.focused_runtime() {
+ if let Some(ws_idx) = self.state.active {
+ if let Some(rt) = self.state.focused_runtime_in_workspace(ws_idx) {
let _ = rt.send_paste(text).await;
}
}
@@ -197,11 +197,17 @@ impl AppState {
let cwd = self
.active
.and_then(|i| self.workspaces.get(i))
- .and_then(|ws| ws.focused_runtime())
- .and_then(|rt| rt.cwd());
+ .and_then(|ws| {
+ let tab = ws.active_tab()?;
+ tab.cwd_for_pane(
+ tab.layout.focused(),
+ &self.terminals,
+ &self.terminal_runtimes,
+ )
+ });
if let Some(ws) = self.active.and_then(|i| self.workspaces.get_mut(i)) {
- if let Ok(new_id) = ws.split_focused(
+ if let Ok(new_pane) = ws.split_focused(
direction,
new_rows,
new_cols,
@@ -209,6 +215,9 @@ impl AppState {
self.pane_scrollback_limit_bytes,
self.host_terminal_theme,
) {
+ let new_id = new_pane.pane_id;
+ self.terminals
+ .insert(new_pane.terminal.id.clone(), new_pane.terminal);
ws.layout.focus_pane(new_id);
self.mark_session_dirty();
self.mode = Mode::Terminal;
@@ -277,6 +286,8 @@ fn numbered_lines_bytes(count: usize) -> Vec<u8> {
fn capture_snapshot(state: &AppState) -> crate::persist::SessionSnapshot {
crate::persist::capture(
&state.workspaces,
+ &state.terminals,
+ &state.terminal_runtimes,
state.active,
state.selected,
state.agent_panel_scope,
diff --git a/src/app/input/modal.rs b/src/app/input/modal.rs
index 367e7e84..6de349cb 100644
--- a/src/app/input/modal.rs
+++ b/src/app/input/modal.rs
@@ -190,11 +190,14 @@ pub(super) fn open_rename_pane(state: &mut AppState, pane_id: crate::layout::Pan
let Some(pane) = ws.pane_state(pane_id) else {
return;
};
+ let terminal = state.terminals.get(&pane.attached_terminal_id);
state.creating_new_tab = false;
state.requested_new_tab_name = None;
state.rename_pane_target = Some(pane_id);
- state.name_input = pane.manual_label.clone().unwrap_or_default();
- state.name_input_replace_on_type = pane.manual_label.is_none();
+ state.name_input = terminal
+ .and_then(|t| t.manual_label.clone())
+ .unwrap_or_default();
+ state.name_input_replace_on_type = terminal.and_then(|t| t.manual_label.as_ref()).is_none();
state.mode = Mode::RenamePane;
}
@@ -316,10 +319,13 @@ pub(super) fn apply_rename_action(state: &mut AppState, action: ModalAction) {
Mode::RenamePane => {
if let (Some(ws_idx), Some(pane_id)) = (state.active, state.rename_pane_target)
{
- if let Some(ws) = state.workspaces.get_mut(ws_idx) {
- if let Some(pane) = ws.pane_state_mut(pane_id) {
- pane.set_manual_label(new_name);
- state.mark_session_dirty();
+ if let Some(ws) = state.workspaces.get(ws_idx) {
+ if let Some(pane) = ws.pane_state(pane_id) {
+ let terminal_id = pane.attached_terminal_id.clone();
+ if let Some(terminal) = state.terminals.get_mut(&terminal_id) {
+ terminal.set_manual_label(new_name);
+ state.mark_session_dirty();
+ }
}
}
}
@@ -468,10 +474,13 @@ pub(super) fn apply_context_menu_action(state: &mut AppState, menu: ContextMenuS
}
(ContextMenuKind::Pane { pane_id, .. }, Some("Clear pane name")) => {
if let Some(ws_idx) = state.active {
- if let Some(ws) = state.workspaces.get_mut(ws_idx) {
- if let Some(pane) = ws.pane_state_mut(pane_id) {
- pane.clear_manual_label();
- state.mark_session_dirty();
+ if let Some(ws) = state.workspaces.get(ws_idx) {
+ if let Some(pane) = ws.pane_state(pane_id) {
+ let terminal_id = pane.attached_terminal_id.clone();
+ if let Some(terminal) = state.terminals.get_mut(&terminal_id) {
+ terminal.clear_manual_label();
+ state.mark_session_dirty();
+ }
}
}
}
diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs
index a7a2159a..10ef9c18 100644
--- a/src/app/input/mouse.rs
+++ b/src/app/input/mouse.rs
@@ -712,7 +712,8 @@ impl AppState {
.active
.and_then(|ws_idx| self.workspaces.get(ws_idx))
.and_then(|ws| ws.pane_state(info.id))
- .and_then(|pane| pane.manual_label.as_ref())
+ .and_then(|pane| self.terminals.get(&pane.attached_terminal_id))
+ .and_then(|terminal| terminal.manual_label.as_ref())
.is_some();
self.context_menu = Some(ContextMenuState {
kind: ContextMenuKind::Pane {
@@ -1061,16 +1062,16 @@ impl AppState {
}
pub(super) fn scroll_pane_up(&self, pane_id: crate::layout::PaneId, lines: usize) {
- if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) {
- if let Some(rt) = ws.runtimes.get(&pane_id) {
+ if let Some(ws_idx) = self.active {
+ if let Some(rt) = self.runtime_for_pane_in_workspace(ws_idx, pane_id) {
rt.scroll_up(lines);
}
}
}
pub(super) fn scroll_pane_down(&self, pane_id: crate::layout::PaneId, lines: usize) {
- if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) {
- if let Some(rt) = ws.runtimes.get(&pane_id) {
+ if let Some(ws_idx) = self.active {
+ if let Some(rt) = self.runtime_for_pane_in_workspace(ws_idx, pane_id) {
rt.scroll_down(lines);
}
}
@@ -1081,9 +1082,8 @@ impl AppState {
pane_id: crate::layout::PaneId,
) -> Option<crate::pane::ScrollMetrics> {
self.active
- .and_then(|i| self.workspaces.get(i))
- .and_then(|ws| ws.runtime(pane_id))
- .and_then(crate::pane::PaneRuntime::scroll_metrics)
+ .and_then(|i| self.runtime_for_pane_in_workspace(i, pane_id))
+ .and_then(crate::terminal::TerminalRuntime::scroll_metrics)
}
pub(super) fn handle_terminal_wheel(&mut self, mouse: MouseEvent) {
@@ -1112,8 +1112,8 @@ impl AppState {
return;
}
- if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) {
- if let Some(rt) = ws.focused_runtime() {
+ if let Some(ws_idx) = self.active {
+ if let Some(rt) = self.focused_runtime_in_workspace(ws_idx) {
match mouse.kind {
MouseEventKind::ScrollUp => rt.scroll_up(LINES_PER_NOTCH),
MouseEventKind::ScrollDown => rt.scroll_down(LINES_PER_NOTCH),
@@ -1124,10 +1124,10 @@ impl AppState {
}
pub(super) fn forward_pane_mouse_button(&self, info: &PaneInfo, mouse: MouseEvent) -> bool {
- let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) else {
+ let Some(ws_idx) = self.active else {
return false;
};
- let Some(rt) = ws.runtimes.get(&info.id) else {
+ let Some(rt) = self.runtime_for_pane_in_workspace(ws_idx, info.id) else {
return false;
};
let column = mouse.column.saturating_sub(info.inner_rect.x);
@@ -1169,10 +1169,10 @@ impl AppState {
}
pub(super) fn forward_pane_wheel(&self, info: &PaneInfo, mouse: MouseEvent) -> bool {
- let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) else {
+ let Some(ws_idx) = self.active else {
return false;
};
- let Some(rt) = ws.runtimes.get(&info.id) else {
+ let Some(rt) = self.runtime_for_pane_in_workspace(ws_idx, info.id) else {
return false;
};
match rt.wheel_routing() {
@@ -1209,8 +1209,8 @@ impl AppState {
pane_id: crate::layout::PaneId,
offset_from_bottom: usize,
) {
- if let Some(ws) = self.active.and_then(|i| self.workspaces.get(i)) {
- if let Some(rt) = ws.runtimes.get(&pane_id) {
+ if let Some(ws_idx) = self.active {
+ if let Some(rt) = self.runtime_for_pane_in_workspace(ws_idx, pane_id) {
rt.set_scroll_offset_from_bottom(offset_from_bottom);
}
}
@@ -1221,7 +1221,7 @@ impl AppState {
col: u16,
row: u16,
) -> Option<(crate::layout::PaneId, ScrollbarClickTarget)> {
- let ws = self.active.and_then(|i| self.workspaces.get(i))?;
+ let ws_idx = self.active?;
let info = self.view.pane_infos.iter().find(|info| {
crate::ui::pane_scrollbar_rect(info).is_some_and(|track| {
col >= track.x
@@ -1230,7 +1230,7 @@ impl AppState {
&& row < track.y + track.height
})
})?;
- let rt = ws.runtimes.get(&info.id)?;
+ let rt = self.runtime_for_pane_in_workspace(ws_idx, info.id)?;
let metrics = rt.scroll_metrics()?;
if metrics.max_offset_from_bottom == 0 {
return None;
@@ -1254,14 +1254,14 @@ impl AppState {
row: u16,
grab_row_offset: u16,
) -> Option<usize> {
- let ws = self.active.and_then(|i| self.workspaces.get(i))?;
+ let ws_idx = self.active?;
let info = self
.view
.pane_infos
.iter()
.find(|info| info.id == pane_id)?;
let track = crate::ui::pane_scrollbar_rect(info)?;
- let rt = ws.runtimes.get(&pane_id)?;
+ let rt = self.runtime_for_pane_in_workspace(ws_idx, pane_id)?;
let metrics = rt.scroll_metrics()?;
if metrics.max_offset_from_bottom == 0 {
return None;
@@ -1345,12 +1345,19 @@ mod tests {
background.tabs[0].layout.focus_pane(first_pane);
app.state.workspaces = vec![active, background];
+ app.state.ensure_test_terminals();
app.state.active = Some(0);
app.state.selected = 0;
app.state.toast_config.delivery = crate::config::ToastDelivery::Herdr;
- app.state.workspaces[1]
+ let target_terminal_id = app.state.workspaces[1]
.panes
- .get_mut(&target_pane)
+ .get(&target_pane)
+ .unwrap()
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&target_terminal_id)
.unwrap()
.state = AgentState::Working;
@@ -1606,17 +1613,17 @@ mod tests {
.expect("second pane info")
.clone();
- app.state.workspaces[0].tabs[0].runtimes.insert(
+ app.state.insert_test_runtime(
first_pane,
- crate::pane::PaneRuntime::test_with_screen_bytes(
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(
first_info.inner_rect.width.max(1),
first_info.inner_rect.height.max(1),
b"\x1b[?1002h",
),
);
- app.state.workspaces[0].tabs[0].runtimes.insert(
+ app.state.insert_test_runtime(
second_pane,
- crate::pane::PaneRuntime::test_with_screen_bytes(
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(
second_info.inner_rect.width.max(1),
second_info.inner_rect.height.max(1),
b"\x1b[?1002h",
@@ -1675,17 +1682,17 @@ mod tests {
.expect("second pane info")
.clone();
- app.state.workspaces[0].tabs[0].runtimes.insert(
+ app.state.insert_test_runtime(
first_pane,
- crate::pane::PaneRuntime::test_with_screen_bytes(
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(
first_info.inner_rect.width.max(1),
first_info.inner_rect.height.max(1),
b"\x1b[?1002h",
),
);
- app.state.workspaces[0].tabs[0].runtimes.insert(
+ app.state.insert_test_runtime(
second_pane,
- crate::pane::PaneRuntime::test_with_screen_bytes(
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(
second_info.inner_rect.width.max(1),
second_info.inner_rect.height.max(1),
b"\x1b[?1002h",
diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs
index f5a0bcaa..9fc11605 100644
--- a/src/app/input/navigate.rs
+++ b/src/app/input/navigate.rs
@@ -126,10 +126,10 @@ impl App {
}
fn pass_through_key_to_focused_pane(&mut self, key: TerminalKey) -> bool {
- let Some(ws) = self.state.active.and_then(|i| self.state.workspaces.get(i)) else {
+ let Some(ws_idx) = self.state.active else {
return false;
};
- let Some(rt) = ws.focused_runtime() else {
+ let Some(rt) = self.state.focused_runtime_in_workspace(ws_idx) else {
return false;
};
@@ -191,10 +191,13 @@ impl App {
if let Some(public_pane_id) = self.public_pane_id(ws_idx, pane_id) {
env.push(("HERDR_ACTIVE_PANE_ID".to_string(), public_pane_id));
}
- if let Some(pane_cwd) = workspace
- .active_tab()
- .and_then(|tab| tab.cwd_for_pane(pane_id))
- {
+ if let Some(pane_cwd) = workspace.active_tab().and_then(|tab| {
+ tab.cwd_for_pane(
+ pane_id,
+ &self.state.terminals,
+ &self.state.terminal_runtimes,
+ )
+ }) {
env.push((
"HERDR_ACTIVE_PANE_CWD".to_string(),
pane_cwd.display().to_string(),
@@ -308,10 +311,14 @@ impl App {
.focused_pane_id()
.ok_or_else(|| std::io::Error::other("no focused pane"))?;
let previous_zoomed = ws.active_tab().map(|tab| tab.zoomed).unwrap_or(false);
- let cwd = ws
- .active_tab()
- .and_then(|tab| tab.cwd_for_pane(previous_focus));
- let new_pane_id = ws.split_focused_command(
+ let cwd = ws.active_tab().and_then(|tab| {
+ tab.cwd_for_pane(
+ previous_focus,
+ &self.state.terminals,
+ &self.state.terminal_runtimes,
+ )
+ });
+ let new_pane = ws.split_focused_command(
Direction::Horizontal,
new_rows,
new_cols,
@@ -321,6 +328,10 @@ impl App {
self.state.pane_scrollback_limit_bytes,
self.state.host_terminal_theme,
)?;
+ let new_pane_id = new_pane.pane_id;
+ self.state
+ .terminals
+ .insert(new_pane.terminal.id.clone(), new_pane.terminal);
ws.active_tab_mut()
.expect("workspace must have an active tab")
.layout
@@ -1072,7 +1083,7 @@ mod tests {
api_rx,
crate::api::EventHub::default(),
);
- let workspace = Workspace::new(
+ let (workspace, terminal, runtime) = Workspace::new(
std::env::current_dir().unwrap_or_else(|_| "/".into()),
24,
80,
@@ -1084,6 +1095,10 @@ mod tests {
)
.expect("workspace should spawn");
app.state.workspaces = vec![workspace];
+ app.state
+ .terminal_runtimes
+ .insert(terminal.id.clone(), runtime);
+ app.state.terminals.insert(terminal.id.clone(), terminal);
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;
diff --git a/src/app/input/sidebar.rs b/src/app/input/sidebar.rs
index 8f6c7792..e8e1e094 100644
--- a/src/app/input/sidebar.rs
+++ b/src/app/input/sidebar.rs
@@ -351,7 +351,7 @@ impl AppState {
let ws_idx = self.collapsed_detail_workspace_idx()?;
let ws = self.workspaces.get(ws_idx)?;
let detail_idx = (row - detail_content_area.y) as usize;
- let details = ws.pane_details();
+ let details = ws.pane_details(&self.terminals);
let detail = details.get(detail_idx)?;
Some((ws_idx, detail.tab_idx, detail.pane_id))
}
@@ -633,19 +633,26 @@ mod tests {
let mut ws = Workspace::test_new("test");
ws.tabs[0].set_custom_name("main".into());
let first_pane = ws.tabs[0].root_pane;
- ws.tabs[0]
- .panes
- .get_mut(&first_pane)
+ let first_tab = ws.test_add_tab(Some("logs"));
+ let second_pane = ws.tabs[first_tab].root_pane;
+ app.state.workspaces = vec![ws];
+ app.state.ensure_test_terminals();
+ let first_terminal_id = app.state.workspaces[0].tabs[0].panes[&first_pane]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&first_terminal_id)
.unwrap()
.detected_agent = Some(Agent::Pi);
- let second_tab = ws.test_add_tab(Some("logs"));
- let second_pane = ws.tabs[second_tab].root_pane;
- ws.tabs[second_tab]
- .panes
- .get_mut(&second_pane)
+ let second_terminal_id = app.state.workspaces[0].tabs[first_tab].panes[&second_pane]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&second_terminal_id)
.unwrap()
.detected_agent = Some(Agent::Claude);
- app.state.workspaces = vec![ws];
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;
@@ -659,9 +666,9 @@ mod tests {
);
assert_eq!(app.state.mode, Mode::Terminal);
let snapshot = capture_snapshot(&app.state);
- assert_eq!(snapshot.workspaces[0].active_tab, second_tab);
+ assert_eq!(snapshot.workspaces[0].active_tab, first_tab);
assert_eq!(
- snapshot.workspaces[0].tabs[second_tab].focused,
+ snapshot.workspaces[0].tabs[first_tab].focused,
Some(second_pane.raw())
);
}
@@ -701,23 +708,30 @@ mod tests {
#[test]
fn clicking_all_workspaces_agent_row_switches_to_correct_workspace() {
let mut app = app_for_mouse_test();
- let mut first = Workspace::test_new("one");
+ let first = Workspace::test_new("one");
let first_pane = first.tabs[0].root_pane;
- first.tabs[0]
- .panes
- .get_mut(&first_pane)
- .unwrap()
- .detected_agent = Some(Agent::Pi);
- let mut second = Workspace::test_new("two");
+ let second = Workspace::test_new("two");
let second_pane = second.tabs[0].root_pane;
- second.tabs[0]
- .panes
- .get_mut(&second_pane)
- .unwrap()
- .detected_agent = Some(Agent::Claude);
app.state.workspaces = vec![first, second];
+ app.state.ensure_test_terminals();
+ let first_terminal_id = app.state.workspaces[0].tabs[0].panes[&first_pane]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&first_terminal_id)
+ .unwrap()
+ .detected_agent = Some(Agent::Pi);
+ let second_terminal_id = app.state.workspaces[1].tabs[0].panes[&second_pane]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&second_terminal_id)
+ .unwrap()
+ .detected_agent = Some(Agent::Claude);
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;
@@ -747,12 +761,8 @@ mod tests {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
let first_pane = ws.tabs[0].root_pane;
- ws.tabs[0]
- .panes
- .get_mut(&first_pane)
- .unwrap()
- .detected_agent = Some(Agent::Pi);
+ let mut tabs = Vec::new();
for (tab_name, agent) in [
("logs", Agent::Claude),
("review", Agent::Codex),
@@ -760,14 +770,29 @@ mod tests {
] {
let tab_idx = ws.test_add_tab(Some(tab_name));
let pane_id = ws.tabs[tab_idx].root_pane;
- ws.tabs[tab_idx]
- .panes
- .get_mut(&pane_id)
- .unwrap()
- .detected_agent = Some(agent);
+ tabs.push((tab_idx, pane_id, agent));
}
app.state.workspaces = vec![ws];
+ app.state.ensure_test_terminals();
+ let first_terminal_id = app.state.workspaces[0].tabs[0].panes[&first_pane]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&first_terminal_id)
+ .unwrap()
+ .detected_agent = Some(Agent::Pi);
+ for (tab_idx, pane_id, agent) in tabs {
+ let terminal_id = app.state.workspaces[0].tabs[tab_idx].panes[&pane_id]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&terminal_id)
+ .unwrap()
+ .detected_agent = Some(agent);
+ }
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;
@@ -792,31 +817,43 @@ mod tests {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
let first_pane = ws.tabs[0].root_pane;
- ws.tabs[0]
- .panes
- .get_mut(&first_pane)
- .unwrap()
- .detected_agent = Some(Agent::Pi);
-
let second_tab = ws.test_add_tab(Some("logs"));
let second_pane = ws.tabs[second_tab].root_pane;
- ws.tabs[second_tab]
- .panes
- .get_mut(&second_pane)
- .unwrap()
- .detected_agent = Some(Agent::Claude);
-
+ let mut extra_tabs = Vec::new();
for (tab_name, agent) in [("review", Agent::Codex), ("ops", Agent::Gemini)] {
let tab_idx = ws.test_add_tab(Some(tab_name));
let pane_id = ws.tabs[tab_idx].root_pane;
- ws.tabs[tab_idx]
- .panes
- .get_mut(&pane_id)
- .unwrap()
- .detected_agent = Some(agent);
+ extra_tabs.push((tab_idx, pane_id, agent));
}
app.state.workspaces = vec![ws];
+ app.state.ensure_test_terminals();
+ let first_terminal_id = app.state.workspaces[0].tabs[0].panes[&first_pane]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&first_terminal_id)
+ .unwrap()
+ .detected_agent = Some(Agent::Pi);
+ let second_terminal_id = app.state.workspaces[0].tabs[second_tab].panes[&second_pane]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&second_terminal_id)
+ .unwrap()
+ .detected_agent = Some(Agent::Claude);
+ for (tab_idx, pane_id, agent) in extra_tabs {
+ let terminal_id = app.state.workspaces[0].tabs[tab_idx].panes[&pane_id]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&terminal_id)
+ .unwrap()
+ .detected_agent = Some(agent);
+ }
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;
@@ -843,19 +880,26 @@ mod tests {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
let first_pane = ws.tabs[0].root_pane;
- ws.tabs[0]
- .panes
- .get_mut(&first_pane)
- .unwrap()
- .detected_agent = Some(Agent::Pi);
let second_tab = ws.test_add_tab(Some("logs"));
let second_pane = ws.tabs[second_tab].root_pane;
- ws.tabs[second_tab]
- .panes
- .get_mut(&second_pane)
+ app.state.workspaces = vec![ws];
+ app.state.ensure_test_terminals();
+ let first_terminal_id = app.state.workspaces[0].tabs[0].panes[&first_pane]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&first_terminal_id)
+ .unwrap()
+ .detected_agent = Some(Agent::Pi);
+ let second_terminal_id = app.state.workspaces[0].tabs[second_tab].panes[&second_pane]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&second_terminal_id)
.unwrap()
.detected_agent = Some(Agent::Claude);
- app.state.workspaces = vec![ws];
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Terminal;
@@ -1128,20 +1172,27 @@ mod tests {
let mut first = Workspace::test_new("a");
let first_root = first.tabs[0].root_pane;
first.identity_cwd = first_repo.clone();
- first.tabs[0]
- .pane_cwds
- .insert(first_root, first_repo.clone());
first.refresh_git_ahead_behind();
let mut second = Workspace::test_new("b");
let second_root = second.tabs[0].root_pane;
second.identity_cwd = second_repo.clone();
- second.tabs[0]
- .pane_cwds
- .insert(second_root, second_repo.clone());
second.refresh_git_ahead_behind();
app.state.workspaces = vec![first, second];
+ app.state.ensure_test_terminals();
+ let first_terminal_id = app.state.workspaces[0].tabs[0].panes[&first_root]
+ .attached_terminal_id
+ .clone();
+ app.state.terminals.get_mut(&first_terminal_id).unwrap().cwd = first_repo.clone();
+ let second_terminal_id = app.state.workspaces[1].tabs[0].panes[&second_root]
+ .attached_terminal_id
+ .clone();
+ app.state
+ .terminals
+ .get_mut(&second_terminal_id)
+ .unwrap()
+ .cwd = second_repo.clone();
crate::ui::compute_view(&mut app.state, Rect::new(0, 0, 106, 20));
assert_eq!(app.state.workspace_drop_index_at_row(0), Some(0));
diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs
index 22cf3169..3f0bff2c 100644
--- a/src/app/input/terminal.rs
+++ b/src/app/input/terminal.rs
@@ -64,7 +64,7 @@ impl App {
let ws_idx = self.state.active?;
let ws = self.state.workspaces.get(ws_idx)?;
let pane_id = ws.focused_pane_id()?;
- let rt = ws.runtimes.get(&pane_id)?;
+ let rt = self.state.runtime_for_pane_in_workspace(ws_idx, pane_id)?;
// Intercept plain PageUp/PageDown presses for pane scrollback when the
// focused pane doesn't handle its own scrolling (e.g., a plain shell
@@ -179,9 +179,9 @@ mod tests {
let pane_id = ws.tabs[0].root_pane;
let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
let info = pane_infos[0].clone();
- ws.tabs[0].runtimes.insert(
+ ws.insert_test_runtime(
pane_id,
- crate::pane::PaneRuntime::test_with_scrollback_bytes(
+ crate::terminal::TerminalRuntime::test_with_scrollback_bytes(
info.inner_rect.width,
info.inner_rect.height,
16 * 1024,
@@ -195,9 +195,10 @@ mod tests {
app.state.mode = Mode::Terminal;
app.state.view.pane_infos = pane_infos;
- let start_metrics = app.state.workspaces[0]
- .runtime(pane_id)
- .and_then(crate::pane::PaneRuntime::scroll_metrics)
+ let start_metrics = app
+ .state
+ .runtime_for_pane(pane_id)
+ .and_then(crate::terminal::TerminalRuntime::scroll_metrics)
.expect("initial scroll metrics");
let start_row = info.inner_rect.y;
let start_col = info.inner_rect.x + 2;
@@ -213,9 +214,10 @@ mod tests {
info.inner_rect.y.saturating_sub(1),
));
- let end_metrics = app.state.workspaces[0]
- .runtime(pane_id)
- .and_then(crate::pane::PaneRuntime::scroll_metrics)
+ let end_metrics = app
+ .state
+ .runtime_for_pane(pane_id)
+ .and_then(crate::terminal::TerminalRuntime::scroll_metrics)
.expect("scroll metrics after drag");
assert_eq!(
end_metrics.offset_from_bottom,
@@ -243,9 +245,9 @@ mod tests {
let pane_id = ws.tabs[0].root_pane;
let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
let info = pane_infos[0].clone();
- ws.tabs[0].runtimes.insert(
+ ws.insert_test_runtime(
pane_id,
- crate::pane::PaneRuntime::test_with_scrollback_bytes(
+ crate::terminal::TerminalRuntime::test_with_scrollback_bytes(
info.inner_rect.width,
info.inner_rect.height,
16 * 1024,
@@ -283,9 +285,9 @@ mod tests {
let pane_id = ws.tabs[0].root_pane;
let pane_infos = ws.tabs[0].layout.panes(Rect::new(26, 2, 80, 18));
let info = pane_infos[0].clone();
- ws.tabs[0].runtimes.insert(
+ ws.insert_test_runtime(
pane_id,
- crate::pane::PaneRuntime::test_with_scrollback_bytes(
+ crate::terminal::TerminalRuntime::test_with_scrollback_bytes(
info.inner_rect.width,
info.inner_rect.height,
16 * 1024,
@@ -299,9 +301,10 @@ mod tests {
app.state.mode = Mode::Terminal;
app.state.view.pane_infos = pane_infos;
- let start_metrics = app.state.workspaces[0]
- .runtime(pane_id)
- .and_then(crate::pane::PaneRuntime::scroll_metrics)
+ let start_metrics = app
+ .state
+ .runtime_for_pane(pane_id)
+ .and_then(crate::terminal::TerminalRuntime::scroll_metrics)
.expect("initial scroll metrics");
let top_row = info.inner_rect.y;
let col = info.inner_rect.x + 2;
@@ -309,9 +312,10 @@ mod tests {
app.handle_mouse(mouse(MouseEventKind::Down(MouseButton::Left), col, top_row));
app.handle_mouse(mouse(MouseEventKind::ScrollUp, col, top_row));
- let end_metrics = app.state.workspaces[0]
- .runtime(pane_id)
- .and_then(crate::pane::PaneRuntime::scroll_metrics)
+ let end_metrics = app
+ .state
+ .runtime_for_pane(pane_id)
+ .and_then(crate::terminal::TerminalRuntime::scroll_metrics)
.expect("scroll metrics after wheel");
assert_eq!(
end_metrics.offset_from_bottom,
@@ -352,17 +356,17 @@ mod tests {
.unwrap()
.clone();
- ws.tabs[0].runtimes.insert(
+ ws.insert_test_runtime(
first_pane,
- crate::pane::PaneRuntime::test_with_screen_bytes(
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(
first_info.inner_rect.width.max(1),
first_info.inner_rect.height.max(1),
b"",
),
);
- ws.tabs[0].runtimes.insert(
+ ws.insert_test_runtime(
second_pane,
- crate::pane::PaneRuntime::test_with_screen_bytes(
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(
second_info.inner_rect.width.max(1),
second_info.inner_rect.height.max(1),
b"\x1b[?1002h",
@@ -410,17 +414,17 @@ mod tests {
.unwrap()
.clone();
- ws.tabs[0].runtimes.insert(
+ ws.insert_test_runtime(
first_pane,
- crate::pane::PaneRuntime::test_with_screen_bytes(
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(
first_info.inner_rect.width.max(1),
first_info.inner_rect.height.max(1),
b"",
),
);
- ws.tabs[0].runtimes.insert(
+ ws.insert_test_runtime(
second_pane,
- crate::pane::PaneRuntime::test_with_screen_bytes(
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(
second_info.inner_rect.width.max(1),
second_info.inner_rect.height.max(1),
b"\x1b[?1002h",