aboutsummaryrefslogtreecommitdiff
path: root/src/app/api
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-29 20:39:54 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-29 20:39:54 +0300
commit8b74fb8a12716269c76f0b503ee7db5bbec2c921 (patch)
treee5230f94cb1e6eb70d1f00c75cd85dae8b5b9ead /src/app/api
parent5fffc59cd8aaea1604abb90e749e58fd97acfac0 (diff)
feat: emit placement mutation events
Diffstat (limited to 'src/app/api')
-rw-r--r--src/app/api/layouts.rs6
-rw-r--r--src/app/api/plugins/context.rs6
-rw-r--r--src/app/api/tabs.rs76
-rw-r--r--src/app/api/workspaces.rs90
4 files changed, 120 insertions, 58 deletions
diff --git a/src/app/api/layouts.rs b/src/app/api/layouts.rs
index 709d9f39..c7c02f5f 100644
--- a/src/app/api/layouts.rs
+++ b/src/app/api/layouts.rs
@@ -243,7 +243,7 @@ impl App {
let Some(layout) = self.layout_description(ws_idx, tab_idx) else {
return encode_error(id, "layout_not_found", "layout unavailable");
};
- encode_success(id, ResponseResult::LayoutApply { layout })
+ encode_success(id, ResponseResult::LayoutSplitRatioSet { layout })
}
fn resolve_layout_export_target(&self, params: &LayoutExportParams) -> Option<(usize, usize)> {
@@ -684,8 +684,8 @@ mod tests {
);
let success: SuccessResponse = serde_json::from_str(&response).unwrap();
- let ResponseResult::LayoutApply { layout } = success.result else {
- panic!("expected layout apply response");
+ let ResponseResult::LayoutSplitRatioSet { layout } = success.result else {
+ panic!("expected layout split ratio set response");
};
let LayoutNode::Split { ratio, .. } = layout.root else {
panic!("expected split layout root");
diff --git a/src/app/api/plugins/context.rs b/src/app/api/plugins/context.rs
index 92d0d4c6..e81a74d3 100644
--- a/src/app/api/plugins/context.rs
+++ b/src/app/api/plugins/context.rs
@@ -63,6 +63,7 @@ impl App {
})
}),
EventData::WorkspaceRenamed { workspace_id, .. }
+ | EventData::WorkspaceMoved { workspace_id, .. }
| EventData::WorkspaceFocused { workspace_id } => self
.plugin_context_for_workspace_id(workspace_id, correlation_id)
.unwrap_or_else(|| {
@@ -103,6 +104,11 @@ impl App {
workspace_id,
..
}
+ | EventData::TabMoved {
+ tab_id,
+ workspace_id,
+ ..
+ }
| EventData::TabFocused {
tab_id,
workspace_id,
diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs
index 1ffcafdd..abd05b29 100644
--- a/src/app/api/tabs.rs
+++ b/src/app/api/tabs.rs
@@ -14,12 +14,10 @@ impl App {
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 {
+ let Some(_) = 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()
+ self.tab_list_info(ws_idx)
} else {
let mut tabs = Vec::new();
for (ws_idx, ws) in self.state.workspaces.iter().enumerate() {
@@ -190,34 +188,35 @@ impl App {
);
}
+ let tab_id = self
+ .public_tab_id(ws_idx, tab_idx)
+ .unwrap_or_else(|| crate::workspace::public_tab_id_for_number(&ws.id, tab_idx + 1));
+ let workspace_id = self.public_workspace_id(ws_idx);
+ let insert_index = params.insert_index;
let moved = self
.state
.workspaces
.get_mut(ws_idx)
- .is_some_and(|ws| ws.move_tab(tab_idx, params.insert_index));
+ .is_some_and(|ws| ws.move_tab(tab_idx, insert_index));
+ let tabs = self.tab_list_info(ws_idx);
if moved {
self.schedule_session_save();
if self.state.active == Some(ws_idx) {
self.state.tab_scroll_follow_active = true;
self.state.refresh_tab_bar_view();
}
+ self.emit_event(EventEnvelope {
+ event: EventKind::TabMoved,
+ data: EventData::TabMoved {
+ tab_id,
+ workspace_id,
+ insert_index,
+ tabs: tabs.clone(),
+ },
+ });
}
- encode_success(
- id,
- ResponseResult::TabList {
- tabs: self
- .state
- .workspaces
- .get(ws_idx)
- .map(|ws| {
- (0..ws.tabs.len())
- .filter_map(|idx| self.tab_info(ws_idx, idx))
- .collect()
- })
- .unwrap_or_default(),
- },
- )
+ encode_success(id, ResponseResult::TabList { tabs })
}
pub(super) fn handle_tab_close(&mut self, id: String, target: TabTarget) -> String {
@@ -269,6 +268,18 @@ impl App {
encode_success(id, ResponseResult::Ok {})
}
+
+ fn tab_list_info(&self, ws_idx: usize) -> Vec<crate::api::schema::TabInfo> {
+ self.state
+ .workspaces
+ .get(ws_idx)
+ .map(|ws| {
+ (0..ws.tabs.len())
+ .filter_map(|idx| self.tab_info(ws_idx, idx))
+ .collect()
+ })
+ .unwrap_or_default()
+ }
}
fn workspace_not_found(id: String, workspace_id: &str) -> String {
@@ -290,14 +301,9 @@ mod tests {
#[test]
fn api_tab_move_reorders_tabs_in_target_workspace() {
+ let event_hub = crate::api::EventHub::default();
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
- let mut app = App::new(
- &Config::default(),
- true,
- None,
- api_rx,
- crate::api::EventHub::default(),
- );
+ let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone());
let mut workspace = Workspace::test_new("tabs");
workspace.test_add_tab(Some("two"));
workspace.test_add_tab(Some("three"));
@@ -310,7 +316,7 @@ mod tests {
let response = app.handle_tab_move(
"req".into(),
TabMoveParams {
- tab_id: moved_id,
+ tab_id: moved_id.clone(),
insert_index: 3,
},
);
@@ -321,5 +327,19 @@ mod tests {
};
assert_eq!(app.state.workspaces[0].tabs[2].root_pane, moved_root);
assert_eq!(tabs[2].tab_id, app.public_tab_id(0, 2).unwrap());
+ let events = event_hub.events_after(0);
+ assert!(events.iter().any(|(_, event)| {
+ matches!(
+ &event.data,
+ EventData::TabMoved {
+ tab_id,
+ workspace_id,
+ insert_index: 3,
+ tabs,
+ } if tab_id == &moved_id
+ && workspace_id == &app.public_workspace_id(0)
+ && tabs[2].tab_id == moved_id
+ )
+ }));
}
}
diff --git a/src/app/api/workspaces.rs b/src/app/api/workspaces.rs
index 56427463..f4189a04 100644
--- a/src/app/api/workspaces.rs
+++ b/src/app/api/workspaces.rs
@@ -13,13 +13,7 @@ impl App {
encode_success(
id,
ResponseResult::WorkspaceList {
- workspaces: self
- .state
- .workspaces
- .iter()
- .enumerate()
- .map(|(idx, _)| self.workspace_info(idx))
- .collect(),
+ workspaces: self.workspace_list_info(),
},
)
}
@@ -140,20 +134,22 @@ impl App {
);
}
- self.state.move_workspace(index, params.insert_index);
+ let workspace_id = self.public_workspace_id(index);
+ let insert_index = params.insert_index;
+ let moved = self.state.move_workspace(index, insert_index);
+ let workspaces = self.workspace_list_info();
+ if moved {
+ self.emit_event(EventEnvelope {
+ event: EventKind::WorkspaceMoved,
+ data: EventData::WorkspaceMoved {
+ workspace_id,
+ insert_index,
+ workspaces: workspaces.clone(),
+ },
+ });
+ }
- encode_success(
- id,
- ResponseResult::WorkspaceList {
- workspaces: self
- .state
- .workspaces
- .iter()
- .enumerate()
- .map(|(idx, _)| self.workspace_info(idx))
- .collect(),
- },
- )
+ encode_success(id, ResponseResult::WorkspaceList { workspaces })
}
pub(super) fn handle_workspace_close(&mut self, id: String, target: WorkspaceTarget) -> String {
@@ -192,6 +188,15 @@ impl App {
encode_success(id, ResponseResult::Ok {})
}
+
+ fn workspace_list_info(&self) -> Vec<crate::api::schema::WorkspaceInfo> {
+ self.state
+ .workspaces
+ .iter()
+ .enumerate()
+ .map(|(idx, _)| self.workspace_info(idx))
+ .collect()
+ }
}
fn workspace_not_found(id: String, workspace_id: &str) -> String {
@@ -279,14 +284,9 @@ mod tests {
#[test]
fn api_workspace_move_reorders_workspaces() {
+ let event_hub = crate::api::EventHub::default();
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
- let mut app = App::new(
- &Config::default(),
- true,
- None,
- api_rx,
- crate::api::EventHub::default(),
- );
+ let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone());
app.state.workspaces = vec![
Workspace::test_new("one"),
Workspace::test_new("two"),
@@ -310,5 +310,41 @@ mod tests {
};
assert_eq!(workspaces[2].workspace_id, moved_id);
assert_eq!(app.state.workspaces[2].display_name(), "one");
+ let events = event_hub.events_after(0);
+ assert!(events.iter().any(|(_, event)| {
+ matches!(
+ &event.data,
+ EventData::WorkspaceMoved {
+ workspace_id,
+ insert_index: 3,
+ workspaces,
+ } if workspace_id == &moved_id
+ && workspaces[2].workspace_id == moved_id
+ )
+ }));
+ }
+
+ #[test]
+ fn api_workspace_move_noop_does_not_emit_event() {
+ let event_hub = crate::api::EventHub::default();
+ let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
+ let mut app = App::new(&Config::default(), true, None, api_rx, event_hub.clone());
+ app.state.workspaces = vec![Workspace::test_new("one"), Workspace::test_new("two")];
+ let moved_id = app.public_workspace_id(0);
+
+ let response = app.handle_workspace_move(
+ "req".into(),
+ WorkspaceMoveParams {
+ workspace_id: moved_id.clone(),
+ insert_index: 1,
+ },
+ );
+
+ let success: SuccessResponse = serde_json::from_str(&response).unwrap();
+ let ResponseResult::WorkspaceList { workspaces } = success.result else {
+ panic!("expected workspace list");
+ };
+ assert_eq!(workspaces[0].workspace_id, moved_id);
+ assert!(event_hub.events_after(0).is_empty());
}
}