1use crate::grid::{Grid, GridCell, GridCellFlags, GridLine};
4use crate::hyperlinks::Hyperlinks;
5use crate::input::mode;
6use crate::input::{CellState, SavedState, ScreenWriter, COLOUR_DEFAULT};
7use crate::utf8::{combine_char as utf8_combine_char, CombineResult, Utf8Config};
8use rmux_proto::TerminalSize;
9
10#[path = "screen/capture.rs"]
11mod capture;
12#[path = "screen/selection.rs"]
13mod selection;
14#[path = "screen/view.rs"]
15mod view;
16#[path = "screen/writer.rs"]
17mod writer;
18
19pub use view::{ScreenCellView, ScreenLineView};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22struct SavedGrid {
23 grid: Grid,
24 history_enabled: bool,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Screen {
31 grid: Grid,
32 cursor_x: u32,
33 cursor_y: u32,
34 pending_wrap: bool,
35 saved_cursor_x: Option<u32>,
36 saved_cursor_y: Option<u32>,
37 saved_cursor_pending_wrap: bool,
38 saved_state: SavedState,
39 saved_grid: Option<SavedGrid>,
40 rupper: u32,
41 rlower: u32,
42 mode: u32,
43 cursor_style: u32,
44 title: String,
45 window_name: String,
46 path: String,
47 title_stack: Vec<String>,
48 tabs: Vec<bool>,
49 hyperlinks: Hyperlinks,
50 active_hyperlink: u32,
51 bell_count: u64,
52 utf8_config: Utf8Config,
53}
54
55impl Screen {
56 #[must_use]
58 pub fn new(size: TerminalSize, history_limit: usize) -> Self {
59 let grid = Grid::new(size, history_limit);
60 let mut screen = Self {
61 grid,
62 cursor_x: 0,
63 cursor_y: 0,
64 pending_wrap: false,
65 saved_cursor_x: None,
66 saved_cursor_y: None,
67 saved_cursor_pending_wrap: false,
68 saved_state: SavedState::default(),
69 saved_grid: None,
70 rupper: 0,
71 rlower: u32::from(size.rows.max(1)).saturating_sub(1),
72 mode: mode::MODE_CURSOR | mode::MODE_WRAP,
73 cursor_style: 0,
74 title: String::new(),
75 window_name: String::new(),
76 path: String::new(),
77 title_stack: Vec::new(),
78 tabs: Vec::new(),
79 hyperlinks: Hyperlinks::new(),
80 active_hyperlink: 0,
81 bell_count: 0,
82 utf8_config: Utf8Config::default(),
83 };
84 screen.reset_tabs();
85 screen
86 }
87
88 #[must_use]
90 pub const fn mode(&self) -> u32 {
91 self.mode
92 }
93
94 #[must_use]
96 pub const fn cursor_style(&self) -> u32 {
97 self.cursor_style
98 }
99
100 #[must_use]
102 pub fn size(&self) -> TerminalSize {
103 self.grid.size()
104 }
105
106 #[cfg_attr(not(test), allow(dead_code))]
107 #[must_use]
108 pub(crate) fn grid(&self) -> &Grid {
109 &self.grid
110 }
111
112 #[must_use]
114 pub fn title(&self) -> &str {
115 &self.title
116 }
117
118 pub fn set_title(&mut self, title: impl Into<String>) {
120 self.title = title.into();
121 }
122
123 #[must_use]
125 pub fn path(&self) -> &str {
126 &self.path
127 }
128
129 #[must_use]
131 pub fn is_alternate(&self) -> bool {
132 self.saved_grid.is_some()
133 }
134
135 #[must_use]
137 pub fn history_limit(&self) -> usize {
138 self.grid.hlimit()
139 }
140
141 #[must_use]
143 pub fn history_size(&self) -> usize {
144 self.grid.hsize()
145 }
146
147 #[must_use]
149 pub const fn cursor_position(&self) -> (u32, u32) {
150 (self.cursor_x, self.cursor_y)
151 }
152
153 #[must_use]
155 pub fn cursor_absolute_y(&self) -> usize {
156 self.grid.hsize() + self.cursor_y as usize
157 }
158
159 #[must_use]
161 pub fn absolute_line_count(&self) -> usize {
162 self.grid.hsize() + self.grid.sy() as usize
163 }
164
165 pub fn delete_visible_line(&mut self, y: u32) -> bool {
170 if y >= self.grid.sy() {
171 return false;
172 }
173
174 let cursor_x = self.cursor_x;
175 let cursor_y = self.cursor_y;
176 let rupper = self.rupper;
177 let rlower = self.rlower;
178
179 self.cursor_x = 0;
180 self.cursor_y = y;
181 self.pending_wrap = false;
182 self.rupper = 0;
183 self.rlower = self.grid.sy().saturating_sub(1);
184 self.delete_line(1, COLOUR_DEFAULT);
185
186 self.cursor_y = if cursor_y > y {
187 cursor_y.saturating_sub(1)
188 } else {
189 cursor_y
190 }
191 .min(self.grid.sy().saturating_sub(1));
192 self.cursor_x = cursor_x.min(self.grid.sx().saturating_sub(1));
193 self.pending_wrap = false;
194 self.rupper = rupper;
195 self.rlower = rlower;
196 true
197 }
198
199 pub fn delete_absolute_line(&mut self, absolute_y: usize) -> bool {
201 let history_size = self.grid.hsize();
202 let visible_y = absolute_y.saturating_sub(history_size);
203 let removed = self.grid.remove_absolute_line(absolute_y);
204 if !removed {
205 return false;
206 }
207
208 if absolute_y >= history_size {
209 let visible_y = visible_y as u32;
210 if visible_y < self.cursor_y {
211 self.cursor_y = self.cursor_y.saturating_sub(1);
212 }
213 }
214 self.pending_wrap = false;
215 true
216 }
217
218 #[must_use]
220 pub fn history_bytes(&self) -> usize {
221 self.grid.history_byte_size()
222 }
223
224 pub fn take_bell_count(&mut self) -> u64 {
226 let bell_count = self.bell_count;
227 self.bell_count = 0;
228 bell_count
229 }
230
231 #[must_use]
233 pub fn hyperlink_uri(&self, inner_id: u32) -> Option<&str> {
234 self.hyperlinks
235 .get(inner_id)
236 .map(|entry| entry.uri.as_str())
237 }
238
239 pub fn set_history_limit(&mut self, limit: usize) {
241 self.grid.set_hlimit(limit);
242 }
243
244 pub fn set_utf8_config(&mut self, utf8_config: Utf8Config) {
246 self.utf8_config = utf8_config;
247 }
248
249 pub fn resize(&mut self, size: TerminalSize) {
251 let cols = u32::from(size.cols.max(1));
252 let rows = u32::from(size.rows.max(1));
253 if cols != self.grid.sx() {
254 self.grid.resize_width(cols, COLOUR_DEFAULT);
255 self.reset_tabs();
256 }
257 if rows != self.grid.sy() {
258 self.grid
259 .resize_height(rows, &mut self.cursor_y, COLOUR_DEFAULT);
260 }
261 self.rupper = 0;
262 self.rlower = rows.saturating_sub(1);
263 self.cursor_x = self.cursor_x.min(self.max_cursor_x());
264 self.pending_wrap &= self.cursor_x == self.max_cursor_x();
265 }
266
267 pub fn clear_history_and_hyperlinks(&mut self, reset_hyperlinks: bool) {
269 self.grid.clear_history();
270 if reset_hyperlinks {
271 self.hyperlinks.reset();
272 }
273 }
274
275 fn reset_tabs(&mut self) {
276 self.tabs = vec![false; self.grid.sx() as usize];
277 for column in (8..self.grid.sx()).step_by(8) {
278 self.tabs[column as usize] = true;
279 }
280 }
281
282 fn max_cursor_x(&self) -> u32 {
283 self.grid.sx().saturating_sub(1)
284 }
285
286 fn cursor_column(&self) -> u32 {
287 self.cursor_x.min(self.max_cursor_x())
288 }
289
290 fn current_line_mut(&mut self) -> Option<&mut GridLine> {
291 self.grid.visible_line_mut(self.cursor_y)
292 }
293
294 fn clear_pending_wrap(&mut self) {
295 self.pending_wrap = false;
296 }
297
298 fn restore_cursor_position(&mut self, x: u32, y: u32, pending_wrap: bool) {
299 self.cursor_x = x.min(self.max_cursor_x());
300 self.cursor_y = y.min(self.grid.sy().saturating_sub(1));
301 self.pending_wrap = pending_wrap
302 && (self.mode & mode::MODE_WRAP) != 0
303 && self.cursor_x == self.max_cursor_x();
304 }
305
306 fn apply_pending_wrap(&mut self) {
307 if !self.pending_wrap || (self.mode & mode::MODE_WRAP) == 0 {
308 self.pending_wrap = false;
309 return;
310 }
311
312 if let Some(line) = self.current_line_mut() {
313 line.set_wrapped(true);
314 }
315 self.pending_wrap = false;
316 self.linefeed(false, COLOUR_DEFAULT);
317 self.cursor_x = 0;
318 }
319
320 fn blank_cell(&self, bg: i32) -> GridCell {
321 GridCell::blank_with_bg(bg)
322 }
323
324 fn overwrite_for_write(&mut self, x: u32, width: u32) {
325 let sx = self.grid.sx();
326 let blank = GridCell::blank_with_bg(COLOUR_DEFAULT);
327 let Some(line) = self.current_line_mut() else {
328 return;
329 };
330
331 let current_is_padding = line.is_padding_cell(x);
332 if current_is_padding {
333 if let Some(owner_x) = line.owning_cell_x(x).filter(|owner_x| *owner_x != x) {
334 if let Some(owner) = line.cell_mut(owner_x) {
335 *owner = blank.clone();
336 }
337 }
338 }
339
340 let clear_following_padding = width != 1
341 || line
342 .cell(x)
343 .is_some_and(|cell| cell.width() != 1 || cell.is_padding());
344 if clear_following_padding {
345 let mut clear_x = x.saturating_add(width);
346 while clear_x < sx && line.is_padding_cell(clear_x) {
347 if let Some(cell) = line.cell_mut(clear_x) {
348 *cell = blank.clone();
349 }
350 clear_x += 1;
351 }
352 }
353
354 line.touch();
355 }
356
357 fn clear_line_range(&mut self, y: u32, start: u32, end_inclusive: u32, bg: i32) {
358 let sx = self.grid.sx();
359 let end = end_inclusive.min(sx.saturating_sub(1));
360 let Some(line) = self.grid.visible_line_mut(y) else {
361 return;
362 };
363 for x in start.min(sx)..=end {
364 if let Some(cell) = line.cell_mut(x) {
365 *cell = GridCell::blank_with_bg(bg);
366 }
367 }
368 line.set_wrapped(false);
369 line.touch();
370 }
371
372 fn clear_screen_region(&mut self, start_y: u32, end_y_inclusive: u32, bg: i32) {
373 for y in start_y..=end_y_inclusive.min(self.grid.sy().saturating_sub(1)) {
374 if let Some(line) = self.grid.visible_line_mut(y) {
375 line.clear(bg);
376 }
377 }
378 }
379
380 fn write_char(&mut self, ch: char, cell: &CellState, acs: bool) {
381 if self.grid.sx() == 0 || self.grid.sy() == 0 {
382 return;
383 }
384
385 let ch = if acs { translate_acs(ch) } else { ch };
386 let width = u32::from(self.utf8_config.width(ch));
387 if self.combine_char(ch) {
388 return;
389 }
390
391 let automatic_wrap_continuation = self.pending_wrap && (self.mode & mode::MODE_WRAP) != 0;
392 self.apply_pending_wrap();
393
394 if (self.mode & mode::MODE_WRAP) != 0
395 && self.cursor_x > self.grid.sx().saturating_sub(width)
396 {
397 if let Some(line) = self.current_line_mut() {
398 line.set_wrapped(true);
399 }
400 self.linefeed(false, COLOUR_DEFAULT);
401 self.cursor_x = 0;
402 }
403
404 if (self.mode & mode::MODE_WRAP) == 0
405 && width > 1
406 && (width > self.grid.sx() || self.cursor_x > self.grid.sx().saturating_sub(width))
407 {
408 return;
409 }
410
411 if self.cursor_y >= self.grid.sy()
412 || self.cursor_column() > self.grid.sx().saturating_sub(width)
413 {
414 return;
415 }
416
417 let x = self.cursor_column();
418 if x == 0 && !automatic_wrap_continuation {
419 self.break_previous_wrapped_line();
420 }
421 self.overwrite_for_write(x, width);
422 if let Some(line) = self.current_line_mut() {
423 if let Some(target) = line.cell_mut(x) {
424 *target = GridCell::from_state(
425 ch,
426 u8::try_from(width).unwrap_or(1),
427 cell,
428 GridCellFlags::default(),
429 );
430 }
431 for offset in 1..width {
432 if let Some(padding) = line.cell_mut(x + offset) {
433 *padding = GridCell::from_state(' ', 0, cell, GridCellFlags::PADDING);
434 }
435 }
436 line.touch();
437 }
438
439 if (self.mode & mode::MODE_WRAP) != 0 && x + width >= self.grid.sx() {
440 self.cursor_x = self.max_cursor_x();
441 self.pending_wrap = true;
442 } else {
443 self.cursor_x = x.saturating_add(width).min(self.max_cursor_x());
444 self.pending_wrap = false;
445 }
446 }
447
448 fn break_previous_wrapped_line(&mut self) {
449 if self.cursor_y == 0 {
450 return;
451 }
452 if let Some(previous) = self.grid.visible_line_mut(self.cursor_y - 1) {
453 previous.set_wrapped(false);
454 }
455 }
456
457 fn combine_char(&mut self, ch: char) -> bool {
458 let mut x = self.cursor_column();
459 if self.pending_wrap {
460 x = self.max_cursor_x();
461 } else if x == 0 {
462 return matches!(
463 utf8_combine_char(None, ch, &self.utf8_config),
464 CombineResult::Discard
465 );
466 } else {
467 x -= 1;
468 }
469
470 let Some(line) = self.grid.visible_line_mut(self.cursor_y) else {
471 return matches!(
472 utf8_combine_char(None, ch, &self.utf8_config),
473 CombineResult::Discard
474 );
475 };
476 let target_x = line.owning_cell_x(x).unwrap_or(x);
477 let previous = line
478 .cell(target_x)
479 .map(|cell| (cell.text().to_owned(), cell.width()));
480 let result = utf8_combine_char(
481 previous
482 .as_ref()
483 .map(|(text, width)| (text.as_str(), *width)),
484 ch,
485 &self.utf8_config,
486 );
487
488 match result {
489 CombineResult::Standalone { .. } => false,
490 CombineResult::Discard => true,
491 CombineResult::Combined { text, width } => {
492 let previous_width = previous.as_ref().map_or(0, |(_, width)| *width);
493 if let Some(cell) = line.cell_mut(target_x) {
494 cell.set_text(text);
495 cell.set_width(width);
496 if width == 2 {
497 let mut padding = cell.clone();
498 padding.set_text(" ".to_owned());
499 padding.set_width(0);
500 padding.set_flags(GridCellFlags::PADDING);
501 if let Some(padding_cell) = line.cell_mut(target_x + 1) {
502 *padding_cell = padding;
503 }
504 }
505 line.touch();
506 }
507 if previous_width == 1 && width == 2 && !self.pending_wrap {
508 let next_cursor = target_x.saturating_add(2);
509 if next_cursor >= self.grid.sx() {
510 self.cursor_x = self.max_cursor_x();
511 self.pending_wrap = (self.mode & mode::MODE_WRAP) != 0;
512 } else {
513 self.cursor_x = next_cursor;
514 }
515 }
516 true
517 }
518 }
519 }
520
521 fn parse_hyperlink(data: &str) -> (Option<String>, String) {
522 let (params, uri) = data.split_once(';').unwrap_or((data, ""));
523 let mut internal_id = None;
524 for part in params.split(':') {
525 if let Some(value) = part.strip_prefix("id=") {
526 internal_id = Some(value.to_owned());
527 }
528 }
529 (internal_id, uri.to_owned())
530 }
531
532 fn previous_cell_x(&self, y: u32, x: u32) -> u32 {
533 let Some(candidate) = x.checked_sub(1) else {
534 return 0;
535 };
536 let Some(line) = self.grid.visible_line(y) else {
537 return candidate;
538 };
539 if line.is_padding_cell(candidate) {
540 return line.owning_cell_x(candidate).unwrap_or(candidate);
541 }
542 candidate
543 }
544
545 fn next_cell_x(&self, y: u32, x: u32) -> u32 {
546 let max_x = self.grid.sx().saturating_sub(1);
547 if x >= max_x {
548 return max_x;
549 }
550
551 let Some(line) = self.grid.visible_line(y) else {
552 return x.saturating_add(1).min(max_x);
553 };
554 let owner_x = line.owning_cell_x(x).unwrap_or(x);
555 let width = line
556 .cell(owner_x)
557 .map_or(1, |cell| u32::from(cell.width().max(1)));
558
559 owner_x.saturating_add(width).min(max_x)
560 }
561}
562
563fn translate_acs(ch: char) -> char {
564 match ch {
565 'j' => '┘',
566 'k' => '┐',
567 'l' => '┌',
568 'm' => '└',
569 'n' => '┼',
570 'q' => '─',
571 't' => '├',
572 'u' => '┤',
573 'v' => '┴',
574 'w' => '┬',
575 'x' => '│',
576 _ => ch,
577 }
578}
579
580#[cfg(test)]
581#[path = "screen/tests.rs"]
582mod tests;