use std::cmp::Ordering;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GraphemeClusterMode {
Unicode,
WcWidth,
NoZwj,
}
impl GraphemeClusterMode {
#[cfg(test)]
pub fn from_env() -> Self {
GraphemeClusterMode::default()
}
#[cfg(not(test))]
pub fn from_env() -> Self {
let gcm = match std::env::var("TERM_PROGRAM").as_deref() {
Ok("Apple_Terminal") => GraphemeClusterMode::Unicode,
Ok("iTerm.app") => GraphemeClusterMode::Unicode,
Ok("WezTerm") => GraphemeClusterMode::Unicode,
Err(std::env::VarError::NotPresent) => match std::env::var("TERM").as_deref() {
Ok("xterm-kitty") => GraphemeClusterMode::NoZwj,
_ => GraphemeClusterMode::WcWidth,
},
_ => GraphemeClusterMode::WcWidth,
};
log::debug!(target: "rustyline", "GraphemeClusterMode: {gcm:?}");
gcm
}
pub fn width(&self, s: &str) -> Unit {
match self {
GraphemeClusterMode::Unicode => uwidth(s),
GraphemeClusterMode::WcWidth => wcwidth(s),
GraphemeClusterMode::NoZwj => no_zwj(s),
}
}
}
#[cfg(test)]
#[expect(clippy::derivable_impls)]
impl Default for GraphemeClusterMode {
fn default() -> Self {
GraphemeClusterMode::Unicode
}
}
pub type Unit = u16;
pub(crate) fn cwidh(c: char) -> Unit {
use unicode_width::UnicodeWidthChar as _;
Unit::try_from(c.width().unwrap_or(0)).unwrap()
}
fn uwidth(s: &str) -> Unit {
use unicode_width::UnicodeWidthStr as _;
Unit::try_from(s.width()).unwrap()
}
fn wcwidth(s: &str) -> Unit {
let mut width = 0;
for c in s.chars() {
width += cwidh(c);
}
width
}
const ZWJ: char = '\u{200D}';
fn no_zwj(s: &str) -> Unit {
let mut width = 0;
for x in s.split(ZWJ) {
width += uwidth(x);
}
width
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Position {
pub col: Unit, pub row: Unit, }
impl PartialOrd for Position {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Position {
fn cmp(&self, other: &Self) -> Ordering {
match self.row.cmp(&other.row) {
Ordering::Equal => self.col.cmp(&other.col),
o => o,
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Default))]
pub struct Layout {
pub grapheme_cluster_mode: GraphemeClusterMode,
pub prompt_size: Position,
pub default_prompt: bool,
pub cursor: Position,
pub end: Position,
pub has_info: bool,
}
impl Layout {
pub fn new(grapheme_cluster_mode: GraphemeClusterMode) -> Self {
Self {
grapheme_cluster_mode,
prompt_size: Position::default(),
default_prompt: false,
cursor: Position::default(),
end: Position::default(),
has_info: false,
}
}
pub fn width(&self, s: &str) -> Unit {
self.grapheme_cluster_mode.width(s)
}
}
#[cfg(test)]
mod test {
#[test]
fn unicode_width() {
assert_eq!(1, super::uwidth("a"));
assert_eq!(2, super::uwidth("๐ฉโ๐"));
assert_eq!(2, super::uwidth("๐๐ฟ"));
assert_eq!(2, super::uwidth("๐จโ๐ฉโ๐งโ๐ฆ"));
assert_eq!(2, super::uwidth("๐ฉ๐ผโ๐จ๐ผโ๐ฆ๐ผโ๐ฆ๐ผ"));
assert_eq!(2, super::uwidth("โค๏ธ"));
}
#[test]
fn test_wcwidth() {
assert_eq!(1, super::wcwidth("a"));
assert_eq!(4, super::wcwidth("๐ฉโ๐"));
assert_eq!(4, super::wcwidth("๐๐ฟ"));
assert_eq!(8, super::wcwidth("๐จโ๐ฉโ๐งโ๐ฆ"));
assert_eq!(16, super::wcwidth("๐ฉ๐ผโ๐จ๐ผโ๐ฆ๐ผโ๐ฆ๐ผ"));
assert_eq!(1, super::wcwidth("โค๏ธ"));
}
#[test]
fn test_no_zwj() {
assert_eq!(1, super::no_zwj("a"));
assert_eq!(4, super::no_zwj("๐ฉโ๐"));
assert_eq!(2, super::no_zwj("๐๐ฟ"));
assert_eq!(8, super::no_zwj("๐จโ๐ฉโ๐งโ๐ฆ"));
assert_eq!(8, super::no_zwj("๐ฉ๐ผโ๐จ๐ผโ๐ฆ๐ผโ๐ฆ๐ผ"));
assert_eq!(2, super::no_zwj("๏ธโค๏ธ"));
}
}