Skip to main content

oxihuman_core/
command_bus.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(dead_code)]
5
6use std::collections::HashMap;
7
8// ─── Core types ───────────────────────────────────────────────────────────────
9
10/// Result returned by command execution or undo.
11#[derive(Debug, Clone)]
12pub struct CommandResult {
13    pub success: bool,
14    pub message: String,
15}
16
17/// Shared mutable state that commands operate on.
18#[derive(Debug, Clone)]
19pub struct CommandState {
20    pub params: HashMap<String, f64>,
21    pub flags: HashMap<String, bool>,
22    /// Log of executed command descriptions.
23    pub history: Vec<String>,
24}
25
26/// A reversible command that operates on `CommandState`.
27pub trait Command: std::fmt::Debug {
28    fn execute(&self, state: &mut CommandState) -> CommandResult;
29    fn undo(&self, state: &mut CommandState) -> CommandResult;
30    fn description(&self) -> &str;
31}
32
33/// The command bus with undo/redo stacks.
34pub struct CommandBus {
35    pub undo_stack: Vec<Box<dyn Command>>,
36    pub redo_stack: Vec<Box<dyn Command>>,
37    pub max_history: usize,
38    pub state: CommandState,
39}
40
41// ─── Built-in commands ────────────────────────────────────────────────────────
42
43/// Set a numeric parameter, recording the previous value for undo.
44#[derive(Debug)]
45pub struct SetParamCommand {
46    pub key: String,
47    pub value: f64,
48    pub old_value: f64,
49}
50
51impl Command for SetParamCommand {
52    fn execute(&self, state: &mut CommandState) -> CommandResult {
53        state.params.insert(self.key.clone(), self.value);
54        CommandResult {
55            success: true,
56            message: format!("set {} = {}", self.key, self.value),
57        }
58    }
59
60    fn undo(&self, state: &mut CommandState) -> CommandResult {
61        state.params.insert(self.key.clone(), self.old_value);
62        CommandResult {
63            success: true,
64            message: format!("undo set {} -> {}", self.key, self.old_value),
65        }
66    }
67
68    fn description(&self) -> &str {
69        "SetParamCommand"
70    }
71}
72
73/// Set a boolean flag, recording the previous value for undo.
74#[derive(Debug)]
75pub struct SetFlagCommand {
76    pub key: String,
77    pub value: bool,
78    pub old_value: bool,
79}
80
81impl Command for SetFlagCommand {
82    fn execute(&self, state: &mut CommandState) -> CommandResult {
83        state.flags.insert(self.key.clone(), self.value);
84        CommandResult {
85            success: true,
86            message: format!("set flag {} = {}", self.key, self.value),
87        }
88    }
89
90    fn undo(&self, state: &mut CommandState) -> CommandResult {
91        state.flags.insert(self.key.clone(), self.old_value);
92        CommandResult {
93            success: true,
94            message: format!("undo flag {} -> {}", self.key, self.old_value),
95        }
96    }
97
98    fn description(&self) -> &str {
99        "SetFlagCommand"
100    }
101}
102
103/// Execute multiple commands atomically (undo reverses all in reverse order).
104#[derive(Debug)]
105pub struct BatchCommand {
106    pub commands: Vec<Box<dyn Command>>,
107    pub name: String,
108}
109
110impl Command for BatchCommand {
111    fn execute(&self, state: &mut CommandState) -> CommandResult {
112        let mut all_ok = true;
113        let mut msgs = Vec::new();
114        for cmd in &self.commands {
115            let r = cmd.execute(state);
116            if !r.success {
117                all_ok = false;
118            }
119            msgs.push(r.message);
120        }
121        CommandResult {
122            success: all_ok,
123            message: msgs.join("; "),
124        }
125    }
126
127    fn undo(&self, state: &mut CommandState) -> CommandResult {
128        let mut all_ok = true;
129        let mut msgs = Vec::new();
130        for cmd in self.commands.iter().rev() {
131            let r = cmd.undo(state);
132            if !r.success {
133                all_ok = false;
134            }
135            msgs.push(r.message);
136        }
137        CommandResult {
138            success: all_ok,
139            message: msgs.join("; "),
140        }
141    }
142
143    fn description(&self) -> &str {
144        &self.name
145    }
146}
147
148// ─── Construction ─────────────────────────────────────────────────────────────
149
150pub fn new_command_state() -> CommandState {
151    CommandState {
152        params: HashMap::new(),
153        flags: HashMap::new(),
154        history: Vec::new(),
155    }
156}
157
158pub fn new_command_bus(max_history: usize) -> CommandBus {
159    CommandBus {
160        undo_stack: Vec::new(),
161        redo_stack: Vec::new(),
162        max_history,
163        state: new_command_state(),
164    }
165}
166
167// ─── Operations ───────────────────────────────────────────────────────────────
168
169/// Execute a command and push it onto the undo stack.
170/// Clears the redo stack (standard undo/redo semantics).
171pub fn execute_command(bus: &mut CommandBus, cmd: Box<dyn Command>) -> CommandResult {
172    let result = cmd.execute(&mut bus.state);
173    if result.success {
174        bus.state.history.push(cmd.description().to_string());
175        bus.redo_stack.clear();
176        bus.undo_stack.push(cmd);
177        // Trim undo stack to max_history
178        if bus.undo_stack.len() > bus.max_history {
179            bus.undo_stack.remove(0);
180        }
181    }
182    result
183}
184
185/// Undo the last command and push it onto the redo stack.
186pub fn undo_last(bus: &mut CommandBus) -> Option<CommandResult> {
187    let cmd = bus.undo_stack.pop()?;
188    let result = cmd.undo(&mut bus.state);
189    bus.redo_stack.push(cmd);
190    Some(result)
191}
192
193/// Redo the last undone command and push it back onto the undo stack.
194pub fn redo_last(bus: &mut CommandBus) -> Option<CommandResult> {
195    let cmd = bus.redo_stack.pop()?;
196    let result = cmd.execute(&mut bus.state);
197    bus.undo_stack.push(cmd);
198    Some(result)
199}
200
201pub fn undo_count(bus: &CommandBus) -> usize {
202    bus.undo_stack.len()
203}
204
205pub fn redo_count(bus: &CommandBus) -> usize {
206    bus.redo_stack.len()
207}
208
209pub fn clear_history(bus: &mut CommandBus) {
210    bus.undo_stack.clear();
211    bus.redo_stack.clear();
212}
213
214/// Return descriptions of commands in the undo stack (oldest first).
215pub fn command_descriptions(bus: &CommandBus) -> Vec<&str> {
216    bus.undo_stack.iter().map(|c| c.description()).collect()
217}
218
219// ─── Tests ────────────────────────────────────────────────────────────────────
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_new_bus_empty() {
227        let bus = new_command_bus(10);
228        assert_eq!(undo_count(&bus), 0);
229        assert_eq!(redo_count(&bus), 0);
230    }
231
232    #[test]
233    fn test_execute_set_param() {
234        let mut bus = new_command_bus(10);
235        let cmd = Box::new(SetParamCommand {
236            key: "height".to_string(),
237            value: 1.75,
238            old_value: 0.0,
239        });
240        let result = execute_command(&mut bus, cmd);
241        assert!(result.success);
242        assert_eq!(
243            *bus.state.params.get("height").expect("should succeed"),
244            1.75
245        );
246    }
247
248    #[test]
249    fn test_undo_set_param() {
250        let mut bus = new_command_bus(10);
251        let cmd = Box::new(SetParamCommand {
252            key: "age".to_string(),
253            value: 30.0,
254            old_value: 25.0,
255        });
256        execute_command(&mut bus, cmd);
257        let r = undo_last(&mut bus).expect("should succeed");
258        assert!(r.success);
259        assert_eq!(*bus.state.params.get("age").expect("should succeed"), 25.0);
260    }
261
262    #[test]
263    fn test_redo_after_undo() {
264        let mut bus = new_command_bus(10);
265        let cmd = Box::new(SetParamCommand {
266            key: "x".to_string(),
267            value: 5.0,
268            old_value: 0.0,
269        });
270        execute_command(&mut bus, cmd);
271        undo_last(&mut bus);
272        let r = redo_last(&mut bus).expect("should succeed");
273        assert!(r.success);
274        assert_eq!(*bus.state.params.get("x").expect("should succeed"), 5.0);
275    }
276
277    #[test]
278    fn test_redo_cleared_on_new_command() {
279        let mut bus = new_command_bus(10);
280        execute_command(
281            &mut bus,
282            Box::new(SetParamCommand {
283                key: "a".to_string(),
284                value: 1.0,
285                old_value: 0.0,
286            }),
287        );
288        undo_last(&mut bus);
289        assert_eq!(redo_count(&bus), 1);
290        execute_command(
291            &mut bus,
292            Box::new(SetParamCommand {
293                key: "b".to_string(),
294                value: 2.0,
295                old_value: 0.0,
296            }),
297        );
298        assert_eq!(redo_count(&bus), 0);
299    }
300
301    #[test]
302    fn test_set_flag_command() {
303        let mut bus = new_command_bus(10);
304        let cmd = Box::new(SetFlagCommand {
305            key: "visible".to_string(),
306            value: true,
307            old_value: false,
308        });
309        execute_command(&mut bus, cmd);
310        assert!(*bus.state.flags.get("visible").expect("should succeed"));
311    }
312
313    #[test]
314    fn test_undo_set_flag() {
315        let mut bus = new_command_bus(10);
316        execute_command(
317            &mut bus,
318            Box::new(SetFlagCommand {
319                key: "f".to_string(),
320                value: true,
321                old_value: false,
322            }),
323        );
324        undo_last(&mut bus);
325        assert!(!*bus.state.flags.get("f").expect("should succeed"));
326    }
327
328    #[test]
329    fn test_batch_command() {
330        let mut bus = new_command_bus(10);
331        let batch = Box::new(BatchCommand {
332            name: "set_both".to_string(),
333            commands: vec![
334                Box::new(SetParamCommand {
335                    key: "p1".to_string(),
336                    value: 1.0,
337                    old_value: 0.0,
338                }),
339                Box::new(SetParamCommand {
340                    key: "p2".to_string(),
341                    value: 2.0,
342                    old_value: 0.0,
343                }),
344            ],
345        });
346        let r = execute_command(&mut bus, batch);
347        assert!(r.success);
348        assert_eq!(*bus.state.params.get("p1").expect("should succeed"), 1.0);
349        assert_eq!(*bus.state.params.get("p2").expect("should succeed"), 2.0);
350    }
351
352    #[test]
353    fn test_batch_undo() {
354        let mut bus = new_command_bus(10);
355        execute_command(
356            &mut bus,
357            Box::new(BatchCommand {
358                name: "batch".to_string(),
359                commands: vec![Box::new(SetParamCommand {
360                    key: "q".to_string(),
361                    value: 9.0,
362                    old_value: 1.0,
363                })],
364            }),
365        );
366        undo_last(&mut bus);
367        assert_eq!(*bus.state.params.get("q").expect("should succeed"), 1.0);
368    }
369
370    #[test]
371    fn test_clear_history() {
372        let mut bus = new_command_bus(10);
373        execute_command(
374            &mut bus,
375            Box::new(SetParamCommand {
376                key: "x".to_string(),
377                value: 1.0,
378                old_value: 0.0,
379            }),
380        );
381        clear_history(&mut bus);
382        assert_eq!(undo_count(&bus), 0);
383        assert_eq!(redo_count(&bus), 0);
384    }
385
386    #[test]
387    fn test_command_descriptions() {
388        let mut bus = new_command_bus(10);
389        execute_command(
390            &mut bus,
391            Box::new(SetParamCommand {
392                key: "x".to_string(),
393                value: 1.0,
394                old_value: 0.0,
395            }),
396        );
397        execute_command(
398            &mut bus,
399            Box::new(SetFlagCommand {
400                key: "f".to_string(),
401                value: true,
402                old_value: false,
403            }),
404        );
405        let descs = command_descriptions(&bus);
406        assert_eq!(descs.len(), 2);
407        assert!(descs.contains(&"SetParamCommand"));
408        assert!(descs.contains(&"SetFlagCommand"));
409    }
410
411    #[test]
412    fn test_undo_empty_returns_none() {
413        let mut bus = new_command_bus(10);
414        assert!(undo_last(&mut bus).is_none());
415    }
416
417    #[test]
418    fn test_redo_empty_returns_none() {
419        let mut bus = new_command_bus(10);
420        assert!(redo_last(&mut bus).is_none());
421    }
422
423    #[test]
424    fn test_max_history_trimmed() {
425        let mut bus = new_command_bus(3);
426        for i in 0..5 {
427            execute_command(
428                &mut bus,
429                Box::new(SetParamCommand {
430                    key: format!("p{}", i),
431                    value: i as f64,
432                    old_value: 0.0,
433                }),
434            );
435        }
436        assert_eq!(undo_count(&bus), 3);
437    }
438
439    #[test]
440    fn test_state_history_log() {
441        let mut bus = new_command_bus(10);
442        execute_command(
443            &mut bus,
444            Box::new(SetParamCommand {
445                key: "k".to_string(),
446                value: 1.0,
447                old_value: 0.0,
448            }),
449        );
450        assert_eq!(bus.state.history.len(), 1);
451        assert_eq!(bus.state.history[0], "SetParamCommand");
452    }
453}