print!() 格式化带有终端转义码的字符串

print!() formatting for strings with terminal escape codes

我正在编写一个应该列出目录中文件的小程序。 类似于 'ls' 所做的,但需要做其他事情。

我 运行 在将文件列表打印到屏幕时遇到了问题。

所有文件都分类为目录、可执行文件、符号链接等,因此附有特殊的 ANSI 转义码。

{} 中的宽度格式存在问题。

┆ Library /                     ┆ prcomp /                      ┆.tmux.conf@
┆ Movies /                      ┆ quicklisp /                   ┆.viminfo
┆ Music /                       ┆.CFUserTextEncoding        ┆.vimrc@

1 [Terminal Output]

/ = Directories , @ = symlinks

看到 .CFUserTextEncoding.vimrc@ 之间的尴尬间距了吗?

问题是不同的格式有不同的转义码和转义码本身的可变长度

┆[47;30m Library [0m/                     ┆[47;30m prcomp [0m/                      ┆[1;3;4;36m.tmux.conf[0m@
┆[47;30m Movies [0m/                      ┆[47;30m quicklisp [0m/                   ┆[1;38;5;15m.viminfo[0m  
┆[47;30m Music [0m/                       ┆[1;38;5;15m.CFUserTextEncoding[0m        ┆[1;3;4;36m.vimrc[0m@    

2 [Raw Output]

,îÜ = Escape character

因此,由于终端将转义字符转换为颜色并且字符串按预期显示 'trimmed',因此忽略它格式化的任何宽度。但这 'trimming' 导致格式缩短,整个格式看起来一团糟。

格式更改的所有地方都会导致格式混乱。

我试图通过使用填充向量 (vec![" "; space_count],join("")) 来解决这个问题,但这是一个完全不同的混乱领域,这个本机格式化程序到目前为止给出了最干净的结果。 有没有办法绕过转义字符并对齐文本?

编辑:

一个简单的可重现示例:

use std::{env , fs, path::PathBuf};
use ansi_term::Colour;
use term_size;

fn main() {
    println!("{esc}[2J{esc}[1;1H", esc = 27 as char); // clear
    let term_cols = match term_size::dimensions() { Some((cols,_)) => cols, None => 0};
    let mut files: Vec<PathBuf> = fs::read_dir(env::current_dir().unwrap())
                                  .unwrap()
                                  .filter_map(|file| Some(file.unwrap().path()))
                                  .collect();
    files.sort();
    let max_columns = term_cols / 30 ;
    let max_rows = (files.len() / max_columns) + 1;
    for i in 0..max_rows {
        for j in 0..max_columns {
            let index = i+(max_rows*j);
            if index >= files.len() { break; }
            if files[index].is_dir() {
                print!("|{:<1$}",format!("{}", Colour::Black.on(Colour::White)
                                                           .paint(files[index].file_name()
                                                                              .unwrap()
                                                                              .to_str()
                                                                              .unwrap())), 25);
            }
            else if files[index].symlink_metadata().unwrap().file_type().is_symlink() {
                print!("|{:<1$}", format!("{}",Colour::Cyan.bold()
                                                          .italic()
                                                          .underline()
                                                          .paint(files[index].file_name()
                                                                            .unwrap()
                                                                            .to_str()
                                                                            .unwrap())), 25);
            }
            else {
                print!("|{:<1$}", format!("{}", Colour::Fixed(15).bold()
                                                                .paint(files[index].file_name()
                                                                                   .unwrap()
                                                                                   .to_str()
                                                                                   .unwrap())), 25);
            }
        }
        print!("\n");
    }
}

Depends on

  1. ansi_term For colors & formatting
  2. term_size For getting terminal window width

这是代码的简化版本。

fmt 格式化程序不知道 CSI 转义序列,因此您需要自己进行一些计算或使用专用的 crate。

在这里计算它并不难:

  • 为每个单元格计算底层字符串(没有样式的字符串)的可见长度。您应该使用 unicode-width crate,这样当一个字符在屏幕上占用 2 个终端列或两个字符组合成仅占用一个终端列时,您的代码就不会中断
  • 将每列的宽度计算为其单元格的最大值
  • 在样式化的字符串之后 and/or 之前打印空格,以使总和等于列宽

现在,由现有的 crate 完成此计算也是合理的。有几个。我不会给出名字,因为我是其中一些作者的作者,我不想在答案中做广告,但您可以在 https://crates.io.

上搜索“终端”箱子