1use crate::width::visible_width;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum Alignment {
6 Left,
8 Right,
10 Center,
12 Decimal,
14}
15
16impl Alignment {
17 pub fn parse(value: &str) -> Option<Self> {
19 match value {
20 "left" => Some(Self::Left),
21 "right" => Some(Self::Right),
22 "center" => Some(Self::Center),
23 "decimal" => Some(Self::Decimal),
24 _ => None,
25 }
26 }
27}
28
29#[derive(Clone, Copy, Debug)]
31pub struct DecimalLayout {
32 pub integer: usize,
33 pub fraction: usize,
34}
35
36#[allow(clippy::too_many_arguments)]
38pub fn align_cell(
39 cell: &str,
40 width: usize,
41 alignment: Alignment,
42 decimal_marker: char,
43 decimal_layout: Option<DecimalLayout>,
44 per_line: bool,
45 enforce_left_alignment: bool,
46 enable_widechars: bool,
47) -> String {
48 if per_line {
49 let lines: Vec<&str> = cell.split('\n').collect();
50 let aligned_lines: Vec<String> = lines
51 .into_iter()
52 .map(|line| {
53 align_line(
54 line,
55 width,
56 alignment,
57 decimal_marker,
58 decimal_layout,
59 enable_widechars,
60 )
61 })
62 .collect();
63 aligned_lines.join("\n")
64 } else {
65 let mut aligned = if matches!(alignment, Alignment::Left) && !enforce_left_alignment {
66 cell.to_string()
67 } else {
68 align_line(
69 cell,
70 width,
71 alignment,
72 decimal_marker,
73 decimal_layout,
74 enable_widechars,
75 )
76 };
77 if matches!(alignment, Alignment::Left) && aligned.contains('\n') {
78 let mut lines: Vec<String> = aligned.split('\n').map(|line| line.to_string()).collect();
79 let last = lines.len().saturating_sub(1);
80 if last > 0 {
81 for line in lines.iter_mut().take(last) {
82 let trimmed = line.trim_end();
83 line.truncate(trimmed.len());
84 }
85 }
86 if let Some(last_line) = lines.last_mut() {
87 let trimmed = last_line.trim_end();
88 last_line.truncate(trimmed.len());
89 }
90 aligned = lines.join("\n");
91 }
92 aligned
93 }
94}
95
96fn align_line(
97 line: &str,
98 width: usize,
99 alignment: Alignment,
100 decimal_marker: char,
101 decimal_layout: Option<DecimalLayout>,
102 enable_widechars: bool,
103) -> String {
104 match alignment {
105 Alignment::Left => align_left_line(line, width, enable_widechars),
106 Alignment::Right => align_right_line(line, width, enable_widechars),
107 Alignment::Center => align_center_line(line, width, enable_widechars),
108 Alignment::Decimal => align_decimal_line(
109 line,
110 width,
111 decimal_marker,
112 decimal_layout,
113 enable_widechars,
114 ),
115 }
116}
117
118fn align_left_line(line: &str, width: usize, enable_widechars: bool) -> String {
119 let cell_width = visible_width(line, enable_widechars);
120 if cell_width >= width {
121 line.to_string()
122 } else {
123 format!("{line}{:padding$}", "", padding = width - cell_width)
124 }
125}
126
127fn align_right_line(line: &str, width: usize, enable_widechars: bool) -> String {
128 let cell_width = visible_width(line, enable_widechars);
129 if cell_width >= width {
130 line.to_string()
131 } else {
132 format!("{:padding$}{line}", "", padding = width - cell_width)
133 }
134}
135
136fn align_center_line(line: &str, width: usize, enable_widechars: bool) -> String {
137 let cell_width = visible_width(line, enable_widechars);
138 if cell_width >= width {
139 line.to_string()
140 } else {
141 let padding = width - cell_width;
142 let left = padding / 2;
143 let right = padding - left;
144 format!(
145 "{:left$}{line}{:right$}",
146 "",
147 "",
148 left = left,
149 right = right
150 )
151 }
152}
153
154fn align_decimal_line(
155 line: &str,
156 width: usize,
157 decimal_marker: char,
158 decimal_layout: Option<DecimalLayout>,
159 enable_widechars: bool,
160) -> String {
161 let layout = decimal_layout.unwrap_or(DecimalLayout {
162 integer: width,
163 fraction: 0,
164 });
165 let split_pos = line.find(decimal_marker);
166
167 let (integer_part, fractional_part) = match split_pos {
168 Some(pos) => (&line[..pos], Some(&line[pos + decimal_marker.len_utf8()..])),
169 None => (line, None),
170 };
171
172 let integer_width = visible_width(integer_part, enable_widechars);
173 let mut result = String::new();
174 if integer_width < layout.integer {
175 result.push_str(&" ".repeat(layout.integer - integer_width));
176 }
177 result.push_str(integer_part);
178 let mut fraction_width = 0usize;
179 if let Some(fractional) = fractional_part {
180 result.push(decimal_marker);
181 result.push_str(fractional);
182 fraction_width = visible_width(fractional, enable_widechars);
183 }
184
185 if layout.fraction > 0 {
186 if fractional_part.is_some() {
187 if fraction_width < layout.fraction {
188 result.push_str(&" ".repeat(layout.fraction - fraction_width));
189 }
190 } else {
191 result.push_str(&" ".repeat(layout.fraction));
192 }
193 }
194
195 let current_width = visible_width(&result, enable_widechars);
196 if current_width < width {
197 let mut padded = String::with_capacity(width);
198 padded.push_str(&" ".repeat(width - current_width));
199 padded.push_str(&result);
200 return padded;
201 }
202
203 result
204}