use std::io::{self, Write};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnLayout {
Column,
Row,
Plain,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ColOpts(u32);
const LAYOUT_MASK: u32 = 0x000F;
const ENABLE_MASK: u32 = 0x0030;
const PARSEOPT: u32 = 0x0040;
const DENSE: u32 = 0x0080;
const DISABLED: u32 = 0x0000;
const ENABLED: u32 = 0x0010;
const AUTO: u32 = 0x0020;
const LAYOUT_COLUMN: u32 = 0;
const LAYOUT_ROW: u32 = 1;
const LAYOUT_PLAIN: u32 = 15;
impl ColOpts {
#[must_use]
pub const fn new() -> Self {
Self(0)
}
fn layout_bits(self) -> u32 {
self.0 & LAYOUT_MASK
}
#[must_use]
pub fn is_active(self) -> bool {
self.0 & ENABLE_MASK == ENABLED
}
fn dense(self) -> bool {
self.0 & DENSE != 0
}
fn layout_mode(self) -> ColumnLayout {
match self.layout_bits() {
LAYOUT_ROW => ColumnLayout::Row,
LAYOUT_PLAIN => ColumnLayout::Plain,
_ => ColumnLayout::Column,
}
}
}
impl Default for ColOpts {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ColumnOptions {
pub width: Option<usize>,
pub padding: usize,
pub indent: String,
pub nl: String,
}
impl Default for ColumnOptions {
fn default() -> Self {
Self {
width: None,
padding: 1,
indent: String::new(),
nl: "\n".to_owned(),
}
}
}
fn div_round_up(a: usize, b: usize) -> usize {
if b == 0 {
return a;
}
a.div_ceil(b)
}
fn item_width(s: &str) -> usize {
UnicodeWidthStr::width(s)
}
fn xy_to_linear(layout: ColumnLayout, cols: usize, rows: usize, x: usize, y: usize) -> usize {
match layout {
ColumnLayout::Column => x * rows + y,
ColumnLayout::Row => y * cols + x,
ColumnLayout::Plain => y,
}
}
pub fn parse_column_tokens_into(value: &str, colopts: &mut ColOpts) -> Result<(), String> {
let mut group_set: u8 = 0;
for raw in value.split([' ', ',']) {
let token = raw.trim();
if token.is_empty() {
continue;
}
parse_one_token(token, colopts, &mut group_set)?;
}
if group_set & 1 != 0 && group_set & 2 == 0 {
colopts.0 = (colopts.0 & !ENABLE_MASK) | ENABLED;
}
Ok(())
}
fn parse_one_token(token: &str, colopts: &mut ColOpts, group_set: &mut u8) -> Result<(), String> {
const LAYOUT_SET: u8 = 1;
const ENABLE_SET: u8 = 2;
let (neg_dense, name) = token
.strip_prefix("no")
.filter(|rest| rest.len() > 2)
.map(|rest| (true, rest))
.unwrap_or((false, token));
match name {
"always" => {
*group_set |= ENABLE_SET;
colopts.0 = (colopts.0 & !ENABLE_MASK) | ENABLED;
}
"never" => {
*group_set |= ENABLE_SET;
colopts.0 = (colopts.0 & !ENABLE_MASK) | DISABLED;
}
"auto" => {
*group_set |= ENABLE_SET;
colopts.0 = (colopts.0 & !ENABLE_MASK) | AUTO;
}
"plain" => {
*group_set |= LAYOUT_SET;
colopts.0 = (colopts.0 & !LAYOUT_MASK) | LAYOUT_PLAIN;
}
"column" => {
*group_set |= LAYOUT_SET;
colopts.0 = (colopts.0 & !LAYOUT_MASK) | LAYOUT_COLUMN;
}
"row" => {
*group_set |= LAYOUT_SET;
colopts.0 = (colopts.0 & !LAYOUT_MASK) | LAYOUT_ROW;
}
"dense" => {
if neg_dense {
colopts.0 &= !DENSE;
} else {
colopts.0 |= DENSE;
}
}
_ => return Err(format!("unsupported column option '{token}'")),
}
Ok(())
}
pub fn finalize_colopts(colopts: &mut ColOpts, stdout_is_tty: bool) {
if colopts.0 & ENABLE_MASK != AUTO {
return;
}
colopts.0 &= !ENABLE_MASK;
if stdout_is_tty {
colopts.0 |= ENABLED;
}
}
fn compute_column_width(
layout: ColumnLayout,
list_len: usize,
len: &[usize],
cols: usize,
rows: usize,
width_idx: &mut [usize],
) {
let n = list_len;
for x in 0..cols {
width_idx[x] = xy_to_linear(layout, cols, rows, x, 0);
for y in 0..rows {
let i = xy_to_linear(layout, cols, rows, x, y);
if i < n && len[width_idx[x]] < len[i] {
width_idx[x] = i;
}
}
}
}
pub fn print_columns(
out: &mut impl Write,
list: &[String],
colopts: ColOpts,
opts: &ColumnOptions,
) -> io::Result<()> {
if list.is_empty() {
return Ok(());
}
if !colopts.is_active() {
for s in list {
write!(out, "{}{}{}", opts.indent, s, opts.nl)?;
}
return Ok(());
}
let layout = colopts.layout_mode();
if layout == ColumnLayout::Plain {
for s in list {
write!(out, "{}{}{}", opts.indent, s, opts.nl)?;
}
return Ok(());
}
let n = list.len();
let len: Vec<usize> = list.iter().map(|s| item_width(s)).collect();
let width_budget = opts.width.unwrap_or(79);
let indent_len = item_width(&opts.indent);
let mut cell_w = 0usize;
for &l in &len {
cell_w = cell_w.max(l);
}
cell_w += opts.padding;
let mut cols = (width_budget.saturating_sub(indent_len)) / cell_w;
if cols == 0 {
cols = 1;
}
let mut rows = div_round_up(n, cols);
let mut width_idx: Vec<usize> = vec![0; cols];
compute_column_width(layout, n, &len, cols, rows, &mut width_idx);
if colopts.dense() {
while rows > 1 {
let prev_rows = rows;
let prev_cols = cols;
rows -= 1;
cols = div_round_up(n, rows);
if cols != prev_cols {
width_idx.resize(cols, 0);
}
compute_column_width(layout, n, &len, cols, rows, &mut width_idx);
let mut total = indent_len;
for x in 0..cols {
total += len[width_idx[x]];
total += opts.padding;
}
if total > width_budget {
rows = prev_rows;
cols = prev_cols;
width_idx.resize(cols, 0);
compute_column_width(layout, n, &len, cols, rows, &mut width_idx);
break;
}
}
}
let initial_width = len.iter().copied().max().unwrap_or(0) + opts.padding;
let spaces = vec![b' '; initial_width];
for y in 0..rows {
for x in 0..cols {
let i = xy_to_linear(layout, cols, rows, x, y);
if i >= n {
continue;
}
let cell_len = len[i];
let mut pad_len = cell_len;
if len[width_idx[x]] < initial_width {
pad_len += initial_width - len[width_idx[x]];
pad_len = pad_len.saturating_sub(opts.padding);
}
let newline = match layout {
ColumnLayout::Column => i + rows >= n,
ColumnLayout::Row => x == cols - 1 || i == n - 1,
ColumnLayout::Plain => true,
};
if x == 0 {
write!(out, "{}", opts.indent)?;
}
write!(out, "{}", &list[i])?;
if newline {
write!(out, "{}", opts.nl)?;
} else {
let run = initial_width.saturating_sub(pad_len);
let run = run.min(spaces.len());
out.write_all(&spaces[..run])?;
}
}
}
Ok(())
}
pub fn apply_column_cli_arg(colopts: &mut ColOpts, arg: Option<&str>) -> Result<(), String> {
colopts.0 |= PARSEOPT;
colopts.0 &= !ENABLE_MASK;
colopts.0 |= ENABLED;
if let Some(a) = arg {
parse_column_tokens_into(a, colopts)?;
}
Ok(())
}
pub fn merge_column_config(
config: &crate::config::ConfigSet,
colopts: &mut ColOpts,
) -> Result<(), String> {
if let Some(v) = config.get("column.status") {
parse_column_tokens_into(&v, colopts)?;
}
if let Some(v) = config.get("column.ui") {
parse_column_tokens_into(&v, colopts)?;
}
Ok(())
}