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