diff --git a/Cargo.toml b/Cargo.toml index dce3400..89ae1e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ documentation = "https://docs.rs/promptuity" [dependencies] crossterm = "0.27.0" +strip-ansi-escapes = "0.2.0" thiserror = "1.0.50" unicode-width = "0.1.11" diff --git a/src/style.rs b/src/style.rs index 3b86bf0..b2ce1e3 100644 --- a/src/style.rs +++ b/src/style.rs @@ -32,6 +32,7 @@ /// [`Color`] re-exports from [`crossterm::style::Color`]. pub use crossterm::style::Color; use crossterm::style::{Attribute, Attributes, ContentStyle, Stylize}; +use unicode_width::UnicodeWidthChar; /// A styling utility for strings wrapped in [`crossterm::style::ContentStyle`]. #[derive(Debug)] @@ -166,3 +167,36 @@ impl<'a, 'b> std::fmt::Display for Symbol<'a, 'b> { } } } + +/// A utility function to wrap text at a specified character count. +/// +/// # Examples +/// +/// ``` +/// use promptuity::style::wrap_text; +/// +/// let text = "This is a long text that will be wrapped at 10 characters."; +/// +/// assert_eq!(wrap_text(text, 10), "This is a \nlong text \nthat will \nbe wrapped\n at 10 cha\nracters."); +/// ``` +pub fn wrap_text(input: &str, col: u16) -> String { + let mut output = String::new(); + let mut cw = 0; + + for (_, c) in input.char_indices() { + let w = c.width().unwrap_or(0); + if c == '\n' { + cw = 0; + output.push(c); + continue; + } + if cw + w > col as usize { + output.push('\n'); + cw = 0; + } + output.push(c); + cw += w; + } + + output +} diff --git a/src/themes/fancy.rs b/src/themes/fancy.rs index be0da77..d1e407c 100644 --- a/src/themes/fancy.rs +++ b/src/themes/fancy.rs @@ -1,3 +1,5 @@ +use strip_ansi_escapes::strip_str; + use crate::style::*; use crate::{ Error, InputCursor, PromptBody, PromptInput, PromptState, RenderSnapshot, Terminal, Theme, @@ -244,7 +246,9 @@ impl Theme for FancyTheme { output.push_str(&self.fmt_body_active(Color::Cyan, payload.body)); output.push_str(&self.fmt_end(Color::Cyan, true)); - self.prev_lines = output.lines().count() as u16; + self.prev_lines = wrap_text(&strip_str(&output), term.size()?.width) + .lines() + .count() as u16; } PromptState::Error(msg) | PromptState::Fatal(msg) => { @@ -278,7 +282,9 @@ impl Theme for FancyTheme { self.fmt_error(msg.clone()), )); - self.prev_lines = output.lines().count() as u16; + self.prev_lines = wrap_text(&strip_str(&output), term.size()?.width) + .lines() + .count() as u16; } PromptState::Submit => { diff --git a/src/themes/minimal.rs b/src/themes/minimal.rs index 7e76d60..7770041 100644 --- a/src/themes/minimal.rs +++ b/src/themes/minimal.rs @@ -1,3 +1,5 @@ +use strip_ansi_escapes::strip_str; + use crate::style::*; use crate::{Error, InputCursor, PromptBody, PromptInput, PromptState, Terminal, Theme}; @@ -184,7 +186,9 @@ impl Theme for MinimalTheme { output.push_str(&self.fmt_body_active(payload.body)); output.push_str(&self.fmt_hint(payload.hint)); - self.prev_lines = output.lines().count() as u16; + self.prev_lines = wrap_text(&strip_str(&output), term.size()?.width) + .lines() + .count() as u16; } PromptState::Error(msg) | PromptState::Fatal(msg) => { @@ -204,7 +208,9 @@ impl Theme for MinimalTheme { output.push_str(&self.fmt_error(msg.clone())); output.push_str(&self.fmt_hint(payload.hint)); - self.prev_lines = output.lines().count() as u16; + self.prev_lines = wrap_text(&strip_str(&output), term.size()?.width) + .lines() + .count() as u16; } PromptState::Submit => {