1use std::cmp::Ordering;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum GraphemeClusterMode {
6 Unicode,
8 WcWidth,
10 NoZwj,
12}
13
14impl GraphemeClusterMode {
15 #[cfg(test)]
17 pub fn from_env() -> Self {
18 GraphemeClusterMode::default()
19 }
20
21 #[cfg(not(test))]
23 pub fn from_env() -> Self {
24 let gcm = match std::env::var("TERM_PROGRAM").as_deref() {
25 Ok("Apple_Terminal") => GraphemeClusterMode::Unicode,
26 Ok("iTerm.app") => GraphemeClusterMode::Unicode,
27 Ok("WezTerm") => GraphemeClusterMode::Unicode,
28 Err(std::env::VarError::NotPresent) => match std::env::var("TERM").as_deref() {
29 Ok("xterm-kitty") => GraphemeClusterMode::NoZwj,
30 _ => GraphemeClusterMode::WcWidth,
31 },
32 _ => GraphemeClusterMode::WcWidth,
33 };
34 log::debug!(target: "rustyline", "GraphemeClusterMode: {gcm:?}");
35 gcm
36 }
37
38 pub fn width(&self, s: &str) -> Unit {
40 match self {
41 GraphemeClusterMode::Unicode => uwidth(s),
42 GraphemeClusterMode::WcWidth => wcwidth(s),
43 GraphemeClusterMode::NoZwj => no_zwj(s),
44 }
45 }
46}
47
48#[cfg(test)]
49#[expect(clippy::derivable_impls)]
50impl Default for GraphemeClusterMode {
51 fn default() -> Self {
52 GraphemeClusterMode::Unicode
53 }
54}
55
56pub type Unit = u16;
58pub(crate) fn cwidh(c: char) -> Unit {
60 use unicode_width::UnicodeWidthChar;
61 Unit::try_from(c.width().unwrap_or(0)).unwrap()
62}
63
64fn uwidth(s: &str) -> Unit {
65 use unicode_width::UnicodeWidthStr;
66 Unit::try_from(s.width()).unwrap()
67}
68
69fn wcwidth(s: &str) -> Unit {
70 let mut width = 0;
71 for c in s.chars() {
72 width += cwidh(c);
73 }
74 width
75}
76
77const ZWJ: char = '\u{200D}';
78fn no_zwj(s: &str) -> Unit {
79 let mut width = 0;
80 for x in s.split(ZWJ) {
81 width += uwidth(x);
82 }
83 width
84}
85
86#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
87pub struct Position {
88 pub col: Unit, pub row: Unit, }
91
92impl PartialOrd for Position {
93 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
94 Some(self.cmp(other))
95 }
96}
97
98impl Ord for Position {
99 fn cmp(&self, other: &Self) -> Ordering {
100 match self.row.cmp(&other.row) {
101 Ordering::Equal => self.col.cmp(&other.col),
102 o => o,
103 }
104 }
105}
106
107#[derive(Debug)]
108#[cfg_attr(test, derive(Default))]
109pub struct Layout {
110 pub grapheme_cluster_mode: GraphemeClusterMode,
111 pub prompt_size: Position,
113 pub default_prompt: bool,
114 pub cursor: Position,
116 pub end: Position,
118}
119
120impl Layout {
121 pub fn new(grapheme_cluster_mode: GraphemeClusterMode) -> Self {
122 Self {
123 grapheme_cluster_mode,
124 prompt_size: Position::default(),
125 default_prompt: false,
126 cursor: Position::default(),
127 end: Position::default(),
128 }
129 }
130
131 pub fn width(&self, s: &str) -> Unit {
132 self.grapheme_cluster_mode.width(s)
133 }
134}
135
136#[cfg(test)]
137mod test {
138 #[test]
139 fn unicode_width() {
140 assert_eq!(1, super::uwidth("a"));
141 assert_eq!(2, super::uwidth("๐ฉโ๐"));
142 assert_eq!(2, super::uwidth("๐๐ฟ"));
143 assert_eq!(2, super::uwidth("๐จโ๐ฉโ๐งโ๐ฆ"));
144 assert_eq!(2, super::uwidth("๐ฉ๐ผโ๐จ๐ผโ๐ฆ๐ผโ๐ฆ๐ผ"));
146 assert_eq!(2, super::uwidth("โค๏ธ"));
148 }
149 #[test]
150 fn test_wcwidth() {
151 assert_eq!(1, super::wcwidth("a"));
152 assert_eq!(4, super::wcwidth("๐ฉโ๐"));
153 assert_eq!(4, super::wcwidth("๐๐ฟ"));
154 assert_eq!(8, super::wcwidth("๐จโ๐ฉโ๐งโ๐ฆ"));
155 assert_eq!(16, super::wcwidth("๐ฉ๐ผโ๐จ๐ผโ๐ฆ๐ผโ๐ฆ๐ผ"));
156 assert_eq!(1, super::wcwidth("โค๏ธ"));
157 }
158 #[test]
159 fn test_no_zwj() {
160 assert_eq!(1, super::no_zwj("a"));
161 assert_eq!(4, super::no_zwj("๐ฉโ๐"));
162 assert_eq!(2, super::no_zwj("๐๐ฟ"));
163 assert_eq!(8, super::no_zwj("๐จโ๐ฉโ๐งโ๐ฆ"));
164 assert_eq!(8, super::no_zwj("๐ฉ๐ผโ๐จ๐ผโ๐ฆ๐ผโ๐ฆ๐ผ"));
165 assert_eq!(2, super::no_zwj("๏ธโค๏ธ"));
166 }
167}