artifactize_core/monitor/
session.rs1use crate::agent::session::{
4 document::{Anchor, Mode, Move, Position, Scroll},
5 live::{Reader, Source, Window},
6};
7
8#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
10pub struct ReaderId(u64);
11
12impl ReaderId {
13 pub fn next(self) -> Self {
15 Self(
16 self.0
17 .checked_add(1)
18 .expect("session reader identity exhausted"),
19 )
20 }
21}
22
23impl From<u64> for ReaderId {
24 fn from(value: u64) -> Self {
25 Self(value)
26 }
27}
28
29pub struct Live {
30 pub serial: ReaderId,
31 pub source: Source,
32 pub reader: Option<Reader>,
33 pub scroll: Scroll,
34 pub window: Window,
35 width: usize,
36 height: usize,
37 anchor: Anchor,
38 position: Position,
39 revision: u64,
40 rendered_width: usize,
41 dirty: bool,
42 expanded: std::collections::BTreeSet<crate::agent::session::transcript::BlockId>,
43}
44
45pub struct Job {
46 pub serial: ReaderId,
47 pub revision: u64,
48 pub width: usize,
49 pub height: usize,
50 pub position: Position,
51 pub reader: Reader,
52 pub expanded: std::collections::BTreeSet<crate::agent::session::transcript::BlockId>,
53}
54impl Live {
55 pub fn new(serial: ReaderId, source: Source) -> Self {
56 Self {
57 serial,
58 source: source.clone(),
59 reader: Some(Reader::new(source)),
60 scroll: Scroll::default(),
61 window: Window::default(),
62 width: 0,
63 height: 0,
64 anchor: Anchor::default(),
65 position: Position::Bottom,
66 revision: 0,
67 rendered_width: 0,
68 dirty: true,
69 expanded: std::collections::BTreeSet::new(),
70 }
71 }
72 pub fn geometry(&mut self, width: usize, height: usize) {
73 if self.width != width || self.height != height {
74 self.width = width;
75 self.height = height;
76 self.revision += 1;
79 self.dirty = true;
80 }
81 }
82 pub fn invalidate(&mut self) {
83 self.dirty = true;
84 }
85
86 pub fn refresh(&mut self, source: &Source) -> bool {
90 if self.source.reference != source.reference {
91 return false;
92 }
93 self.source = source.clone();
94 if let Some(reader) = &mut self.reader {
95 reader.source = source.clone();
96 }
97 self.invalidate();
98 true
99 }
100 #[cfg(any(test, feature = "test-support"))]
101 pub fn expanded(&self, id: crate::agent::session::transcript::BlockId) -> bool {
102 self.expanded.contains(&id)
103 }
104 pub fn toggle(&mut self, id: crate::agent::session::transcript::BlockId) {
105 if !self.expanded.remove(&id) {
106 self.expanded.insert(id);
107 }
108 self.revision += 1;
109 self.dirty = true;
110 }
111 pub fn toggle_visible(&mut self) {
112 if let Some((_, id)) = self.window.groups.first().copied() {
113 self.toggle(id);
114 }
115 }
116 pub fn movement(&mut self, movement: Move) {
117 let before = self.scroll.top;
118 self.scroll.apply(movement);
119 let delta = self.scroll.top as isize - before as isize;
120 self.position = match movement {
121 Move::Bottom => Position::Bottom,
122 Move::Top => Position::Anchor(Anchor::default()),
123 Move::Up(_) | Move::Down(_) if self.scroll.mode == Mode::Following => Position::Bottom,
124 Move::Up(_) | Move::Down(_) => match self.position {
125 Position::Anchor(anchor) => Position::Relative {
126 anchor,
127 rows: delta,
128 width: self.rendered_width.max(1),
129 },
130 Position::Relative {
131 anchor,
132 rows,
133 width,
134 } => Position::Relative {
135 anchor,
136 rows: rows.saturating_add(delta),
137 width,
138 },
139 _ => Position::Relative {
140 anchor: self.anchor,
141 rows: self.scroll.top as isize - self.window.top as isize,
142 width: self.rendered_width.max(1),
143 },
144 },
145 };
146 self.revision += 1;
147 self.dirty = true;
148 }
149 pub fn job(&mut self) -> Option<Job> {
150 if !self.dirty || self.width == 0 || self.height == 0 {
151 return None;
152 }
153 let reader = self.reader.take()?;
154 self.dirty = false;
155 Some(Job {
156 serial: self.serial,
157 revision: self.revision,
158 width: self.width,
159 height: self.height,
160 position: self.position,
161 reader,
162 expanded: self.expanded.clone(),
163 })
164 }
165 pub fn finish(&mut self, job: Job, window: Window) {
166 let mut reader = job.reader;
167 reader.source = self.source.clone();
168 self.reader = Some(reader);
169 if window.reset {
171 self.scroll = Scroll::default();
172 self.position = Position::Bottom;
173 self.dirty = true;
174 }
175 if self.revision != job.revision {
176 self.dirty = true;
177 return;
178 }
179 if window.loading && window.rows.is_empty() && window.status.is_none() {
180 self.window.loading = true;
182 self.position = window.position;
183 } else {
184 self.scroll
185 .update(window.total, self.height, Some(window.top));
186 self.anchor = window.anchor;
187 self.rendered_width = job.width;
188 if let Some(reader) = &mut self.reader {
189 reader.commit(job.width);
190 }
191 self.position = match self.scroll.mode {
192 Mode::Following => Position::Bottom,
193 Mode::Paused => Position::Anchor(self.anchor),
194 };
195 self.window = window;
196 }
197 self.dirty |= self.window.loading;
198 }
199 pub fn indicator(&self) -> String {
200 let position = if self.scroll.total == 0 {
201 "0/0".into()
202 } else {
203 format!("{}/{}", self.scroll.top + 1, self.scroll.total)
204 };
205 format!(
206 "{position} {} {}{}",
207 if self.scroll.at_bottom() {
208 "bottom"
209 } else {
210 "history"
211 },
212 if self.scroll.mode == Mode::Following {
213 "following"
214 } else {
215 "paused"
216 },
217 if self.window.loading {
218 " · indexing…"
219 } else {
220 ""
221 }
222 )
223 }
224}