Skip to main content

rusty_rich/
segment.rs

1//! Segment — styled text unit. Equivalent to Rich's `segment.py`.
2//!
3//! A [`Segment`] is the smallest unit of output: a piece of text with an
4//! associated [`Style`](crate::Style) and optional control code.
5//!
6//! # Core Types
7//!
8//! - [`Segment`] — text + optional style + optional control code
9//! - [`Segments`] — a collection of segments with convenience methods
10//! - `ControlType` — 16 terminal control codes (bell, cursor movement, etc.)
11//! - `ControlCode` — a control type with optional parameters
12//!
13//! # Utility Functions (v0.2)
14//!
15//! | Function | Description |
16//! |----------|-------------|
17//! | `Segments::simplify` | Combine adjacent segments with identical styles |
18//! | `split_lines` | Split segments into lines at newline boundaries |
19//! | `strip_styles` | Remove all styling, returning plain text |
20//! | `strip_links` | Remove link IDs and URLs from segment styles |
21//! | `align_top` / `align_middle` / `align_bottom` | Vertical alignment helpers |
22//! | `divide` | Split segments at given cell offsets |
23//! | `set_shape` | Pad or truncate segments to exact width × height |
24//! | `filter_control` | Keep only control segments (or only non-control) |
25//! | `get_line_length` | Total cell width of a line of segments |
26//!
27//! # Example
28//!
29//! ```rust
30//! use rusty_rich::{Segment, Segments, Style};
31//!
32//! let segs = Segments::from(vec![
33//!     Segment::styled("Hello ", Style::new().bold(true)),
34//!     Segment::styled("World", Style::new().bold(true)),
35//! ]);
36//!
37//! // Combine adjacent same-styled segments
38//! let merged = segs.simplify();
39//! assert_eq!(merged.segments.len(), 1);
40//! ```
41
42use std::fmt;
43
44use crate::style::Style;
45
46// ---------------------------------------------------------------------------
47// ControlType
48// ---------------------------------------------------------------------------
49
50/// Non-printable control codes (equivalent to Rich's `ControlType`).
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum ControlType {
53    Bell,
54    CarriageReturn,
55    Home,
56    Clear,
57    ShowCursor,
58    HideCursor,
59    EnableAltScreen,
60    DisableAltScreen,
61    CursorUp,
62    CursorDown,
63    CursorForward,
64    CursorBackward,
65    CursorMoveToColumn,
66    CursorMoveTo,
67    EraseInLine,
68    SetWindowTitle,
69}
70
71impl ControlType {
72    /// Get the ANSI escape sequence for this control type.
73    pub fn to_ansi(&self, params: &[i32]) -> String {
74        match self {
75            Self::Bell => "\x07".into(),
76            Self::CarriageReturn => "\r".into(),
77            Self::Home => "\x1b[H".into(),
78            Self::Clear => "\x1b[2J".into(),
79            Self::ShowCursor => "\x1b[?25h".into(),
80            Self::HideCursor => "\x1b[?25l".into(),
81            Self::EnableAltScreen => "\x1b[?1049h".into(),
82            Self::DisableAltScreen => "\x1b[?1049l".into(),
83            Self::CursorUp => {
84                let n = params.first().copied().unwrap_or(1);
85                format!("\x1b[{n}A")
86            }
87            Self::CursorDown => {
88                let n = params.first().copied().unwrap_or(1);
89                format!("\x1b[{n}B")
90            }
91            Self::CursorForward => {
92                let n = params.first().copied().unwrap_or(1);
93                format!("\x1b[{n}C")
94            }
95            Self::CursorBackward => {
96                let n = params.first().copied().unwrap_or(1);
97                format!("\x1b[{n}D")
98            }
99            Self::CursorMoveToColumn => {
100                let col = params.first().copied().unwrap_or(0);
101                format!("\x1b[{col}G")
102            }
103            Self::CursorMoveTo => {
104                let row = params.first().copied().unwrap_or(0);
105                let col = params.get(1).copied().unwrap_or(0);
106                format!("\x1b[{row};{col}H")
107            }
108            Self::EraseInLine => {
109                let mode = params.first().copied().unwrap_or(0);
110                format!("\x1b[{mode}K")
111            }
112            Self::SetWindowTitle => {
113                let title: String = params
114                    .iter()
115                    .map(|n| char::from(*n as u8))
116                    .collect();
117                format!("\x1b]0;{title}\x07")
118            }
119        }
120    }
121}
122
123// ---------------------------------------------------------------------------
124// ControlCode
125// ---------------------------------------------------------------------------
126
127#[derive(Debug, Clone, PartialEq, Eq, Hash)]
128pub enum ControlCode {
129    Simple(ControlType),
130    WithInt(ControlType, i32),
131    WithTwoInts(ControlType, i32, i32),
132    WithString(ControlType, String),
133}
134
135// ---------------------------------------------------------------------------
136// Segment
137// ---------------------------------------------------------------------------
138
139/// A piece of text with an associated style.
140///
141/// Segments are produced during rendering and ultimately converted to strings
142/// for terminal output.
143#[derive(Debug, Clone, PartialEq)]
144pub struct Segment {
145    pub text: String,
146    pub style: Option<Style>,
147    pub control: Option<ControlCode>,
148}
149
150impl Segment {
151    /// Create a new segment with just text.
152    pub fn new(text: impl Into<String>) -> Self {
153        Self {
154            text: text.into(),
155            style: None,
156            control: None,
157        }
158    }
159
160    /// Create a segment with text and style.
161    pub fn styled(text: impl Into<String>, style: Style) -> Self {
162        Self {
163            text: text.into(),
164            style: Some(style),
165            control: None,
166        }
167    }
168
169    /// Create a control-only segment.
170    pub fn control(code: ControlCode) -> Self {
171        Self {
172            text: String::new(),
173            style: None,
174            control: Some(code),
175        }
176    }
177
178    /// Create a newline segment.
179    pub fn line() -> Self {
180        Self::new("\n")
181    }
182
183    /// Get the cell length of this segment's text (using Unicode width).
184    pub fn cell_length(&self) -> usize {
185        if self.control.is_some() {
186            return 0;
187        }
188        unicode_width::UnicodeWidthStr::width(self.text.as_str())
189    }
190
191    /// Check if this segment has any text content.
192    pub fn is_empty(&self) -> bool {
193        self.text.is_empty() && self.control.is_none()
194    }
195
196    /// Split this segment at the given offset, returning two segments.
197    /// The first goes up to (but not including) `offset`, the second from
198    /// `offset` to end.
199    pub fn split(&self, offset: usize) -> (Segment, Option<Segment>) {
200        if offset == 0 {
201            return (Segment::new(""), Some(self.clone()));
202        }
203        let cell_len = self.cell_length();
204        if offset >= cell_len {
205            return (self.clone(), None);
206        }
207
208        // Find the byte position at which the cell count reaches `offset`
209        let mut cell_count = 0usize;
210        let mut byte_pos = 0usize;
211        for (i, ch) in self.text.char_indices() {
212            let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
213            if cell_count + w > offset {
214                break;
215            }
216            cell_count += w;
217            byte_pos = i + ch.len_utf8();
218        }
219
220        let left = Segment {
221            text: self.text[..byte_pos].to_string(),
222            style: self.style.clone(),
223            control: self.control.clone(),
224        };
225        let right = Segment {
226            text: self.text[byte_pos..].to_string(),
227            style: self.style.clone(),
228            control: self.control.clone(),
229        };
230        (left, Some(right))
231    }
232
233    /// Return the ANSI string for this segment (style + text + reset).
234    pub fn to_ansi(&self) -> String {
235        if let Some(ref code) = self.control {
236            return match code {
237                ControlCode::Simple(ct) => ct.to_ansi(&[]),
238                ControlCode::WithInt(ct, a) => ct.to_ansi(&[*a]),
239                ControlCode::WithTwoInts(ct, a, b) => ct.to_ansi(&[*a, *b]),
240                ControlCode::WithString(ct, s) => {
241                    let params: Vec<i32> = s.bytes().map(|b| b as i32).collect();
242                    ct.to_ansi(&params)
243                }
244            };
245        }
246
247        let style_ansi = self.style.as_ref().map(|s| s.to_ansi()).unwrap_or_default();
248        let reset = self.style.as_ref().map(|s| s.reset_ansi()).unwrap_or("");
249
250        if style_ansi.is_empty() {
251            self.text.clone()
252        } else {
253            format!("{style_ansi}{}{reset}", self.text)
254        }
255    }
256}
257
258impl fmt::Display for Segment {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        write!(f, "{}", self.to_ansi())
261    }
262}
263
264// ---------------------------------------------------------------------------
265// Segments — a collection of segments
266// ---------------------------------------------------------------------------
267
268/// A collection of `Segment`s, with convenience methods.
269#[derive(Debug, Clone, Default)]
270pub struct Segments {
271    pub segments: Vec<Segment>,
272}
273
274impl Segments {
275    pub fn new() -> Self {
276        Self {
277            segments: Vec::new(),
278        }
279    }
280
281    pub fn push(&mut self, seg: Segment) {
282        self.segments.push(seg);
283    }
284
285    pub fn extend(&mut self, other: impl IntoIterator<Item = Segment>) {
286        self.segments.extend(other);
287    }
288
289    /// Render all segments to an ANSI string.
290    pub fn to_ansi(&self) -> String {
291        let mut out = String::new();
292        for seg in &self.segments {
293            out.push_str(&seg.to_ansi());
294        }
295        out
296    }
297
298    /// Get the total cell width.
299    pub fn cell_len(&self) -> usize {
300        self.segments.iter().map(Segment::cell_length).sum()
301    }
302}
303
304impl fmt::Display for Segments {
305    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306        write!(f, "{}", self.to_ansi())
307    }
308}
309
310impl From<Vec<Segment>> for Segments {
311    fn from(segments: Vec<Segment>) -> Self {
312        Self { segments }
313    }
314}
315
316impl IntoIterator for Segments {
317    type Item = Segment;
318    type IntoIter = std::vec::IntoIter<Segment>;
319
320    fn into_iter(self) -> Self::IntoIter {
321        self.segments.into_iter()
322    }
323}
324
325// ---------------------------------------------------------------------------
326// Utility: line()
327// ---------------------------------------------------------------------------
328
329/// Helper: create a newline segment.
330pub fn line() -> Segment {
331    Segment::line()
332}
333
334/// Helper: create a space segment.
335pub fn space(count: usize) -> Segment {
336    Segment::new(" ".repeat(count))
337}
338
339// ---------------------------------------------------------------------------
340// Segment collection utilities
341// ---------------------------------------------------------------------------
342
343impl Segments {
344    /// Combine adjacent segments that have the same style.
345    pub fn simplify(&self) -> Segments {
346        let mut result: Vec<Segment> = Vec::new();
347        for seg in &self.segments {
348            if let Some(last) = result.last_mut() {
349                if last.style == seg.style && last.control.is_none() && seg.control.is_none() {
350                    last.text.push_str(&seg.text);
351                    continue;
352                }
353            }
354            result.push(seg.clone());
355        }
356        Segments { segments: result }
357    }
358}
359
360/// Split an iterable of segments into lines at newline boundaries.
361pub fn split_lines(segments: &[Segment]) -> Vec<Vec<Segment>> {
362    let mut lines: Vec<Vec<Segment>> = Vec::new();
363    let mut current: Vec<Segment> = Vec::new();
364    for seg in segments {
365        if seg.text == "\n" && seg.style.is_none() && seg.control.is_none() {
366            lines.push(std::mem::take(&mut current));
367        } else if seg.text.contains('\n') && seg.style.is_none() && seg.control.is_none() {
368            let parts: Vec<&str> = seg.text.split('\n').collect();
369            for (i, part) in parts.iter().enumerate() {
370                if i > 0 {
371                    lines.push(std::mem::take(&mut current));
372                }
373                if !part.is_empty() {
374                    current.push(Segment::new(*part));
375                }
376            }
377        } else {
378            current.push(seg.clone());
379        }
380    }
381    if !current.is_empty() {
382        lines.push(current);
383    }
384    lines
385}
386
387/// Remove all styles from segments, returning plain text only.
388pub fn strip_styles(segments: &[Segment]) -> String {
389    let mut out = String::new();
390    for seg in segments {
391        if seg.control.is_none() {
392            out.push_str(&seg.text);
393        }
394    }
395    out
396}
397
398/// Remove link IDs and URLs from all segment styles.
399pub fn strip_links(segments: &[Segment]) -> Vec<Segment> {
400    segments
401        .iter()
402        .map(|seg| {
403            let mut s = seg.clone();
404            if let Some(ref style) = seg.style {
405                let mut new_style = style.clone();
406                new_style.link_id = 0;
407                new_style.link = None;
408                s.style = Some(new_style);
409            }
410            s
411        })
412        .collect()
413}
414
415/// Align lines to the top of a region of given height.
416pub fn align_top(
417    lines: &[Vec<Segment>],
418    _width: usize,
419    height: usize,
420    _style: Option<&Style>,
421) -> Vec<Vec<Segment>> {
422    let blank_line = vec![Segment::new(" ".repeat(_width))];
423    let mut result: Vec<Vec<Segment>> = lines.to_vec();
424    while result.len() < height {
425        result.push(blank_line.clone());
426    }
427    result.truncate(height);
428    result
429}
430
431/// Align lines to the middle of a region of given height.
432pub fn align_middle(
433    lines: &[Vec<Segment>],
434    _width: usize,
435    height: usize,
436    _style: Option<&Style>,
437) -> Vec<Vec<Segment>> {
438    let blank_line = vec![Segment::new(" ".repeat(_width))];
439    let top_pad = (height.saturating_sub(lines.len())) / 2;
440    let mut result: Vec<Vec<Segment>> = Vec::new();
441    for _ in 0..top_pad {
442        result.push(blank_line.clone());
443    }
444    result.extend(lines.iter().cloned());
445    while result.len() < height {
446        result.push(blank_line.clone());
447    }
448    result.truncate(height);
449    result
450}
451
452/// Align lines to the bottom of a region of given height.
453pub fn align_bottom(
454    lines: &[Vec<Segment>],
455    _width: usize,
456    height: usize,
457    _style: Option<&Style>,
458) -> Vec<Vec<Segment>> {
459    let blank_line = vec![Segment::new(" ".repeat(_width))];
460    let bottom_pad = height.saturating_sub(lines.len());
461    let mut result: Vec<Vec<Segment>> = Vec::new();
462    for _ in 0..bottom_pad {
463        result.push(blank_line.clone());
464    }
465    result.extend(lines.iter().cloned());
466    result.truncate(height);
467    result
468}
469
470/// Divide segments at the given cell offsets.
471pub fn divide(segments: &[Segment], cuts: &[usize]) -> Vec<Vec<Segment>> {
472    let mut result: Vec<Vec<Segment>> = Vec::new();
473    let mut remaining = segments.to_vec();
474    let mut offset = 0usize;
475
476    for &cut in cuts {
477        let mut chunk: Vec<Segment> = Vec::new();
478        let target = cut.saturating_sub(offset);
479
480        let mut chunk_cells = 0usize;
481        while chunk_cells < target && !remaining.is_empty() {
482            let seg = remaining.remove(0);
483            let seg_len = seg.cell_length();
484            if chunk_cells + seg_len <= target {
485                chunk_cells += seg_len;
486                chunk.push(seg);
487            } else {
488                let split_at = target - chunk_cells;
489                let (left, right) = seg.split(split_at);
490                chunk.push(left);
491                if let Some(r) = right {
492                    remaining.insert(0, r);
493                }
494                chunk_cells = target;
495            }
496        }
497        result.push(chunk);
498        offset = cut;
499    }
500
501    if !remaining.is_empty() {
502        result.push(remaining);
503    }
504
505    result
506}
507
508/// Set segments to an exact width and height, padding/truncating as needed.
509pub fn set_shape(
510    lines: &[Vec<Segment>],
511    width: usize,
512    height: usize,
513    _style: Option<&Style>,
514) -> Vec<Vec<Segment>> {
515    let blank_line = vec![Segment::new(" ".repeat(width))];
516    let mut result: Vec<Vec<Segment>> = Vec::new();
517
518    for line in lines.iter().take(height) {
519        let cell_len: usize = line.iter().map(|s| s.cell_length()).sum();
520        let mut new_line = line.clone();
521        if cell_len < width {
522            new_line.push(Segment::new(" ".repeat(width - cell_len)));
523        } else if cell_len > width {
524            let mut truncated = Vec::new();
525            let mut count = 0usize;
526            for seg in line {
527                let seg_len = seg.cell_length();
528                if count + seg_len <= width {
529                    truncated.push(seg.clone());
530                    count += seg_len;
531                } else if count < width {
532                    let (left, _) = seg.split(width - count);
533                    truncated.push(left);
534                    break;
535                }
536            }
537            new_line = truncated;
538        }
539        result.push(new_line);
540    }
541
542    while result.len() < height {
543        result.push(blank_line.clone());
544    }
545
546    result
547}
548
549/// Filter segments, keeping only control codes if `is_control` is true,
550/// or only non-control segments if `is_control` is false.
551pub fn filter_control(segments: &[Segment], is_control: bool) -> Vec<Segment> {
552    segments
553        .iter()
554        .filter(|seg| seg.control.is_some() == is_control)
555        .cloned()
556        .collect()
557}
558
559/// Get the total cell length of a line of segments.
560pub fn get_line_length(line: &[Segment]) -> usize {
561    line.iter().map(|s| s.cell_length()).sum()
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use crate::style::Style;
568
569    #[test]
570    fn test_segment_cell_length() {
571        let seg = Segment::new("Hello");
572        assert_eq!(seg.cell_length(), 5);
573    }
574
575    #[test]
576    fn test_segment_split() {
577        let seg = Segment::new("Hello World");
578        let (left, right) = seg.split(5);
579        assert_eq!(left.text, "Hello");
580        assert_eq!(right.unwrap().text, " World");
581    }
582
583    #[test]
584    fn test_segment_to_ansi() {
585        let style = Style::new().bold(true);
586        let seg = Segment::styled("Bold", style);
587        let ansi = seg.to_ansi();
588        assert!(ansi.contains("[1m"));
589        assert!(ansi.contains("Bold"));
590    }
591}