feat(proto): M2 commands (split/ratios/move/preset/restart/groups/meta) and events
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::ids::{SurfaceId, WorkspaceId};
|
use crate::ids::{GroupId, SurfaceId, WorkspaceId};
|
||||||
|
use crate::layout::LayoutNode;
|
||||||
|
use crate::workspace::{Group, WorkspaceView};
|
||||||
|
|
||||||
/// Wire envelope. `kind` is the serde tag.
|
/// Wire envelope. `kind` is the serde tag.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
@@ -26,6 +28,33 @@ pub struct ErrorBody {
|
|||||||
pub msg: String,
|
pub msg: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Direction a split grows the new neighbor.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum SplitDir {
|
||||||
|
Right,
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Edge of a target leaf to drop a moved panel against.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Edge {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One panel slot when applying a preset.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct PresetSlot {
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub command: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Client → daemon commands. The active subset for M0+M1.
|
/// Client → daemon commands. The active subset for M0+M1.
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(tag = "cmd", content = "args", rename_all = "snake_case")]
|
#[serde(tag = "cmd", content = "args", rename_all = "snake_case")]
|
||||||
@@ -50,6 +79,41 @@ pub enum Cmd {
|
|||||||
Detach { surface_id: SurfaceId },
|
Detach { surface_id: SurfaceId },
|
||||||
Focus { surface_id: SurfaceId },
|
Focus { surface_id: SurfaceId },
|
||||||
Close { surface_id: SurfaceId },
|
Close { surface_id: SurfaceId },
|
||||||
|
SplitSurface {
|
||||||
|
surface_id: SurfaceId,
|
||||||
|
dir: SplitDir,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
command: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
args: Vec<String>,
|
||||||
|
},
|
||||||
|
SetRatios { workspace_id: WorkspaceId, node_path: Vec<u32>, ratios: Vec<f32> },
|
||||||
|
MoveSurface { surface_id: SurfaceId, target_surface_id: SurfaceId, edge: Edge },
|
||||||
|
ApplyPreset { workspace_id: WorkspaceId, preset_id: String, slots: Vec<PresetSlot> },
|
||||||
|
RestartSurface { surface_id: SurfaceId },
|
||||||
|
CloseWorkspace { workspace_id: WorkspaceId },
|
||||||
|
SetWorkspaceMeta {
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
name: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
group_id: Option<Option<GroupId>>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
unread: Option<bool>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
order: Option<u32>,
|
||||||
|
},
|
||||||
|
CreateGroup { name: String, color: String },
|
||||||
|
SetGroup {
|
||||||
|
group_id: GroupId,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
name: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
color: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
order: Option<u32>,
|
||||||
|
},
|
||||||
|
DeleteGroup { group_id: GroupId },
|
||||||
Status,
|
Status,
|
||||||
Shutdown,
|
Shutdown,
|
||||||
}
|
}
|
||||||
@@ -62,6 +126,11 @@ pub enum Evt {
|
|||||||
Exit { surface_id: SurfaceId, code: i32 },
|
Exit { surface_id: SurfaceId, code: i32 },
|
||||||
SurfaceCreated { surface_id: SurfaceId, workspace_id: WorkspaceId },
|
SurfaceCreated { surface_id: SurfaceId, workspace_id: WorkspaceId },
|
||||||
SurfaceClosed { surface_id: SurfaceId },
|
SurfaceClosed { surface_id: SurfaceId },
|
||||||
|
LayoutChanged { workspace_id: WorkspaceId, layout: Option<LayoutNode> },
|
||||||
|
WorkspaceChanged { workspace: WorkspaceView },
|
||||||
|
WorkspaceClosed { workspace_id: WorkspaceId },
|
||||||
|
GroupsChanged { groups: Vec<Group> },
|
||||||
|
SurfaceRestarted { surface_id: SurfaceId },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -112,4 +181,63 @@ mod tests {
|
|||||||
_ => panic!("wrong variant"),
|
_ => panic!("wrong variant"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_surface_serializes() {
|
||||||
|
let env = Envelope::Req {
|
||||||
|
id: 1,
|
||||||
|
cmd: Cmd::SplitSurface {
|
||||||
|
surface_id: SurfaceId("s_1".into()),
|
||||||
|
dir: SplitDir::Right,
|
||||||
|
command: None,
|
||||||
|
args: vec![],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let j = serde_json::to_string(&env).unwrap();
|
||||||
|
assert!(j.contains("split_surface"));
|
||||||
|
assert!(j.contains(r#""dir":"right""#));
|
||||||
|
let back: Envelope = serde_json::from_str(&j).unwrap();
|
||||||
|
assert_eq!(back, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_preset_round_trips() {
|
||||||
|
let env = Envelope::Req {
|
||||||
|
id: 2,
|
||||||
|
cmd: Cmd::ApplyPreset {
|
||||||
|
workspace_id: WorkspaceId("w_1".into()),
|
||||||
|
preset_id: "2x2".into(),
|
||||||
|
slots: vec![
|
||||||
|
PresetSlot { command: Some("claude".into()), args: vec![] },
|
||||||
|
PresetSlot { command: None, args: vec![] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let back: Envelope = serde_json::from_str(&serde_json::to_string(&env).unwrap()).unwrap();
|
||||||
|
assert_eq!(back, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_ratios_round_trips() {
|
||||||
|
let env = Envelope::Req {
|
||||||
|
id: 3,
|
||||||
|
cmd: Cmd::SetRatios {
|
||||||
|
workspace_id: WorkspaceId("w_1".into()),
|
||||||
|
node_path: vec![0, 1],
|
||||||
|
ratios: vec![0.3, 0.7],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let back: Envelope = serde_json::from_str(&serde_json::to_string(&env).unwrap()).unwrap();
|
||||||
|
assert_eq!(back, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layout_changed_event_round_trips() {
|
||||||
|
let evt = Envelope::Evt(Evt::LayoutChanged {
|
||||||
|
workspace_id: WorkspaceId("w_1".into()),
|
||||||
|
layout: Some(crate::layout::LayoutNode::leaf(SurfaceId("s_1".into()))),
|
||||||
|
});
|
||||||
|
let back: Envelope = serde_json::from_str(&serde_json::to_string(&evt).unwrap()).unwrap();
|
||||||
|
assert_eq!(back, evt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user