aboutsummaryrefslogtreecommitdiff
path: root/src/app/api/tabs.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-21 20:19:54 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-21 20:22:57 +0300
commit866aaf740e10fcfce169aaf81380ea0257fbe272 (patch)
treef815392b7c6b24a7c0de14e655bd635656383813 /src/app/api/tabs.rs
parentca579c402d5668560ca13059f82c03178be18b2a (diff)
refactor: separate server client boundaries
Diffstat (limited to 'src/app/api/tabs.rs')
-rw-r--r--src/app/api/tabs.rs233
1 files changed, 233 insertions, 0 deletions
diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs
new file mode 100644
index 00000000..7978a17f
--- /dev/null
+++ b/src/app/api/tabs.rs
@@ -0,0 +1,233 @@
+use std::path::PathBuf;
+
+use crate::api::schema::{
+ EventData, EventEnvelope, EventKind, ResponseResult, TabCreateParams, TabListParams,
+ TabRenameParams, TabTarget,
+};
+use crate::app::{App, Mode};
+
+use super::responses::{encode_error, encode_success};
+
+impl App {
+ pub(super) fn handle_tab_list(&mut self, id: String, params: TabListParams) -> String {
+ let tabs = if let Some(workspace_id) = params.workspace_id {
+ let Some(ws_idx) = self.parse_workspace_id(&workspace_id) else {
+ return workspace_not_found(id, &workspace_id);
+ };
+ let Some(ws) = self.state.workspaces.get(ws_idx) else {
+ return workspace_not_found(id, &workspace_id);
+ };
+ (0..ws.tabs.len())
+ .filter_map(|tab_idx| self.tab_info(ws_idx, tab_idx))
+ .collect()
+ } else {
+ let mut tabs = Vec::new();
+ for (ws_idx, ws) in self.state.workspaces.iter().enumerate() {
+ for tab_idx in 0..ws.tabs.len() {
+ if let Some(tab) = self.tab_info(ws_idx, tab_idx) {
+ tabs.push(tab);
+ }
+ }
+ }
+ tabs
+ };
+
+ encode_success(id, ResponseResult::TabList { tabs })
+ }
+
+ pub(super) fn handle_tab_get(&mut self, id: String, target: TabTarget) -> String {
+ let Some((ws_idx, tab_idx)) = self.parse_tab_id(&target.tab_id) else {
+ return tab_not_found(id, &target.tab_id);
+ };
+ let Some(tab) = self.tab_info(ws_idx, tab_idx) else {
+ return tab_not_found(id, &target.tab_id);
+ };
+
+ encode_success(id, ResponseResult::TabInfo { tab })
+ }
+
+ pub(super) fn handle_tab_create(&mut self, id: String, params: TabCreateParams) -> String {
+ let TabCreateParams {
+ workspace_id,
+ cwd,
+ focus,
+ label,
+ } = params;
+ let ws_idx = if let Some(workspace_id) = workspace_id {
+ let Some(ws_idx) = self.parse_workspace_id(&workspace_id) else {
+ return workspace_not_found(id, &workspace_id);
+ };
+ ws_idx
+ } else if let Some(active) = self.state.active {
+ active
+ } else {
+ return encode_error(id, "workspace_not_found", "no active workspace");
+ };
+ let cwd = cwd
+ .map(PathBuf::from)
+ .or_else(|| {
+ self.state
+ .focused_runtime_in_workspace(&self.terminal_runtimes, ws_idx)
+ .and_then(|rt| rt.cwd())
+ })
+ .or_else(|| std::env::current_dir().ok())
+ .unwrap_or_else(|| PathBuf::from("/"));
+ let (rows, cols) = self.state.estimate_pane_size();
+ let default_shell = self.state.default_shell.clone();
+ let scrollback_limit_bytes = self.state.pane_scrollback_limit_bytes;
+ let host_terminal_theme = self.state.host_terminal_theme;
+ let result = self
+ .state
+ .workspaces
+ .get_mut(ws_idx)
+ .ok_or_else(|| std::io::Error::other("workspace disappeared"))
+ .and_then(|ws| {
+ ws.create_tab(
+ rows,
+ cols,
+ cwd,
+ scrollback_limit_bytes,
+ host_terminal_theme,
+ &default_shell,
+ )
+ });
+ match result {
+ Ok((tab_idx, terminal, runtime)) => {
+ self.terminal_runtimes.insert(terminal.id.clone(), runtime);
+ self.state.terminals.insert(terminal.id.clone(), terminal);
+ if let Some(label) = label {
+ let workspace_id = self.state.workspaces[ws_idx].id.clone();
+ let tab_id = self
+ .public_tab_id(ws_idx, tab_idx)
+ .unwrap_or_else(|| format!("{}:{}", workspace_id, tab_idx + 1));
+ if let Some(tab) = self
+ .state
+ .workspaces
+ .get_mut(ws_idx)
+ .and_then(|ws| ws.tabs.get_mut(tab_idx))
+ {
+ tab.set_custom_name(label);
+ crate::logging::tab_renamed(&workspace_id, &tab_id);
+ }
+ }
+ if focus {
+ self.state.switch_workspace(ws_idx);
+ self.state.switch_tab(tab_idx);
+ self.state.mode = Mode::Terminal;
+ }
+ self.schedule_session_save();
+ let tab = self.tab_info(ws_idx, tab_idx).unwrap();
+ let root_pane = self
+ .root_pane_info(ws_idx, tab_idx)
+ .expect("new tab should have a root pane");
+ self.emit_event(EventEnvelope {
+ event: EventKind::TabCreated,
+ data: EventData::TabCreated { tab: tab.clone() },
+ });
+ self.emit_event(EventEnvelope {
+ event: EventKind::PaneCreated,
+ data: EventData::PaneCreated {
+ pane: root_pane.clone(),
+ },
+ });
+ encode_success(
+ id,
+ self.tab_created_result(ws_idx, tab_idx)
+ .expect("new tab should produce a complete create response"),
+ )
+ }
+ Err(err) => encode_error(id, "tab_create_failed", err.to_string()),
+ }
+ }
+
+ pub(super) fn handle_tab_focus(&mut self, id: String, target: TabTarget) -> String {
+ let Some((ws_idx, tab_idx)) = self.parse_tab_id(&target.tab_id) else {
+ return tab_not_found(id, &target.tab_id);
+ };
+ self.state.switch_workspace(ws_idx);
+ self.state.switch_tab(tab_idx);
+ let tab = self.tab_info(ws_idx, tab_idx).unwrap();
+
+ encode_success(id, ResponseResult::TabInfo { tab })
+ }
+
+ pub(super) fn handle_tab_rename(&mut self, id: String, params: TabRenameParams) -> String {
+ let Some((ws_idx, tab_idx)) = self.parse_tab_id(&params.tab_id) else {
+ return tab_not_found(id, &params.tab_id);
+ };
+ let workspace_id = self.state.workspaces[ws_idx].id.clone();
+ let tab_id = self
+ .public_tab_id(ws_idx, tab_idx)
+ .unwrap_or_else(|| format!("{}:{}", workspace_id, tab_idx + 1));
+ let Some(tab) = self
+ .state
+ .workspaces
+ .get_mut(ws_idx)
+ .and_then(|ws| ws.tabs.get_mut(tab_idx))
+ else {
+ return tab_not_found(id, &params.tab_id);
+ };
+ tab.set_custom_name(params.label.clone());
+ crate::logging::tab_renamed(&workspace_id, &tab_id);
+ self.schedule_session_save();
+ self.emit_event(EventEnvelope {
+ event: EventKind::TabRenamed,
+ data: EventData::TabRenamed {
+ tab_id: self.public_tab_id(ws_idx, tab_idx).unwrap(),
+ workspace_id: self.public_workspace_id(ws_idx),
+ label: params.label,
+ },
+ });
+ let tab = self.tab_info(ws_idx, tab_idx).unwrap();
+
+ encode_success(id, ResponseResult::TabInfo { tab })
+ }
+
+ pub(super) fn handle_tab_close(&mut self, id: String, target: TabTarget) -> String {
+ let Some((ws_idx, tab_idx)) = self.parse_tab_id(&target.tab_id) else {
+ return tab_not_found(id, &target.tab_id);
+ };
+ let terminal_ids = self.state.terminal_ids_for_tab(ws_idx, tab_idx);
+ let Some(ws) = self.state.workspaces.get_mut(ws_idx) else {
+ return tab_not_found(id, &target.tab_id);
+ };
+ if ws.tabs.len() <= 1 {
+ return encode_error(
+ id,
+ "tab_close_failed",
+ "cannot close the last tab in a workspace",
+ );
+ }
+ if !ws.close_tab(tab_idx) {
+ return encode_error(
+ id,
+ "tab_close_failed",
+ format!("tab {} could not be closed", target.tab_id),
+ );
+ }
+ self.state.remove_unattached_terminal_ids(terminal_ids);
+ self.shutdown_detached_terminal_runtimes();
+ self.schedule_session_save();
+ self.emit_event(EventEnvelope {
+ event: EventKind::TabClosed,
+ data: EventData::TabClosed {
+ tab_id: target.tab_id,
+ workspace_id: self.public_workspace_id(ws_idx),
+ },
+ });
+
+ encode_success(id, ResponseResult::Ok {})
+ }
+}
+
+fn workspace_not_found(id: String, workspace_id: &str) -> String {
+ encode_error(
+ id,
+ "workspace_not_found",
+ format!("workspace {workspace_id} not found"),
+ )
+}
+
+fn tab_not_found(id: String, tab_id: &str) -> String {
+ encode_error(id, "tab_not_found", format!("tab {tab_id} not found"))
+}