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 { Stdout, Stderr, Both }
8
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum LineSource { Stdout, Stderr }
11
12pub struct OutputBuffer {
15 lines: VecDeque<(LineSource, String)>,
16 max_lines: usize,
17}
18
19impl OutputBuffer {
20 pub fn new(max_lines: usize) -> Self {
21 Self {
22 lines: VecDeque::with_capacity(max_lines),
23 max_lines,
24 }
25 }
26
27 pub fn push(&mut self, source: LineSource, line: String) {
28 if self.lines.len() == self.max_lines { self.lines.pop_front(); }
29 self.lines.push_back((source, line));
30 }
31
32 pub fn stdout_lines(&self) -> Vec<&str> {
33 self.lines.iter()
34 .filter(|(src, _)| *src == LineSource::Stdout)
35 .map(|(_, s)| s.as_str())
36 .collect()
37 }
38
39 pub fn stderr_lines(&self) -> Vec<&str> {
40 self.lines.iter()
41 .filter(|(src, _)| *src == LineSource::Stderr)
42 .map(|(_, s)| s.as_str())
43 .collect()
44 }
45
46 pub fn all_lines(&self) -> Vec<(LineSource, &str)> {
47 self.lines.iter().map(|(src, s)| (*src, s.as_str())).collect()
48 }
49}
50
51pub struct App {
52 pub processes: Vec<ProcessInfo>,
53 pub selected: usize,
54 pub buffers: HashMap<String, OutputBuffer>,
55 pub stream_mode: StreamMode,
56 pub paused: bool,
57 pub scroll_offsets: HashMap<String, usize>,
58 pub running: bool,
59 pub stop_all_on_quit: bool,
60}
61
62impl Default for App {
63 fn default() -> Self { Self::new() }
64}
65
66impl App {
67 pub fn new() -> Self {
68 Self {
69 processes: Vec::new(),
70 selected: 0,
71 buffers: HashMap::new(),
72 stream_mode: StreamMode::Stdout,
73 paused: false,
74 scroll_offsets: HashMap::new(),
75 running: true,
76 stop_all_on_quit: false,
77 }
78 }
79
80 pub fn update_processes(&mut self, processes: Vec<ProcessInfo>) {
81 self.processes = processes;
82 if self.selected >= self.processes.len() && !self.processes.is_empty() {
83 self.selected = self.processes.len() - 1;
84 }
85 }
86
87 pub fn selected_name(&self) -> Option<&str> {
88 self.processes.get(self.selected).map(|p| p.name.as_str())
89 }
90
91 pub fn select_next(&mut self) {
92 if !self.processes.is_empty() {
93 self.selected = (self.selected + 1) % self.processes.len();
94 }
95 }
96
97 pub fn select_prev(&mut self) {
98 if !self.processes.is_empty() {
99 self.selected = if self.selected == 0 {
100 self.processes.len() - 1
101 } else {
102 self.selected - 1
103 };
104 }
105 }
106
107 pub fn cycle_stream_mode(&mut self) {
108 self.stream_mode = match self.stream_mode {
109 StreamMode::Stdout => StreamMode::Stderr,
110 StreamMode::Stderr => StreamMode::Both,
111 StreamMode::Both => StreamMode::Stdout,
112 };
113 }
114
115 pub fn toggle_pause(&mut self) {
116 self.paused = !self.paused;
117 if !self.paused {
118 if let Some(name) = self.processes.get(self.selected).map(|p| p.name.clone()) {
120 self.scroll_offsets.remove(&name);
121 }
122 }
123 }
124
125 pub fn push_output(&mut self, process: &str, stream: Stream, line: &str) {
126 let buf = self.buffers
127 .entry(process.to_string())
128 .or_insert_with(|| OutputBuffer::new(MAX_BUFFER_LINES));
129 let source = match stream {
130 Stream::Stdout => LineSource::Stdout,
131 Stream::Stderr => LineSource::Stderr,
132 };
133 buf.push(source, line.to_string());
134 }
135
136 pub fn quit(&mut self) {
137 self.running = false;
138 }
139
140 pub fn quit_and_stop(&mut self) {
141 self.stop_all_on_quit = true;
142 self.running = false;
143 }
144
145 pub fn running_count(&self) -> usize {
146 self.processes.iter().filter(|p| p.state == crate::protocol::ProcessState::Running).count()
147 }
148
149 pub fn exited_count(&self) -> usize {
150 self.processes.iter().filter(|p| p.state == crate::protocol::ProcessState::Exited).count()
151 }
152}