rmux-server 0.1.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use rmux_core::Screen;
use rmux_proto::RmuxError;

use super::args::parse_single_argument;
use super::text::{
    classify_word_char, line_char, normalize_positions, owner_positions, CopyRange, WordClass,
};
use super::types::{
    ClearPolicy, CopyModeCommandOutcome, CopyPosition, SelectionMode, SelectionState,
};
use super::CopyModeState;

impl CopyModeState {
    pub(super) fn other_end(&mut self) -> Result<CopyModeCommandOutcome, RmuxError> {
        if self.view_mode {
            return Ok(
                self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::EmacsOnly)
            );
        }
        let Some(selection) = self.selection_snapshot() else {
            return Ok(
                self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::EmacsOnly)
            );
        };
        let current = self.cursor;
        self.cursor = selection.anchor;
        if let Some(existing) = &mut self.selection {
            existing.anchor = current;
            existing.end = self.cursor;
            existing.active = true;
        }
        self.ensure_cursor_visible();
        Ok(self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::EmacsOnly))
    }

    pub(super) fn selection_mode(
        &mut self,
        args: &[String],
    ) -> Result<CopyModeCommandOutcome, RmuxError> {
        let value = parse_single_argument("selection-mode", args)?;
        let mode = SelectionMode::parse(&value)
            .ok_or_else(|| RmuxError::Server(format!("invalid selection mode: {value}")))?;
        if let Some(selection) = &mut self.selection {
            selection.mode = mode;
            selection.end = self.cursor;
            selection.active = true;
        } else {
            self.selection = Some(SelectionState {
                anchor: self.cursor,
                end: self.cursor,
                mode,
                active: true,
            });
        }
        Ok(self.finish_policy(CopyModeCommandOutcome::nothing(), ClearPolicy::Never))
    }

    pub(super) fn begin_selection(&mut self) {
        self.selection = Some(SelectionState {
            anchor: self.cursor,
            end: self.cursor,
            mode: SelectionMode::Char,
            active: true,
        });
    }

    pub(super) fn mark_selection_in_viewport(
        &self,
        viewport: &mut Screen,
        selection: SelectionState,
    ) {
        let rows = usize::from(self.rows().max(1));
        let viewport_top = self.top_line;
        let viewport_bottom = viewport_top.saturating_add(rows);
        let (start, end) = normalize_positions(selection.anchor, selection.end);
        let rect_min_x = start.x.min(end.x);
        let rect_max_x = start.x.max(end.x);

        for y in start.y..=end.y {
            if y < viewport_top || y >= viewport_bottom {
                continue;
            }

            let (range_start, range_end) = match selection.mode {
                SelectionMode::Line => (0, self.line_end_x(y)),
                SelectionMode::Char | SelectionMode::Word if self.rectangle => {
                    (rect_min_x, rect_max_x)
                }
                SelectionMode::Char | SelectionMode::Word => {
                    let range_start = if y == start.y { start.x } else { 0 };
                    let range_end = if y == end.y {
                        end.x
                    } else {
                        self.line_end_x(y)
                    };
                    (range_start, range_end)
                }
            };

            viewport.mark_selected_row_range((y - viewport_top) as u32, range_start, range_end);
        }
    }

    pub(super) fn select_line(&mut self) {
        let x = self.line_end_x(self.cursor.y);
        self.selection = Some(SelectionState {
            anchor: CopyPosition {
                x: 0,
                y: self.cursor.y,
            },
            end: CopyPosition {
                x,
                y: self.cursor.y,
            },
            mode: SelectionMode::Line,
            active: true,
        });
    }

    pub(super) fn select_word(&mut self) {
        let range = self.word_selection_range(self.cursor);
        self.selection = Some(SelectionState {
            anchor: range.start,
            end: range.end,
            mode: SelectionMode::Word,
            active: true,
        });
        self.cursor = range.end;
        self.ensure_cursor_visible();
    }

    pub(super) fn selection_snapshot(&self) -> Option<SelectionState> {
        let mut selection = self.selection.clone()?;
        if selection.active {
            selection.end = self.cursor;
        }
        Some(selection)
    }

    pub(super) fn sync_selection_with_cursor(&mut self) {
        if let Some(selection) = &mut self.selection {
            if selection.active {
                selection.end = self.cursor;
            }
        }
    }

    pub(super) fn word_selection_range(&self, position: CopyPosition) -> CopyRange {
        let line = self.line(position.y);
        let positions = owner_positions(&line);
        if positions.is_empty() {
            return CopyRange {
                start: position,
                end: position,
            };
        }
        let owner = line.owning_cell_x(position.x).unwrap_or(position.x);
        let class = line_char(&line, owner)
            .map(|ch| classify_word_char(ch, &self.word_separators, false))
            .unwrap_or(WordClass::Space);
        let mut start = owner;
        let mut end = owner;
        for candidate in positions.iter().copied().rev().filter(|x| *x < owner) {
            let candidate_class = line_char(&line, candidate)
                .map(|ch| classify_word_char(ch, &self.word_separators, false))
                .unwrap_or(WordClass::Space);
            if candidate_class != class {
                break;
            }
            start = candidate;
        }
        for candidate in positions.iter().copied().filter(|x| *x > owner) {
            let candidate_class = line_char(&line, candidate)
                .map(|ch| classify_word_char(ch, &self.word_separators, false))
                .unwrap_or(WordClass::Space);
            if candidate_class != class {
                break;
            }
            end = candidate;
        }
        CopyRange {
            start: CopyPosition {
                x: start,
                y: position.y,
            },
            end: CopyPosition {
                x: end,
                y: position.y,
            },
        }
    }
}