1use std::io::{self, Write};
6
7use unicode_width::UnicodeWidthStr;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ColumnLayout {
12 Column,
14 Row,
16 Plain,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct ColOpts(u32);
23
24const LAYOUT_MASK: u32 = 0x000F;
25const ENABLE_MASK: u32 = 0x0030;
26const PARSEOPT: u32 = 0x0040;
27const DENSE: u32 = 0x0080;
28
29const DISABLED: u32 = 0x0000;
30const ENABLED: u32 = 0x0010;
31const AUTO: u32 = 0x0020;
32
33const LAYOUT_COLUMN: u32 = 0;
34const LAYOUT_ROW: u32 = 1;
35const LAYOUT_PLAIN: u32 = 15;
36
37impl ColOpts {
38 #[must_use]
40 pub const fn new() -> Self {
41 Self(0)
42 }
43
44 fn layout_bits(self) -> u32 {
45 self.0 & LAYOUT_MASK
46 }
47
48 #[must_use]
50 pub fn is_active(self) -> bool {
51 self.0 & ENABLE_MASK == ENABLED
52 }
53
54 fn dense(self) -> bool {
55 self.0 & DENSE != 0
56 }
57
58 fn layout_mode(self) -> ColumnLayout {
59 match self.layout_bits() {
60 LAYOUT_ROW => ColumnLayout::Row,
61 LAYOUT_PLAIN => ColumnLayout::Plain,
62 _ => ColumnLayout::Column,
63 }
64 }
65}
66
67impl Default for ColOpts {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct ColumnOptions {
76 pub width: Option<usize>,
78 pub padding: usize,
80 pub indent: String,
82 pub nl: String,
84}
85
86impl Default for ColumnOptions {
87 fn default() -> Self {
88 Self {
89 width: None,
90 padding: 1,
91 indent: String::new(),
92 nl: "\n".to_owned(),
93 }
94 }
95}
96
97fn div_round_up(a: usize, b: usize) -> usize {
98 if b == 0 {
99 return a;
100 }
101 a.div_ceil(b)
102}
103
104fn item_width(s: &str) -> usize {
105 UnicodeWidthStr::width(s)
106}
107
108fn xy_to_linear(layout: ColumnLayout, cols: usize, rows: usize, x: usize, y: usize) -> usize {
109 match layout {
110 ColumnLayout::Column => x * rows + y,
111 ColumnLayout::Row => y * cols + x,
112 ColumnLayout::Plain => y,
113 }
114}
115
116pub fn parse_column_tokens_into(value: &str, colopts: &mut ColOpts) -> Result<(), String> {
118 let mut group_set: u8 = 0;
119 for raw in value.split([' ', ',']) {
120 let token = raw.trim();
121 if token.is_empty() {
122 continue;
123 }
124 parse_one_token(token, colopts, &mut group_set)?;
125 }
126 if group_set & 1 != 0 && group_set & 2 == 0 {
128 colopts.0 = (colopts.0 & !ENABLE_MASK) | ENABLED;
129 }
130 Ok(())
131}
132
133fn parse_one_token(token: &str, colopts: &mut ColOpts, group_set: &mut u8) -> Result<(), String> {
134 const LAYOUT_SET: u8 = 1;
135 const ENABLE_SET: u8 = 2;
136
137 let (neg_dense, name) = token
138 .strip_prefix("no")
139 .filter(|rest| rest.len() > 2)
140 .map(|rest| (true, rest))
141 .unwrap_or((false, token));
142
143 match name {
144 "always" => {
145 *group_set |= ENABLE_SET;
146 colopts.0 = (colopts.0 & !ENABLE_MASK) | ENABLED;
147 }
148 "never" => {
149 *group_set |= ENABLE_SET;
150 colopts.0 = (colopts.0 & !ENABLE_MASK) | DISABLED;
151 }
152 "auto" => {
153 *group_set |= ENABLE_SET;
154 colopts.0 = (colopts.0 & !ENABLE_MASK) | AUTO;
155 }
156 "plain" => {
157 *group_set |= LAYOUT_SET;
158 colopts.0 = (colopts.0 & !LAYOUT_MASK) | LAYOUT_PLAIN;
159 }
160 "column" => {
161 *group_set |= LAYOUT_SET;
162 colopts.0 = (colopts.0 & !LAYOUT_MASK) | LAYOUT_COLUMN;
163 }
164 "row" => {
165 *group_set |= LAYOUT_SET;
166 colopts.0 = (colopts.0 & !LAYOUT_MASK) | LAYOUT_ROW;
167 }
168 "dense" => {
169 if neg_dense {
170 colopts.0 &= !DENSE;
171 } else {
172 colopts.0 |= DENSE;
173 }
174 }
175 _ => return Err(format!("unsupported column option '{token}'")),
176 }
177 Ok(())
178}
179
180pub fn finalize_colopts(colopts: &mut ColOpts, stdout_is_tty: bool) {
182 if colopts.0 & ENABLE_MASK != AUTO {
183 return;
184 }
185 colopts.0 &= !ENABLE_MASK;
186 if stdout_is_tty {
187 colopts.0 |= ENABLED;
188 }
189}
190
191fn compute_column_width(
192 layout: ColumnLayout,
193 list_len: usize,
194 len: &[usize],
195 cols: usize,
196 rows: usize,
197 width_idx: &mut [usize],
198) {
199 let n = list_len;
200 for x in 0..cols {
201 width_idx[x] = xy_to_linear(layout, cols, rows, x, 0);
202 for y in 0..rows {
203 let i = xy_to_linear(layout, cols, rows, x, y);
204 if i < n && len[width_idx[x]] < len[i] {
205 width_idx[x] = i;
206 }
207 }
208 }
209}
210
211pub fn print_columns(
213 out: &mut impl Write,
214 list: &[String],
215 colopts: ColOpts,
216 opts: &ColumnOptions,
217) -> io::Result<()> {
218 if list.is_empty() {
219 return Ok(());
220 }
221 if !colopts.is_active() {
222 for s in list {
223 write!(out, "{}{}{}", opts.indent, s, opts.nl)?;
224 }
225 return Ok(());
226 }
227
228 let layout = colopts.layout_mode();
229 if layout == ColumnLayout::Plain {
230 for s in list {
231 write!(out, "{}{}{}", opts.indent, s, opts.nl)?;
232 }
233 return Ok(());
234 }
235
236 let n = list.len();
237 let len: Vec<usize> = list.iter().map(|s| item_width(s)).collect();
238
239 let width_budget = opts.width.unwrap_or(79);
241 let indent_len = item_width(&opts.indent);
242
243 let mut cell_w = 0usize;
244 for &l in &len {
245 cell_w = cell_w.max(l);
246 }
247 cell_w += opts.padding;
248
249 let mut cols = (width_budget.saturating_sub(indent_len)) / cell_w;
250 if cols == 0 {
251 cols = 1;
252 }
253 let mut rows = div_round_up(n, cols);
254
255 let mut width_idx: Vec<usize> = vec![0; cols];
256 compute_column_width(layout, n, &len, cols, rows, &mut width_idx);
257
258 if colopts.dense() {
259 while rows > 1 {
260 let prev_rows = rows;
261 let prev_cols = cols;
262 rows -= 1;
263 cols = div_round_up(n, rows);
264 if cols != prev_cols {
265 width_idx.resize(cols, 0);
266 }
267 compute_column_width(layout, n, &len, cols, rows, &mut width_idx);
268
269 let mut total = indent_len;
270 for x in 0..cols {
271 total += len[width_idx[x]];
272 total += opts.padding;
273 }
274 if total > width_budget {
275 rows = prev_rows;
276 cols = prev_cols;
277 width_idx.resize(cols, 0);
278 compute_column_width(layout, n, &len, cols, rows, &mut width_idx);
279 break;
280 }
281 }
282 }
283
284 let initial_width = len.iter().copied().max().unwrap_or(0) + opts.padding;
285 let spaces = vec![b' '; initial_width];
286
287 for y in 0..rows {
288 for x in 0..cols {
289 let i = xy_to_linear(layout, cols, rows, x, y);
290 if i >= n {
291 continue;
292 }
293
294 let cell_len = len[i];
295 let mut pad_len = cell_len;
296 if len[width_idx[x]] < initial_width {
297 pad_len += initial_width - len[width_idx[x]];
298 pad_len = pad_len.saturating_sub(opts.padding);
299 }
300
301 let newline = match layout {
302 ColumnLayout::Column => i + rows >= n,
303 ColumnLayout::Row => x == cols - 1 || i == n - 1,
304 ColumnLayout::Plain => true,
305 };
306
307 if x == 0 {
308 write!(out, "{}", opts.indent)?;
309 }
310 write!(out, "{}", &list[i])?;
311 if newline {
312 write!(out, "{}", opts.nl)?;
313 } else {
314 let run = initial_width.saturating_sub(pad_len);
315 let run = run.min(spaces.len());
316 out.write_all(&spaces[..run])?;
317 }
318 }
319 }
320
321 Ok(())
322}
323
324pub fn apply_column_cli_arg(colopts: &mut ColOpts, arg: Option<&str>) -> Result<(), String> {
326 colopts.0 |= PARSEOPT;
327 colopts.0 &= !ENABLE_MASK;
328 colopts.0 |= ENABLED;
329 if let Some(a) = arg {
330 parse_column_tokens_into(a, colopts)?;
331 }
332 Ok(())
333}
334
335pub fn merge_column_config(
337 config: &crate::config::ConfigSet,
338 colopts: &mut ColOpts,
339) -> Result<(), String> {
340 if let Some(v) = config.get("column.status") {
341 parse_column_tokens_into(&v, colopts)?;
342 }
343 if let Some(v) = config.get("column.ui") {
344 parse_column_tokens_into(&v, colopts)?;
345 }
346 Ok(())
347}