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