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 insert_in_section(&mut self, process: Process) -> usize {
33 let section = process.kind().section_index();
34 let index = self
35 .processes
36 .iter()
37 .position(|existing| existing.kind().section_index() > section)
38 .unwrap_or(self.processes.len());
39 self.processes.insert(index, process);
40 index
41 }
42
43 pub fn remove(&mut self, pane: PaneId) {
46 if let Some(index) = self.position_of(pane) {
47 let selected = self.selected_process().map(|process| *process.id());
48 self.processes.remove(index);
49 self.selected_index = selected
50 .filter(|selected| *selected != pane)
51 .and_then(|selected| self.position_of(selected))
52 .unwrap_or_else(|| index.min(self.processes.len().saturating_sub(1)));
53 }
54 }
55
56 pub fn select_next(&mut self) {
58 if !self.processes.is_empty() {
59 self.selected_index = (self.selected_index + 1) % self.processes.len();
60 }
61 }
62
63 pub fn select_at(&mut self, index: usize) {
66 if !self.processes.is_empty() {
67 self.selected_index = index.min(self.processes.len() - 1);
68 }
69 }
70
71 pub fn select_previous(&mut self) {
73 if !self.processes.is_empty() {
74 let last = self.processes.len() - 1;
75 self.selected_index = if self.selected_index == 0 {
76 last
77 } else {
78 self.selected_index - 1
79 };
80 }
81 }
82
83 pub fn position_of(&self, pane: PaneId) -> Option<usize> {
85 self.processes
86 .iter()
87 .position(|process| *process.id() == pane)
88 }
89
90 pub fn process(&self, pane: PaneId) -> Option<&Process> {
92 self.position_of(pane).map(|index| &self.processes[index])
93 }
94
95 pub fn set_state(&mut self, pane: PaneId, state: ProcessState) {
97 if let Some(index) = self.position_of(pane) {
98 self.processes[index].set_state(state);
99 }
100 }
101
102 pub fn set_autostart(&mut self, pane: PaneId, autostart: bool) {
104 if let Some(index) = self.position_of(pane) {
105 self.processes[index].set_autostart(autostart);
106 }
107 }
108
109 pub fn set_activity(&mut self, pane: PaneId, activity: ActivityState) {
111 if let Some(index) = self.position_of(pane) {
112 self.processes[index].set_activity(activity);
113 }
114 }
115
116 pub fn should_restart(&self, pane: PaneId, outcome: ExitOutcome) -> bool {
118 self.process(pane)
119 .map(|process| process.restart().should_restart(outcome))
120 .unwrap_or(false)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::domain::{
128 process::{ProcessKind, RestartPolicy},
129 value::{CommandLine, ProcessName},
130 };
131
132 fn process(id: u64, restart: RestartPolicy) -> Process {
133 Process::builder()
134 .id(PaneId::new(id))
135 .name(ProcessName::try_new(format!("p{id}")).unwrap())
136 .kind(ProcessKind::Command)
137 .command(Some(CommandLine::try_new("true").unwrap()))
138 .restart(restart)
139 .build()
140 }
141
142 fn workspace() -> Workspace {
143 Workspace::builder()
144 .processes(vec![
145 process(0, RestartPolicy::Never),
146 process(1, RestartPolicy::Always),
147 ])
148 .build()
149 }
150
151 #[test]
152 fn selection_wraps_in_both_directions() {
153 let mut ws = workspace();
154 assert_eq!(*ws.selected_index(), 0);
155 ws.select_previous();
156 assert_eq!(*ws.selected_index(), 1);
157 ws.select_next();
158 assert_eq!(*ws.selected_index(), 0);
159 }
160
161 #[test]
162 fn set_state_updates_the_named_process() {
163 let mut ws = workspace();
164 ws.set_state(PaneId::new(1), ProcessState::Running);
165 assert_eq!(
166 *ws.process(PaneId::new(1)).unwrap().state(),
167 ProcessState::Running
168 );
169 }
170
171 #[test]
173 fn removing_an_earlier_process_preserves_selection() {
174 let mut ws = Workspace::builder()
175 .processes(vec![
176 process(0, RestartPolicy::Never),
177 process(1, RestartPolicy::Never),
178 process(2, RestartPolicy::Never),
179 ])
180 .selected_index(2)
181 .build();
182
183 ws.remove(PaneId::new(0));
184
185 assert_eq!(*ws.selected_process().unwrap().id(), PaneId::new(2));
186 assert_eq!(*ws.selected_index(), 1);
187 }
188
189 #[test]
192 fn removing_the_selected_process_selects_an_adjacent_process() {
193 let mut ws = Workspace::builder()
194 .processes(vec![
195 process(0, RestartPolicy::Never),
196 process(1, RestartPolicy::Never),
197 process(2, RestartPolicy::Never),
198 ])
199 .selected_index(1)
200 .build();
201
202 ws.remove(PaneId::new(1));
203 assert_eq!(*ws.selected_process().unwrap().id(), PaneId::new(2));
204
205 ws.remove(PaneId::new(2));
206 assert_eq!(*ws.selected_process().unwrap().id(), PaneId::new(0));
207 }
208
209 #[test]
210 fn restart_decision_follows_policy() {
211 let ws = workspace();
212 assert!(!ws.should_restart(PaneId::new(0), ExitOutcome::Failed));
213 assert!(ws.should_restart(PaneId::new(1), ExitOutcome::Failed));
214 }
215
216 #[test]
218 fn inserts_a_process_at_the_end_of_its_section() {
219 let mut ws = workspace();
220 let agent = Process::builder()
221 .id(PaneId::new(2))
222 .name(ProcessName::try_new("agent").unwrap())
223 .kind(ProcessKind::Agent)
224 .build();
225
226 let index = ws.insert_in_section(agent);
227
228 assert_eq!(index, 0);
229 assert_eq!(*ws.processes()[index].kind(), ProcessKind::Agent);
230 }
231}