Skip to main content

rmux_core/screen/
selection.rs

1use crate::grid::{GridCell, GridCellFlags};
2use crate::input::{GridAttr, COLOUR_DEFAULT, COLOUR_TERMINAL};
3use crate::style::{style_parse, Style, StyleCell};
4
5use super::Screen;
6
7impl Screen {
8    /// Returns whether any visible cell is currently marked as selected.
9    #[must_use]
10    pub fn has_selected_cells(&self) -> bool {
11        self.has_selected_cells
12    }
13
14    /// Marks one visible row range as selected.
15    pub fn mark_selected_row_range(&mut self, row: u32, start_x: u32, end_x: u32) {
16        let line_width = self.grid.sx();
17        if row >= self.grid.sy() || line_width == 0 {
18            return;
19        }
20
21        let Some(line) = self.grid.visible_line_mut(row) else {
22            return;
23        };
24        let start_x = start_x.min(line_width.saturating_sub(1));
25        let end_x = end_x.min(line_width.saturating_sub(1));
26        if start_x > end_x {
27            return;
28        }
29
30        let start_x = line.owning_cell_x(start_x).unwrap_or(start_x);
31        let end_x = line.owning_cell_x(end_x).unwrap_or(end_x);
32        let mut touched = false;
33        let mut x = start_x;
34        while x <= end_x {
35            let owner_x = line.owning_cell_x(x).unwrap_or(x);
36            let width = line
37                .cell(owner_x)
38                .map(|cell| u32::from(cell.width().max(1)))
39                .unwrap_or(1);
40
41            for offset in 0..width {
42                let cell_x = owner_x.saturating_add(offset);
43                if let Some(cell) = line.cell_mut(cell_x) {
44                    let mut flags = cell.flags();
45                    flags.insert(GridCellFlags::SELECTED);
46                    cell.set_flags(flags);
47                    self.has_selected_cells = true;
48                    touched = true;
49                }
50            }
51
52            x = owner_x.saturating_add(width.max(1));
53        }
54        if touched {
55            line.touch();
56        }
57    }
58
59    /// Clears all visible selected-cell markers.
60    pub fn clear_selected_cells(&mut self) {
61        if !self.has_selected_cells {
62            return;
63        }
64
65        let width = self.grid.sx();
66        for row in 0..self.grid.sy() {
67            let Some(line) = self.grid.visible_line_mut(row) else {
68                continue;
69            };
70            let mut touched = false;
71            for x in 0..width {
72                let Some(cell) = line.cell_mut(x) else {
73                    continue;
74                };
75                if !cell.flags().contains(GridCellFlags::SELECTED) {
76                    continue;
77                }
78                let mut flags = cell.flags();
79                flags.remove(GridCellFlags::SELECTED);
80                cell.set_flags(flags);
81                touched = true;
82            }
83            if touched {
84                line.touch();
85            }
86        }
87        self.has_selected_cells = false;
88    }
89
90    /// Overlays `style_input` onto all selected visible cells.
91    pub fn overlay_style_on_selected(&mut self, style_input: &str) {
92        if style_input.is_empty() {
93            self.clear_selected_cells();
94            return;
95        }
96        let Some(style) = parse_cell_overlay_style(style_input) else {
97            return;
98        };
99
100        let width = self.grid.sx();
101        for row in 0..self.grid.sy() {
102            let Some(line) = self.grid.visible_line_mut(row) else {
103                continue;
104            };
105            let mut touched = false;
106            for x in 0..width {
107                let Some(cell) = line.cell_mut(x) else {
108                    continue;
109                };
110                if !cell.flags().contains(GridCellFlags::SELECTED) || cell.is_padding() {
111                    continue;
112                }
113                apply_cell_overlay_style(cell, &style);
114                touched = true;
115            }
116            if touched {
117                line.touch();
118            }
119        }
120        self.clear_selected_cells();
121    }
122
123    /// Overlays `style` onto one inclusive visible row range.
124    pub fn overlay_style_on_row_range(
125        &mut self,
126        row: u32,
127        start_x: u32,
128        end_x: u32,
129        style: &Style,
130    ) {
131        if row >= self.grid.sy() || self.grid.sx() == 0 {
132            return;
133        }
134        let line_width = self.grid.sx();
135        let Some(line) = self.grid.visible_line_mut(row) else {
136            return;
137        };
138        let start_x = start_x.min(line_width.saturating_sub(1));
139        let end_x = end_x.min(line_width.saturating_sub(1));
140        if start_x > end_x {
141            return;
142        }
143
144        let start_x = line.owning_cell_x(start_x).unwrap_or(start_x);
145        let end_x = line.owning_cell_x(end_x).unwrap_or(end_x);
146        let mut touched = false;
147        let mut x = start_x;
148        while x <= end_x {
149            let owner_x = line.owning_cell_x(x).unwrap_or(x);
150            let width = line
151                .cell(owner_x)
152                .map(|cell| u32::from(cell.width().max(1)))
153                .unwrap_or(1);
154            if let Some(cell) = line.cell_mut(owner_x) {
155                if !cell.is_padding() {
156                    apply_cell_overlay_style(cell, style);
157                    touched = true;
158                }
159            }
160            let next = owner_x.saturating_add(width.max(1));
161            if next <= x {
162                break;
163            }
164            x = next;
165        }
166        if touched {
167            line.touch();
168        }
169    }
170}
171
172fn parse_cell_overlay_style(style_input: &str) -> Option<Style> {
173    let base = StyleCell::default();
174    let mut style = Style::with_cell(base);
175    style_parse(&mut style, &base, style_input).ok()?;
176    Some(style)
177}
178
179fn apply_cell_overlay_style(cell: &mut GridCell, style: &Style) {
180    // Cell overlay composition, probed against the pinned tmux 3.7b oracle
181    // (2026-07-11): explicit fg/bg replace the cell's colours. A complete
182    // style drops the cell's attributes, while a partial style preserves
183    // them and unions attributes supplied by the overlay. `noattr` always
184    // drops inherited attributes.
185    let fg_set = !matches!(style.cell.fg, COLOUR_DEFAULT | COLOUR_TERMINAL);
186    let bg_set = !matches!(style.cell.bg, COLOUR_DEFAULT | COLOUR_TERMINAL);
187    let style_attr = style.cell.attr & !GridAttr::NOATTR;
188    let attr = if style.cell.attr & GridAttr::NOATTR != 0 || (fg_set && bg_set) {
189        style_attr | (cell.attr() & GridAttr::CHARSET)
190    } else {
191        style_attr | cell.attr()
192    };
193    cell.set_attr(attr);
194    if fg_set {
195        cell.set_fg(style.cell.fg);
196    }
197    if bg_set {
198        cell.set_bg(style.cell.bg);
199    }
200    cell.set_us(style.cell.us);
201}