Column based on Descript.ion-style file parsing

The problem

I'm using an external program to gather some specific information from file objects, archives etc which I need to display in a column so the first thing I thought of was dumping it in a descript.ion file and use the metadata.other.userdescription. The file was written in the proper format and was working however I've ran into some problems with its behavior:

  • Dopus wouldn't reread the descript.ion file every time that one was written to displaying older cached values, even when forcing refresh via F5
  • ..or worse sometimes it decided to randomly overwrite my descript.ion with the old cached values for no apparent reason
  • Those things were triggered by navigating to and away from the folder

The solution

That's why I started this mini-project which illustrates how to add your custom descript.ion like column.
Here are the gains from such an approach:

  • Comments are Read-only and will never be changed or deleted on dopus' whims
  • Changes are picked up the moment the description file is modified.
  • Results are cached for the session speeding up access - even between listers and tabs
  • Coexistence with NTFS comments enabled as userdescription.
  • Virtually unlimited columns based on different descript.ion style files
  • Possibility to extend as multi-column so you can store more data ala csv in a single description file (not illustrated here)

Getting started

First let's look at a descript.ion file dopus generates when setting comments on files and NTFS comments are disabled. Those are saved in UTF-8 encoding without BOM (having a byte order mark doesn't hurt as opus ignores it when reading). You might want to stick to this format when generating your files:

<encoding#> UTF-8
item_without_space.rar Item description bla bla bla
"Item With Spaces.doc" Description of this thing as well

It resides in the directory which CONTAINS the files and folders to be described.

  • Now we look at the add-in code which initializes our new column: DescriptionX in this case:
// Set the script type to JScript to use this script
// The OnInit function is called by Directory Opus to initialize the script add-in
function OnInit(initData) {

    // Provide basic information about the script by initializing the properties of the ScriptInitData object
    initData.name = "DescriptionX";
    initData.desc = "Descript.ion-like data loader";
    initData.copyright = "Devocalypse";
    initData.default_enable = true;

    // Create a new ScriptColumn object and initialize it to add the column to Opus
    var col1 = initData.AddColumn();
    col1.name = "DescriptionX";
    col1.method = "DescriptionX";
    col1.label = "DescriptionX";
    col1.autogroup = false;
    col1.namerefresh = true;
    col1.justify = "left";
    col1.match.push_back("Yes");
    col1.match.push_back("No");
}
function DescriptionX(scriptColData) {

    //define file path - changing the last part allow you to have as many lists as needed
    var infoFilePath = DOpus.FSUtil.NewPath(scriptColData.item.path + "\\descript.ion");

    //abort if no file
    if (!DOpus.FSUtil.Exists(infoFilePath)) return;

    //load or use available
    var map = DOpus.Vars.Exists(infoFilePath) ? DOpus.Vars.Get(infoFilePath) : LoadData(infoFilePath);

    //force reload if description file has been modified since last load
    //trivia: comparator fails if both are not cast as strings
    if (String(DOpus.FSUtil.GetItem(infoFilePath).modify_utc) != String(map('~DateTimeUtc'))) {
        DOpus.Output("Cache is invalidated. Description source was modified");
        map = LoadData(infoFilePath);
    }

    //get data for item if present in the list
    if (map.exists(scriptColData.item.name)) {
        scriptColData.value = map(scriptColData.item.name);
    } else {
        //DOpus.Output("Not in map: " + scriptColData.item.name);
    }
}

function LoadData(descriptionFile) {

    DOpus.Output("Reloading: " + descriptionFile);
    var file = DOpus.FSUtil.OpenFile(descriptionFile);
    var map = DOpus.Create().Map();

    if ((file) && (file.error == 0)) {

        var blob = file.Read(file.size);
        file.close();
        var str = DOpus.Create().StringTools().Decode(blob, "utf-8");
        var ar = str.split("\n");

        //comment out the following lines if you don't write the <encoding#> UTF-8 header
        //ar.splice(0, 1); //remove unicode designation: <encoding#> UTF-8
        ar.shift(); //remove unicode designation: <encoding#> UTF-8

        //map for hashtable style dictionary lookups
        ar.forEach(function (key, val) {
            if (val.length < 1) return;
            if (val.startsWith('"')) {
                //filename contains spaces and is quoted
                var parts = val.split('"');
                map(parts[1]) = parts[2];
            } else {
                //filename has no spaces and is not quoted
                var parts = val.splitLimit(' ', 1);
                map(parts[0]) = parts[1];
            }

        });
        DOpus.Output("Added descriptions: " + map.count);
    }

    //add a timestamp for last updated
    map('~DateTimeUtc') = DOpus.FSUtil.GetItem(descriptionFile).modify_utc;

    //cache in global vars
    DOpus.Vars.Set(descriptionFile, map);

    return map;
}

Prototypes and helper functions

// <editor-fold desc="Prototypes">

String.prototype.splitLimit = function (delim, count) {
    var parts = this.split(delim);
    var tail = parts.slice(count).join(delim);
    var result = parts.slice(0, count);
    result.push(tail);
    return result;
};

Object.prototype.forEach = function (func, context) {
    var value;
    context = context || this;  //apply the function to 'this' by default
    for (key in this) {
        if (this.hasOwnProperty(key)) {  //to rule out inherited properties
            value = this[key];
            func.call(context, key, value);
        }
    }
};

String.prototype.contains = function (str, ignoreCase) {
    return (ignoreCase ? this.toUpperCase() : this).indexOf(ignoreCase ? str.toUpperCase() : str) >= 0;
};
String.prototype.startsWith = function (str, ignoreCase) {
    return (ignoreCase ? this.toUpperCase() : this).indexOf(ignoreCase ? str.toUpperCase() : str) == 0;
};
String.prototype.endsWith = function (str, ignoreCase) {
    return (ignoreCase ? this.toUpperCase() : this).slice(-str.length) == (ignoreCase ? str.toUpperCase() : str);
};
// </editor-fold>

Extras

Button script to force a reload even if the data is fresh

function OnClick(clickData) {
    var command = DOpus.NewCommand;
    var descfile = clickData.func.sourcetab.path + "\\descript.ion";
    DOpus.Output(descfile);

    if (DOpus.Vars.Exists(descfile)) {
        var map = DOpus.Vars.Get(descfile);        
        DOpus.Vars.Delete(descfile);
		DOpus.Output("Cleared " + descfile + " data. Had " + map.length + " items.");
        command.RunCommand("Go REFRESH");
    } else {
        DOpus.Output("Variable " + descfile + " is not set.");
    }
}

Download full add-in

DescriptionX.zip (2.3 KB)

2 Likes

Thank You very much !!! :slight_smile:
Now I have for my Work-Folders new 8 columns which I have so long needet!

Regards,
Mani

1 Like

I am a bit lost on how to use this. I would like to modify the descript.ion file without the associated problems.
For example, my descript.ion file contains:
test.txt 1

I then edit it to contain
test.txt 2

However, the "User Description" column still shows "1" and the "DescriptionX" column shows nothing.

Is the DescriptionX column supposed to show "2"? How?
Also, will I need to ignore the "User Description" column, which is not properly updated?

It should work out of the box unless:

  • Your DOpus version is not high enough
  • You messed up in creating the descript.ion file

Latter should be utf-8 encoded. Remove the ar.shift(); line if your file descriptions start at the first line.
And yes, DescX column should show "2". and user description reads via the built-in method so ignore it.

What min DOpus version is required?
How do I get multiple columns (Mani above posted he had 8 new columns)?

I have renamed the DescriptionX.js file to "Column1.js", second copy of to "Column2.js" third to "Column3.js" etc.
Each of the File has become a own comment file like "columnn1.ion", "column2.ion", "column3.ion"... in a Folder in which i want to add the columns.
In file "Column1" I have modified the

//define file path
    var infoFilePath = DOpus.FSUtil.NewPath(scriptColData.item.path + "\\column1.ion");

and (carefully) other things, like name of the column etc...

OK. Thanks Mani.

It seems like DOpus always creates descript.ion files that starts at the first line.

Why would it be advantageous to change it to include the <encoding#> UTF-8 line? If I keep the ar.shift(); line in the DescriptionX.js, then it would not be compatible with the DOpus generated descript.ion files...

Also, how do I create the Button to force a reload (and when would I need it)?

Sorry peter2,
but this is to difficult and complex for me, to answer in the english language.... :frowning:

I will no make mistake...