Skip to main content

fm/io/
cursor_text.rs

1use std::io;
2
3use crossterm::cursor::SetCursorStyle;
4use ratatui::{
5    buffer::Buffer,
6    layout::{Position, Rect, Size},
7};
8
9use crate::config::Bindings;
10
11/// Different states in which the cursor can be.
12/// - Inactive: the cursor isn't being used and it should be used,
13/// - Movement: the cursor can move but no selection is made,
14/// - Selection: the cursor can move and the selection is udapted.
15#[derive(Default, Clone, Copy)]
16enum CursorState {
17    #[default]
18    Inactive,
19    Movement,
20    Selection,
21}
22
23impl CursorState {
24    fn is_active(&self) -> bool {
25        !matches!(self, Self::Inactive)
26    }
27
28    pub fn is_selecting(&self) -> bool {
29        matches!(self, Self::Selection)
30    }
31
32    /// Transition machine:
33    /// Inactive -> does nothing;
34    /// Movement <-> Selection.
35    fn toggle_selection(&mut self) {
36        match self {
37            Self::Inactive => (),
38            Self::Movement => {
39                *self = Self::Selection;
40            }
41            Self::Selection => {
42                *self = Self::Movement;
43            }
44        }
45    }
46
47    fn set_active(&mut self) {
48        *self = Self::Movement;
49    }
50}
51
52/// Different direction in which the cursor can move.
53#[derive(Default, Clone, Copy)]
54pub enum CursorDirection {
55    #[default]
56    Down,
57    Up,
58    Left,
59    Right,
60}
61
62impl CursorDirection {
63    fn go_from(&self, x: u16, y: u16) -> Position {
64        let mut x = x;
65        let mut y = y;
66        match self {
67            CursorDirection::Down => {
68                y = y.saturating_add(1);
69            }
70            CursorDirection::Up => {
71                y = y.saturating_sub(1);
72            }
73            CursorDirection::Left => {
74                x = x.saturating_sub(1);
75            }
76            CursorDirection::Right => {
77                x = x.saturating_add(1);
78            }
79        }
80        Position::new(x, y)
81    }
82}
83
84/// `Cursor` is used to select and copy or log text directly from fm output.
85/// Once the cursor is active, you can't move in the file tree or open menus etc.
86/// You can only :
87/// - toggle the selection state,
88/// - move the cursor with keys or mouse,
89/// - copy the selected chars (only while selecting)
90/// - leave the cursor and go back to normal usage of fm,
91/// - exit fm completely
92///
93/// It's a way to allow copying text without having to exit fm or open a new shell.
94///
95/// `Cursor` has a state (inactive, movement, selecting), knows its position and where it started its selection.
96/// We also store the associated binds to help the user.
97#[derive(Default, Clone)]
98pub struct Cursor {
99    state: CursorState,
100    cursor: Option<Position>,
101    origin: Option<Position>,
102    rect: Option<Rect>,
103    /// Are we dragging the cursor with the mouse ?
104    pub is_dragging: bool,
105    /// What is the keybind associated to leave menu ?
106    pub leave_bind: String,
107    /// What is the keybind associated to entering the cursor ? It's used to toggle selection state.
108    pub enter_bind: String,
109    /// What is the bind associated to copy/paste. Used to copy the selection to clipboard and log it.
110    pub copy_bind: String,
111}
112
113impl Cursor {
114    /// Creates a new cursor with binds read from keybinds.
115    pub fn new(binds: &Bindings) -> Self {
116        let reversed = binds.keybind_reversed();
117        let leave_bind = reversed.get("ResetMode").cloned().unwrap_or_default();
118        let enter_bind = reversed.get("Cursor").cloned().unwrap_or_default();
119        let copy_bind = reversed.get("CopyPaste").cloned().unwrap_or_default();
120        Self {
121            state: CursorState::default(),
122            cursor: None,
123            origin: None,
124            rect: None,
125            is_dragging: false,
126            leave_bind,
127            enter_bind,
128            copy_bind,
129        }
130    }
131
132    /// Copy of the inner rect.
133    pub fn rect(&self) -> Option<Rect> {
134        self.rect
135    }
136
137    /// Position of the cursor if any
138    pub fn cursor(&self) -> Option<Position> {
139        self.cursor
140    }
141
142    /// True iff the cursor is in active mode (either movement or selecting)
143    pub fn is_active(&self) -> bool {
144        self.state.is_active()
145    }
146
147    /// True iff the cursor is selecting.
148    pub fn is_selecting(&self) -> bool {
149        self.state.is_selecting()
150    }
151
152    /// Reset the cursor.
153    /// set state to inactive, erase cusrsor, origin & rect and set is_dragging to false.
154    pub fn reset(&mut self) {
155        self.state = CursorState::default();
156        self.cursor = None;
157        self.origin = None;
158        self.rect = None;
159        self.is_dragging = false;
160    }
161
162    /// Set the state from inactive to movement or toggle between movement & selecting.
163    pub fn toggle(&mut self, position: Position) {
164        if self.state.is_active() {
165            self.toggle_selection();
166        } else {
167            self.start_cursor(position);
168        }
169    }
170
171    /// Set default values for entering selection from this position.
172    /// Wattchout: it also changes the _TERMINAL_ cursor to "steady block".
173    fn start_cursor(&mut self, position: Position) {
174        let _ = crossterm::execute!(io::stdout(), SetCursorStyle::SteadyBlock);
175        self.state.set_active();
176        self.cursor = Some(position);
177        self.origin = Some(position);
178        self.rect = None;
179    }
180
181    /// Toggle between selecting & movement.
182    /// Does nothing if cursor isn't already active.
183    fn toggle_selection(&mut self) {
184        if !self.state.is_active() {
185            return;
186        }
187        self.state.toggle_selection();
188        if self.state.is_selecting() {
189            self.origin = self.cursor;
190        } else {
191            self.clear_selection();
192        }
193    }
194
195    /// Clear the current selected rect.
196    fn clear_selection(&mut self) {
197        self.rect = None;
198    }
199
200    /// Move the cursor in given direction.
201    /// Cursor is clamped to the the screen.
202    /// It's coordinates can't be equal or bigger to terminal height or width.
203    pub fn move_cursor(&mut self, direction: CursorDirection, Size { width, height }: Size) {
204        let Some(Position { x, y }) = self.cursor else {
205            return;
206        };
207        let new_pos = direction
208            .go_from(x, y)
209            .clamp(Position::ORIGIN, Position::new(width, height));
210        self.cursor = Some(new_pos);
211    }
212
213    /// Move the cursor to `position`
214    /// Does nothing if cursor isn't active.
215    pub fn move_cursor_to(&mut self, position: Position) {
216        if !self.state.is_active() {
217            return;
218        }
219        self.cursor = Some(position);
220    }
221
222    /// Move the origin of selection to `position`
223    /// Does nothing if cursor isn't active.
224    pub fn move_origin_to(&mut self, position: Position) {
225        if !self.state.is_active() {
226            return;
227        }
228        self.origin = Some(position);
229    }
230
231    /// Update the selection from origin to current position.
232    /// Does nothing if cursor isn't active.
233    pub fn extend_selection(&mut self) {
234        if !self.state.is_selecting() {
235            return;
236        }
237        let start = self.origin.expect("Should be set");
238        let end = self.cursor.expect("Should be set");
239        let x = start.x.min(end.x);
240        let y = start.y.min(end.y);
241        let width = u16::abs_diff(start.x, end.x) + 1;
242        let height = u16::abs_diff(start.y, end.y) + 1;
243        self.rect = Some(Rect::new(x, y, width, height));
244    }
245
246    /// Used to allow selecting text with the mouse.
247    /// Either start selecting from previous position if we enter the "dragging" state
248    /// Move the cursor to the current mouse position.
249    /// Or extend selection.
250    pub fn mouse_drag(&mut self, row: u16, col: u16) {
251        let pos = Position::new(col, row);
252        self.move_cursor_to(pos);
253        if self.is_dragging {
254            self.extend_selection();
255        } else {
256            self.move_origin_to(pos);
257            self.is_dragging = true;
258        }
259    }
260
261    /// Stop dragging.
262    pub fn stop_drag(&mut self) {
263        self.is_dragging = false;
264    }
265}
266
267/// Returns a string made of the chars from the buffer which are in the given rect.
268pub fn read_rect_from_buffer(rect: &Rect, buffer: &Buffer) -> String {
269    let mut content = String::new();
270    for y in rect.y..rect.y + rect.height {
271        for x in rect.x..rect.x + rect.width {
272            let Some(cell) = buffer.cell((x, y)) else {
273                // Should never happen since the rect is inside the displayed window.
274                continue;
275            };
276            content.push_str(cell.symbol());
277        }
278        content.push('\n')
279    }
280    content
281}