6bytes
6bytes

Reputation: 6132

NodeJS script to show update per line in console

I have a small command line program written in NodeJS to process all text files in a given directory. Since Node is asynch the script will read all the files, process them and output something like this

Converting file: example-file-1.txt
Converting file: example-file-2.txt
Converting file: example-file-3.txt
Converting file: example-file-4.txt
File example-file-3.txt converted!
File example-file-1.txt converted!
File example-file-2.txt converted!
File example-file-4.txt converted!

This is not very pretty, the "converted" messages are not in order as the files are different size and are finished processing at different speeds.
What I'd like to see is something like this

File example-file-1.txt: DONE! // processing of this file has finished
File example-file-2.txt: converting... // this file is still processing
File example-file-3.txt: DONE!
File example-file-4.txt: converting...

The most right part of each line should be dynamically updated with progress.

What I'm really asking here is how to display in console a few lines of messages that I can update as the script progresses?

Upvotes: 6

Views: 3325

Answers (2)

6bytes
6bytes

Reputation: 6132

I ended up processing all my files in a synchronous way with a simple recursive function in a callback. User Ora library to show progress with each file in terminal window.

var files = [];

// get all "txt" files from a given directory and store them in an array "files"
function getFiles(dir) {
    var filesRaw = fs.readdirSync(dir);
    for (var i in filesRaw) {
        var name = dir === '.' ? filesRaw[i] : dir + '/' + filesRaw[i];
        var ext = path.extname(name);
        if (!fs.statSync(name).isDirectory()) {
            if (ext == '.txt') {
                files.push(path.resolve(process.cwd(), name));
            }
        }
    }
    // only after the directory is read, run the function that starts processing those files
    recode(files);
}

function recode() {
    if (files.length > 0) {
        var fileName = files.shift();
        // this is the terminal spinner library
        const spinner = ora(`File ${path.basename(fileName)} processing...`).start();
        var fileSingle = fs.readFileSync(fileName);
        var data = new Buffer(fileSingle, "ascii");
        var translated = encoding.convert(data, "UTF-8", "CP1250");
        var converted = iconvlite.encode(translated, "utf8").toString();

        fs.writeFile(fileName, converted, function(err) {
            if (err) {
                spinner.fail(`File ${path.basename(fileName)} failed.`);
                console.log(err);
            } else {
                spinner.succeed(`File ${path.basename(fileName)} DONE!`);
            }
            // process another file from the files array only when the previous file is finished and saved to disk
            recode(files);
        });
    } else {
        console.log('No more files to process or all files done.');
    }
}

Upvotes: 0

Robert
Robert

Reputation: 156

Terminals

Using standard terminal capabilities, there is only the carriage return \r that allows to reset the cursor to the start of the current line and overwrite it for updates.

Most terminals support ANSI/VT100 control codes that allow setting colors, cursor positioning and other screen updates. Node modules making use of these are for example:

  • charm: Use ansi terminal characters to write colors and cursor positions.
  • blessed: A curses-like library with a high level terminal interface API for node.js.

Windows supports the escape sequences after some tweaks.

Minimal Example

The following example solves the task by sending the control commands directly and writing out a cached screen. The commands used are:

  1. repositioning the cursor in the top left corner by sending \x1b[H and
  2. clearing the screen by sending \x1b[2J.

Writing more lines than available will result in flickering.

var progress = {
    'example-file-1.txt': 'converting',
    'example-file-2.txt': 'converting',
    'example-file-3.txt': 'converting'
}

function renderProgress() {
    // reset cursor, clear screen, do not write a new line
    process.stdout.write('\x1b[H\x1b[2J')

    // render progress
    Object.keys(progress).forEach(filename => console.log(`${filename}: ${progress[filename]}`))
}

function updateProgress(filename, status) {
    progress[filename] = status
    renderProgress()
}

// render initial progress
renderProgress()

// simulare updates
setTimeout(() => updateProgress('example-file-2.txt', 'done'), 1000)
setTimeout(() => updateProgress('example-file-1.txt', 'reconsidering'), 2500)
setTimeout(() => updateProgress('example-file-3.txt', 'done'), 4000)
setTimeout(() => updateProgress('example-file-1.txt', 'done'), 6000)

Upvotes: 3

Related Questions