rmux-server 0.1.1

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use std::ops::Range;

use rmux_core::ScreenLineView;

use super::types::{CopyPosition, SearchMatch};

const REGEX_METACHARS: &[char] = &[
    '.', '^', '$', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|', '\\',
];

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum WordBoundary {
    NextStart,
    NextEnd,
    PreviousStart,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct CopyRange {
    pub(super) start: CopyPosition,
    pub(super) end: CopyPosition,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum WordClass {
    Word,
    Separator,
    Space,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct LineTextMap {
    pub(super) text: String,
    spans: Vec<(Range<usize>, u32)>,
}

impl LineTextMap {
    pub(super) fn new(line: &ScreenLineView) -> Self {
        let mut text = String::new();
        let mut spans = Vec::new();
        for x in owner_positions(line) {
            let Some(cell) = line.cell(x) else {
                continue;
            };
            let start = text.len();
            text.push_str(cell.text());
            spans.push((start..text.len(), x));
        }
        Self { text, spans }
    }

    pub(super) fn match_range(&self, y: usize, range: Range<usize>) -> Option<SearchMatch> {
        let start_x = self
            .spans
            .iter()
            .find(|(span, _)| span.start <= range.start && range.start < span.end)
            .map(|(_, x)| *x)?;
        let end_x = self
            .spans
            .iter()
            .rev()
            .find(|(span, _)| span.start < range.end && range.end <= span.end)
            .or_else(|| {
                self.spans
                    .iter()
                    .rev()
                    .find(|(span, _)| span.start < range.end)
            })
            .map(|(_, x)| *x)?;
        Some(SearchMatch {
            start: CopyPosition { x: start_x, y },
            end: CopyPosition { x: end_x, y },
            text: self.text.get(range)?.to_owned(),
        })
    }
}

pub(super) fn normalize_positions(
    left: CopyPosition,
    right: CopyPosition,
) -> (CopyPosition, CopyPosition) {
    if position_le(left, right) {
        (left, right)
    } else {
        (right, left)
    }
}

pub(super) fn position_le(left: CopyPosition, right: CopyPosition) -> bool {
    left.y < right.y || (left.y == right.y && left.x <= right.x)
}

pub(super) fn position_ge(left: CopyPosition, right: CopyPosition) -> bool {
    left.y > right.y || (left.y == right.y && left.x >= right.x)
}

pub(super) fn owner_positions(line: &ScreenLineView) -> Vec<u32> {
    line.cells()
        .iter()
        .enumerate()
        .filter_map(|(index, cell)| (!cell.is_padding()).then_some(index as u32))
        .collect()
}

pub(super) fn line_char(line: &ScreenLineView, x: u32) -> Option<char> {
    let owner = line.owning_cell_x(x).unwrap_or(x);
    line.cell(owner)?.text().chars().next()
}

pub(super) fn classify_word_char(ch: char, separators: &str, spaces_only: bool) -> WordClass {
    if ch.is_whitespace() {
        WordClass::Space
    } else if !spaces_only && separators.contains(ch) {
        WordClass::Separator
    } else {
        WordClass::Word
    }
}

pub(super) fn pattern_looks_like_regex(pattern: &str) -> bool {
    pattern.chars().any(|ch| REGEX_METACHARS.contains(&ch))
}

pub(super) fn scrollbar_slider_height(rows: u16, history_size: usize) -> usize {
    let sb_h = usize::from(rows.max(1));
    let total_height = history_size.saturating_add(sb_h).max(1);
    (((sb_h as f64) * ((sb_h as f64) / (total_height as f64))).floor() as usize)
        .max(1)
        .min(sb_h)
}