0

I am writing a small program that is supposed to list files in a directory. Similar to what 'ls' does but needed for something else.

I am running into a problem when printing the list of files to the screen.

All of the files are sorted into directories,executables,symlinks,etc and so have special ansi escape codes attatched.

The problem comes with the width formatting inside the {}.

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

1 [Terminal Output]

/ = Directories , @ = symlinks

See the awkard spacing between .CFUserTextEncoding and .vimrc@ ?

The problem is that different formatting has different escape codes and variable length of the escape code itself

┆[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

So whatever width it formats with is ignored since the terminal turns the escape characters into colors and the string appears 'trimmed' as expected. But this 'trimming' is causing the very formatting to cut short and the whole formatting looks messed up.

Everywhere where the formatting changes it is causing this formatting mess.

I've tried to workaround this by using a padding vector(vec![" "; space_count],join("")) but that is a whole different dominion of mess and this native formatter by far gave the most cleanest results. Is there a way for go around the escape characters and align the text?

Edit:

A simple reproducible example:

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

This is a simplified version of the code.

4
  • It's hard to tell what you exactly do because there's no simple reproductible code in your question Commented Oct 17, 2021 at 17:02
  • 2
    Printing styled strings to the terminal is usually not done with just println on strings containing CSI sequences. Several crates are dedicated to this kind of task. Commented Oct 17, 2021 at 17:03
  • @DenysSéguret added the code. Commented Oct 18, 2021 at 5:10
  • @DenysSéguret I wanted to keep the dependencies as low as possible for this one. What crate do you recommend for this? Commented Oct 18, 2021 at 5:15

1 Answer 1

1

The fmt formatter doesn't know about CSI escape sequences, so you need to either do some computations yourself or use a dedicated crate.

Computing it is not that hard here:

  • for each cell compute the visible length of the underlying string (the one without the style). You should use the unicode-width crate so that your code doesn't break when a character takes 2 terminal columns on screen or when two characters are combined to take only one terminal column
  • compute the width of each column as the max of their cells
  • print spaces before and/or after the styled string to have a constant sum equal to the column width

Now, it's also reasonable to have this computation done by an existing crate. There are several ones. I won't give names as I'm the author of some of them and I don't want to advertise in an answer but you may search "terminal" crates on https://crates.io.

Sign up to request clarification or add additional context in comments.

2 Comments

Okay. Followed your advice and made a computation algorithm. I was checking for lengths before to find the file with the longest name, just repurposed that to also pass each string's length along with the string in a tuple (String,usize). Can say it was some work but it looks phenomenal and I didn't have to use a single dependency. Thanks Denys!. Also its no problem to advertise if it solves someones problem :)
@RakshitShetty I didn't want to make my answer look like some kind of ad. Now if you want to look at existing crates, you may have a look at some of the most popular crates of my GitHub account (github.com/Canop) like lfs or termimad. Then look at those made by other developers (search for "terminal crate"). You'll notice they're all (mine and the other ones) very "opinionated" and none of them is obviously the one anybody should use which is why I'm glad you could find your own way!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.