1use std::collections::HashMap;
2
3use getset::Getters;
4use typed_builder::TypedBuilder;
5
6use crate::{
7 application::Workspace,
8 domain::{
9 config::{ProcessSpec, WorkspaceConfig},
10 process::{Process, ProcessKind, ProcessOrigin, RestartPolicy, StopPolicy},
11 value::{CommandLine, Description, PaneId, ProcessName},
12 },
13};
14
15#[derive(Getters, TypedBuilder)]
17#[getset(get = "pub")]
18pub struct Reconciliation {
19 workspace: Workspace,
21 tracked: Vec<PaneId>,
23 retiring: Vec<PaneId>,
25 removed: Vec<PaneId>,
27}
28
29#[derive(Clone)]
31pub struct ProcessSpecMatcher {
32 kind: ProcessKind,
33 name: ProcessName,
34 command: Option<CommandLine>,
35 working_dir: Option<std::path::PathBuf>,
36 description: Option<Description>,
37 restart: RestartPolicy,
38 stop: Option<StopPolicy>,
39 autostart: bool,
40}
41
42impl ProcessSpecMatcher {
43 pub fn of_spec(kind: ProcessKind, spec: &ProcessSpec) -> Self {
45 Self {
46 kind,
47 name: spec.name().clone(),
48 command: spec.command().clone(),
49 working_dir: spec.working_dir().clone(),
50 description: spec.description().clone(),
51 restart: spec.restart_policy(),
52 stop: spec.effective_stop_policy(kind),
53 autostart: spec.should_autostart(kind),
54 }
55 }
56
57 pub fn of(process: &Process) -> Self {
59 Self {
60 kind: *process.kind(),
61 name: process.name().clone(),
62 command: process.command().clone(),
63 working_dir: process.working_dir().clone(),
64 description: process.description().clone(),
65 restart: *process.restart(),
66 stop: process.effective_stop_policy(),
67 autostart: *process.autostart(),
68 }
69 }
70
71 pub fn matches(&self, spec: &ProcessSpec) -> bool {
73 spec.name() == &self.name
74 && spec.command() == &self.command
75 && spec.working_dir() == &self.working_dir
76 && spec.description() == &self.description
77 && spec.restart_policy() == self.restart
78 && spec.effective_stop_policy(self.kind) == self.stop
79 && spec.should_autostart(self.kind) == self.autostart
80 }
81
82 pub fn matches_process(&self, process: &Process) -> bool {
84 *process.kind() == self.kind
85 && process.name() == &self.name
86 && process.command() == &self.command
87 && process.working_dir() == &self.working_dir
88 && process.description() == &self.description
89 && *process.restart() == self.restart
90 && process.effective_stop_policy() == self.stop
91 && *process.autostart() == self.autostart
92 }
93
94 pub fn with_autostart(
96 &self,
97 config: WorkspaceConfig,
98 occurrence: usize,
99 autostart: Option<bool>,
100 ) -> (WorkspaceConfig, bool) {
101 let mut seen = 0;
102 let mut edited = false;
103 let mut apply = |specs: &[ProcessSpec]| {
104 specs
105 .iter()
106 .map(|spec| {
107 if self.matches(spec) {
108 let hit = seen == occurrence;
109 seen += 1;
110 if hit {
111 edited = true;
112 return spec.clone().with_autostart(autostart);
113 }
114 }
115 spec.clone()
116 })
117 .collect()
118 };
119 let config = match self.kind {
120 ProcessKind::Agent => {
121 let specs = apply(config.agents());
122 config.with_agents(specs)
123 },
124 ProcessKind::Terminal => {
125 let specs = apply(config.terminals());
126 config.with_terminals(specs)
127 },
128 ProcessKind::Command => {
129 let specs = apply(config.commands());
130 config.with_commands(specs)
131 },
132 };
133 (config, edited)
134 }
135}
136
137impl Reconciliation {
138 pub fn apply(
141 workspace: &Workspace,
142 config: &WorkspaceConfig,
143 is_live: impl Fn(PaneId) -> bool,
144 ) -> Self {
145 let sections = [
146 (ProcessKind::Agent, config.agents()),
147 (ProcessKind::Terminal, config.terminals()),
148 (ProcessKind::Command, config.commands()),
149 ];
150 let mut config_counts = HashMap::new();
151 for (kind, specs) in sections {
152 for spec in specs {
153 *config_counts
154 .entry(ProcessSpecIdentity::of_spec(kind, spec))
155 .or_insert(0_usize) += 1;
156 }
157 }
158
159 let mut kept = Vec::new();
160 let mut tracked = Vec::new();
161 let mut retiring = Vec::new();
162 let mut removed = Vec::new();
163 let mut next_id = 0;
164 for process in workspace.processes() {
165 next_id = next_id.max((*process.id()).into_inner() + 1);
166 if *process.origin() == ProcessOrigin::Session {
167 kept.push(process.clone());
168 continue;
169 }
170 let identity = ProcessSpecIdentity::of_process(process);
171 let matched = config_counts
172 .get_mut(&identity)
173 .filter(|count| **count > 0)
174 .map(|count| *count -= 1)
175 .is_some();
176 let pane = *process.id();
177 if matched {
178 tracked.push(pane);
179 kept.push(process.clone());
180 } else if is_live(pane) {
181 retiring.push(pane);
182 kept.push(process.clone());
183 } else {
184 removed.push(pane);
185 }
186 }
187
188 for (kind, specs) in sections {
189 for spec in specs {
190 if let Some(count) = config_counts
191 .get_mut(&ProcessSpecIdentity::of_spec(kind, spec))
192 .filter(|count| **count > 0)
193 {
194 *count -= 1;
195 kept.push(spec.to_process(PaneId::new(next_id), kind));
196 next_id += 1;
197 }
198 }
199 }
200
201 kept.sort_by_key(|process| process.kind().section_index());
202 let selected = workspace
203 .selected_process()
204 .map(|process| *process.id())
205 .and_then(|pane| kept.iter().position(|process| *process.id() == pane))
206 .unwrap_or(0);
207 Self::builder()
208 .workspace(
209 Workspace::builder()
210 .processes(kept)
211 .selected_index(selected)
212 .build(),
213 )
214 .tracked(tracked)
215 .retiring(retiring)
216 .removed(removed)
217 .build()
218 }
219
220 pub fn into_workspace(self) -> Workspace {
222 self.workspace
223 }
224}
225
226#[derive(Hash, PartialEq, Eq)]
228struct ProcessSpecIdentity {
229 kind: ProcessKind,
230 name: ProcessName,
231 command: Option<CommandLine>,
232 working_dir: Option<std::path::PathBuf>,
233 description: Option<Description>,
234 restart: RestartPolicy,
235 stop: Option<StopPolicy>,
236 autostart: bool,
237}
238
239impl ProcessSpecIdentity {
240 fn of_spec(kind: ProcessKind, spec: &ProcessSpec) -> Self {
242 Self {
243 kind,
244 name: spec.name().clone(),
245 command: spec.command().clone(),
246 working_dir: spec.working_dir().clone(),
247 description: spec.description().clone(),
248 restart: spec.restart_policy(),
249 stop: spec.effective_stop_policy(kind),
250 autostart: spec.should_autostart(kind),
251 }
252 }
253
254 fn of_process(process: &Process) -> Self {
256 Self {
257 kind: *process.kind(),
258 name: process.name().clone(),
259 command: process.command().clone(),
260 working_dir: process.working_dir().clone(),
261 description: process.description().clone(),
262 restart: *process.restart(),
263 stop: process.effective_stop_policy(),
264 autostart: *process.autostart(),
265 }
266 }
267}