Skip to main content

agent_procs/tui/
app.rs

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
12/// Single ring buffer storing all output with source tags.
13/// Stdout/stderr views are filtered from the same data — no duplication.
14pub 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            // Reset scroll offset to bottom on unpause
119            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}