artifactize_core/agent/session/
document.rs1use std::ops::Range;
4use unicode_width::UnicodeWidthStr;
5
6pub(crate) fn cell_width(grapheme: &str) -> u16 {
9 if grapheme.len() == 1 {
10 return 1;
11 }
12 let marks = grapheme
13 .chars()
14 .filter(|c| matches!(c, '\u{FF9E}' | '\u{FF9F}'))
15 .count() as u16;
16 (grapheme.width() as u16).saturating_add(marks)
17}
18use unicode_segmentation::UnicodeSegmentation;
19
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub struct Anchor {
22 pub event: usize,
23 pub byte: usize,
24}
25
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
27pub enum Position {
28 #[default]
29 Bottom,
30 Row(usize),
31 Relative {
33 anchor: Anchor,
34 rows: isize,
35 width: usize,
36 },
37 Anchor(Anchor),
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Move {
42 Up(usize),
43 Down(usize),
44 Top,
45 Bottom,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum Mode {
50 Following,
51 Paused,
52}
53
54#[derive(Debug, Clone)]
56pub struct Scroll {
57 pub top: usize,
58 pub total: usize,
59 pub height: usize,
60 pub mode: Mode,
61}
62impl Default for Scroll {
63 fn default() -> Self {
64 Self {
65 top: 0,
66 total: 0,
67 height: 0,
68 mode: Mode::Following,
69 }
70 }
71}
72impl Scroll {
73 pub fn bottom(&self) -> usize {
74 self.total.saturating_sub(self.height)
75 }
76 pub fn at_bottom(&self) -> bool {
77 self.top == self.bottom()
78 }
79 pub fn update(&mut self, total: usize, height: usize, anchor_row: Option<usize>) {
80 self.total = total;
81 self.height = height;
82 self.top = match self.mode {
83 Mode::Following => self.bottom(),
84 Mode::Paused => anchor_row.unwrap_or(self.top).min(self.bottom()),
85 };
86 if self.at_bottom() {
88 self.mode = Mode::Following;
89 }
90 }
91 pub fn apply(&mut self, movement: Move) -> Position {
92 self.top = match movement {
93 Move::Up(rows) => self.top.saturating_sub(rows),
94 Move::Down(rows) => self.top.saturating_add(rows).min(self.bottom()),
95 Move::Top => 0,
96 Move::Bottom => self.bottom(),
97 };
98 self.mode = if self.at_bottom() {
99 Mode::Following
100 } else {
101 Mode::Paused
102 };
103 match self.mode {
104 Mode::Following => Position::Bottom,
105 Mode::Paused => Position::Row(self.top),
106 }
107 }
108}
109
110pub fn rows(text: &str, width: usize) -> Vec<Range<usize>> {
114 if text.is_empty() || width == 0 {
115 return Vec::new();
116 }
117 let mut rows = Vec::new();
118 let mut start = 0;
119 let mut columns = 0;
120 for (byte, grapheme) in text.grapheme_indices(true) {
121 if grapheme.contains('\n') {
122 rows.push(start..byte);
123 start = byte + grapheme.len();
124 columns = 0;
125 } else if !grapheme.contains(char::is_control) {
126 let size = usize::from(cell_width(grapheme));
127 if size <= width {
128 if columns + size > width {
129 rows.push(start..byte);
130 start = byte;
131 columns = 0;
132 }
133 columns += size;
134 }
135 }
136 }
137 if start < text.len() {
138 rows.push(start..text.len());
139 }
140 rows
141}
142
143pub fn row(text: &str, range: &Range<usize>, width: usize) -> String {
144 text[range.clone()]
145 .graphemes(true)
146 .filter(|grapheme| {
147 !grapheme.contains(char::is_control) && usize::from(cell_width(grapheme)) <= width
148 })
149 .collect()
150}