muster/application/
workspace.rs1use getset::Getters;
2use typed_builder::TypedBuilder;
3
4use crate::domain::{
5 process::{ActivityState, Process, ProcessState},
6 pty::ExitOutcome,
7 value::PaneId,
8};
9
10#[derive(Getters, TypedBuilder)]
13#[getset(get = "pub")]
14pub struct Workspace {
15 processes: Vec<Process>,
16 #[builder(default)]
17 selected_index: usize,
18}
19
20impl Workspace {
21 pub fn selected_process(&self) -> Option<&Process> {
23 self.processes.get(self.selected_index)
24 }
25
26 pub fn is_empty(&self) -> bool {
28 self.processes.is_empty()
29 }
30
31 pub fn add_process(&mut self, process: Process) {
34 self.processes.push(process);
35 }
36
37 pub fn remove(&mut self, pane: PaneId) {
39 if let Some(index) = self.position_of(pane) {
40 self.processes.remove(index);
41 if self.selected_index >= self.processes.len() {
42 self.selected_index = self.processes.len().saturating_sub(1);
43 }
44 }
45 }
46
47 pub fn select_next(&mut self) {
49 if !self.processes.is_empty() {
50 self.selected_index = (self.selected_index + 1) % self.processes.len();
51 }
52 }
53
54 pub fn select_at(&mut self, index: usize) {
57 if !self.processes.is_empty() {
58 self.selected_index = index.min(self.processes.len() - 1);
59 }
60 }
61
62 pub fn select_previous(&mut self) {
64 if !self.processes.is_empty() {
65 let last = self.processes.len() - 1;
66 self.selected_index = if self.selected_index == 0 {
67 last
68 } else {
69 self.selected_index - 1
70 };
71 }
72 }
73
74 pub fn position_of(&self, pane: PaneId) -> Option<usize> {
76 self.processes
77 .iter()
78 .position(|process| *process.id() == pane)
79 }
80
81 pub fn process(&self, pane: PaneId) -> Option<&Process> {
83 self.position_of(pane).map(|index| &self.processes[index])
84 }
85
86 pub fn set_state(&mut self, pane: PaneId, state: ProcessState) {
88 if let Some(index) = self.position_of(pane) {
89 self.processes[index].set_state(state);
90 }
91 }
92
93 pub fn set_autostart(&mut self, pane: PaneId, autostart: bool) {
95 if let Some(index) = self.position_of(pane) {
96 self.processes[index].set_autostart(autostart);
97 }
98 }
99
100 pub fn set_activity(&mut self, pane: PaneId, activity: ActivityState) {
102 if let Some(index) = self.position_of(pane) {
103 self.processes[index].set_activity(activity);
104 }
105 }
106
107 pub fn should_restart(&self, pane: PaneId, outcome: ExitOutcome) -> bool {
109 self.process(pane)
110 .map(|process| process.restart().should_restart(outcome))
111 .unwrap_or(false)
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::domain::{
119 process::{ProcessKind, RestartPolicy},
120 value::{CommandLine, ProcessName},
121 };
122
123 fn process(id: u64, restart: RestartPolicy) -> Process {
124 Process::builder()
125 .id(PaneId::new(id))
126 .name(ProcessName::try_new(format!("p{id}")).unwrap())
127 .kind(ProcessKind::Command)
128 .command(Some(CommandLine::try_new("true").unwrap()))
129 .restart(restart)
130 .build()
131 }
132
133 fn workspace() -> Workspace {
134 Workspace::builder()
135 .processes(vec![
136 process(0, RestartPolicy::Never),
137 process(1, RestartPolicy::Always),
138 ])
139 .build()
140 }
141
142 #[test]
143 fn selection_wraps_in_both_directions() {
144 let mut ws = workspace();
145 assert_eq!(*ws.selected_index(), 0);
146 ws.select_previous();
147 assert_eq!(*ws.selected_index(), 1);
148 ws.select_next();
149 assert_eq!(*ws.selected_index(), 0);
150 }
151
152 #[test]
153 fn set_state_updates_the_named_process() {
154 let mut ws = workspace();
155 ws.set_state(PaneId::new(1), ProcessState::Running);
156 assert_eq!(
157 *ws.process(PaneId::new(1)).unwrap().state(),
158 ProcessState::Running
159 );
160 }
161
162 #[test]
163 fn restart_decision_follows_policy() {
164 let ws = workspace();
165 assert!(!ws.should_restart(PaneId::new(0), ExitOutcome::Failed));
166 assert!(ws.should_restart(PaneId::new(1), ExitOutcome::Failed));
167 }
168}