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