1use crate::protocol::{ProcessInfo, Stream};
2use std::collections::{HashMap, VecDeque};
3
4const MAX_BUFFER_LINES: usize = 10_000;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum StreamMode {
8 Stdout,
9 Stderr,
10 Both,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum LineSource {
15 Stdout,
16 Stderr,
17}
18
19pub struct OutputBuffer {
22 lines: VecDeque<(LineSource, String)>,
23 max_lines: usize,
24}
25
26impl OutputBuffer {
27 pub fn new(max_lines: usize) -> Self {
28 Self {
29 lines: VecDeque::with_capacity(max_lines),
30 max_lines,
31 }
32 }
33
34 pub fn push(&mut self, source: LineSource, line: String) {
35 if self.lines.len() == self.max_lines {
36 self.lines.pop_front();
37 }
38 self.lines.push_back((source, line));
39 }
40
41 pub fn stdout_lines(&self) -> Vec<&str> {
42 self.lines
43 .iter()
44 .filter(|(src, _)| *src == LineSource::Stdout)
45 .map(|(_, s)| s.as_str())
46 .collect()
47 }
48
49 pub fn stderr_lines(&self) -> Vec<&str> {
50 self.lines
51 .iter()
52 .filter(|(src, _)| *src == LineSource::Stderr)
53 .map(|(_, s)| s.as_str())
54 .collect()
55 }
56
57 pub fn all_lines(&self) -> Vec<(LineSource, &str)> {
58 self.lines
59 .iter()
60 .map(|(src, s)| (*src, s.as_str()))
61 .collect()
62 }
63}
64
65pub struct App {
66 pub processes: Vec<ProcessInfo>,
67 pub selected: usize,
68 pub buffers: HashMap<String, OutputBuffer>,
69 pub stream_mode: StreamMode,
70 pub paused: bool,
71 pub scroll_offsets: HashMap<String, usize>,
72 pub running: bool,
73 pub stop_all_on_quit: bool,
74}
75
76impl Default for App {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl App {
83 pub fn new() -> Self {
84 Self {
85 processes: Vec::new(),
86 selected: 0,
87 buffers: HashMap::new(),
88 stream_mode: StreamMode::Stdout,
89 paused: false,
90 scroll_offsets: HashMap::new(),
91 running: true,
92 stop_all_on_quit: false,
93 }
94 }
95
96 pub fn update_processes(&mut self, processes: Vec<ProcessInfo>) {
97 self.processes = processes;
98 if self.selected >= self.processes.len() && !self.processes.is_empty() {
99 self.selected = self.processes.len() - 1;
100 }
101 }
102
103 pub fn selected_name(&self) -> Option<&str> {
104 self.processes.get(self.selected).map(|p| p.name.as_str())
105 }
106
107 pub fn select_next(&mut self) {
108 if !self.processes.is_empty() {
109 self.selected = (self.selected + 1) % self.processes.len();
110 }
111 }
112
113 pub fn select_prev(&mut self) {
114 if !self.processes.is_empty() {
115 self.selected = if self.selected == 0 {
116 self.processes.len() - 1
117 } else {
118 self.selected - 1
119 };
120 }
121 }
122
123 pub fn cycle_stream_mode(&mut self) {
124 self.stream_mode = match self.stream_mode {
125 StreamMode::Stdout => StreamMode::Stderr,
126 StreamMode::Stderr => StreamMode::Both,
127 StreamMode::Both => StreamMode::Stdout,
128 };
129 }
130
131 pub fn toggle_pause(&mut self) {
132 self.paused = !self.paused;
133 if !self.paused {
134 if let Some(name) = self.processes.get(self.selected).map(|p| p.name.clone()) {
136 self.scroll_offsets.remove(&name);
137 }
138 }
139 }
140
141 pub fn push_output(&mut self, process: &str, stream: Stream, line: &str) {
142 let buf = self
143 .buffers
144 .entry(process.to_string())
145 .or_insert_with(|| OutputBuffer::new(MAX_BUFFER_LINES));
146 let source = match stream {
147 Stream::Stdout => LineSource::Stdout,
148 Stream::Stderr => LineSource::Stderr,
149 };
150 buf.push(source, line.to_string());
151 }
152
153 pub fn quit(&mut self) {
154 self.running = false;
155 }
156
157 pub fn quit_and_stop(&mut self) {
158 self.stop_all_on_quit = true;
159 self.running = false;
160 }
161
162 pub fn running_count(&self) -> usize {
163 self.processes
164 .iter()
165 .filter(|p| p.state == crate::protocol::ProcessState::Running)
166 .count()
167 }
168
169 pub fn exited_count(&self) -> usize {
170 self.processes
171 .iter()
172 .filter(|p| p.state == crate::protocol::ProcessState::Exited)
173 .count()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::protocol::{ProcessInfo, ProcessState, Stream};
181
182 fn make_process(name: &str, state: ProcessState) -> ProcessInfo {
183 let exit_code = if state == ProcessState::Exited {
184 Some(0)
185 } else {
186 None
187 };
188 let uptime_secs = if state == ProcessState::Running {
189 Some(10)
190 } else {
191 None
192 };
193 ProcessInfo {
194 name: name.into(),
195 id: format!("p-{}", name),
196 pid: 100,
197 state,
198 exit_code,
199 uptime_secs,
200 command: "true".into(),
201 port: None,
202 url: None,
203 }
204 }
205
206 #[test]
207 fn test_select_next_wraps() {
208 let mut app = App::new();
209 app.update_processes(vec![
210 make_process("a", ProcessState::Running),
211 make_process("b", ProcessState::Running),
212 make_process("c", ProcessState::Running),
213 ]);
214 app.selected = 2; app.select_next();
216 assert_eq!(app.selected, 0);
217 }
218
219 #[test]
220 fn test_select_prev_wraps() {
221 let mut app = App::new();
222 app.update_processes(vec![
223 make_process("a", ProcessState::Running),
224 make_process("b", ProcessState::Running),
225 make_process("c", ProcessState::Running),
226 ]);
227 app.selected = 0;
228 app.select_prev();
229 assert_eq!(app.selected, 2);
230 }
231
232 #[test]
233 fn test_cycle_stream_mode() {
234 let mut app = App::new();
235 assert_eq!(app.stream_mode, StreamMode::Stdout);
236 app.cycle_stream_mode();
237 assert_eq!(app.stream_mode, StreamMode::Stderr);
238 app.cycle_stream_mode();
239 assert_eq!(app.stream_mode, StreamMode::Both);
240 app.cycle_stream_mode();
241 assert_eq!(app.stream_mode, StreamMode::Stdout);
242 }
243
244 #[test]
245 fn test_toggle_pause() {
246 let mut app = App::new();
247 assert!(!app.paused);
248 app.toggle_pause();
249 assert!(app.paused);
250 app.toggle_pause();
251 assert!(!app.paused);
252 }
253
254 #[test]
255 fn test_push_output() {
256 let mut app = App::new();
257 app.push_output("web", Stream::Stdout, "hello world");
258 let buf = app.buffers.get("web").unwrap();
259 let lines = buf.stdout_lines();
260 assert_eq!(lines.len(), 1);
261 assert_eq!(lines[0], "hello world");
262 }
263
264 #[test]
265 fn test_running_count() {
266 let mut app = App::new();
267 app.update_processes(vec![
268 make_process("a", ProcessState::Running),
269 make_process("b", ProcessState::Exited),
270 make_process("c", ProcessState::Running),
271 ]);
272 assert_eq!(app.running_count(), 2);
273 }
274
275 #[test]
276 fn test_exited_count() {
277 let mut app = App::new();
278 app.update_processes(vec![
279 make_process("a", ProcessState::Running),
280 make_process("b", ProcessState::Exited),
281 make_process("c", ProcessState::Exited),
282 ]);
283 assert_eq!(app.exited_count(), 2);
284 }
285}