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