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 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.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
@@ -26,6 +28,33 @@ pub struct ErrorBody {
|
||||
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.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "cmd", content = "args", rename_all = "snake_case")]
|
||||
@@ -50,6 +79,41 @@ pub enum Cmd {
|
||||
Detach { surface_id: SurfaceId },
|
||||
Focus { 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,
|
||||
Shutdown,
|
||||
}
|
||||
@@ -62,6 +126,11 @@ pub enum Evt {
|
||||
Exit { surface_id: SurfaceId, code: i32 },
|
||||
SurfaceCreated { surface_id: SurfaceId, workspace_id: WorkspaceId },
|
||||
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)]
|
||||
@@ -112,4 +181,63 @@ mod tests {
|
||||
_ => 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