Custom macros to integrate Todo.txt with Komodo Edit

Komodo edit code editor

I went to a PHP developer’s meetup recently, where the topic was IDE’s. As some of you may know, I’m a dedicated Vim user, but that’s because I’m most productive using Vim. I don’t have any extreme aversion to using an IDE. As a matter of fact, if I ever find one that really works for me, then I’d start using it. In any case, I’ve started trying to work Komodo Edit into my workflow. As far as I’m concerned, Komodo Edit is the most flexible IDE out there, and that’s part of the reason that I have trouble finding IDE’s. At any given moment, I may have a mix of perl, python, ruby, xml, yaml, javascript and html files open. No IDE that I’ve ever used has been able to handle that kind of mix. Language optimized IDE’s like PHPStorm or PyCharm are great for their intended language, and if all you do all day is work PHP, then that may work for you. For me, Komodo does a good job with almost every language that I work with.

One of komodo’s biggest strengths, is that it’s built on top of the Mozilla engine, so it’s extremely extensible. Basically, you can write Firefox extensions that work with Komodo. Of course, you’ll have to dig through a metric ton of documentation, not all of it up to date, in order to learn how to write an extension. I’m thinking of writing a “Hello World” tutorial, if for no other reason than to document what I’ve learned so far. But, never fear, there’s hope. Given the fact that Komodo Edit is basically a stripped down version of firefox with a specialized set of extensions, it has a really powerful javascript engine and API. It also allows you to create “Macros”, which can be pretty complex javascript applications. I’ve created a set of macros that allow me to work with my Todo.txt files (I highly recommend Gina Trapani’s implementation at http://todotxt.com/). So let’s take a look at what I’ve done.

Komodo edit toolbox

My Todo.txt macros in their own cozy folder in the toolbox.

First off, I created a new folder in my Komodo Edit toolbox to group the macros together in one place. Then I created a simple macro to open my todo.txt. This one was pretty simple. All it does is to open my todo.txt file in a new buffer:

1
2
3
4
    komodo.assertMacroVersion(3);
    if (komodo.view) { komodo.view.setFocus(); }
 
    komodo.openURI("file:///home/rhibbitts/Dropbox/todo/todo.txt");

Next, I wanted the ability to sort the tasks in the file according to priority. Priority in a Todo.txt file (at least, according to Gina’s format: https://github.com/ginatrapani/todo.txt-cli/wiki/The-Todo.txt-Format) is determined by an alphabetical character at the beginning of the line. So, if your top priority is denoted by ‘A’, then an alphabetical sort, should put that at the top of the line. And in fact, I found an existing Komodo macro on the Komodo forums that works out of the box. It was posted by ericp, who’s an ActiveState staff member (http://community.activestate.com/node/9740). Here it is in it’s base form:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// All code below derived from Davide Ficano's Morekomodo extension
var eolText = [];
eolText[Components.interfaces.koIDocument.EOL_CR] = '\r';
eolText[Components.interfaces.koIDocument.EOL_CRLF] = '\r\n';
eolText[Components.interfaces.koIDocument.EOL_LF] = '\n';
function SortOptions() {
    this.removeDuplicate = false;
    this.ignoreCase = true;
    this.ascending = true;
    this.numeric = false;
}
 
function getSortedBuffer(arr, sortOptions, nl) {
    alert (JSON.stringify(arr));
    var ltValue = sortOptions.ascending ? -1 : 1;
    var gtValue = sortOptions.ascending ? 1 : -1;
 
    function sortFn(a, b) {
        var sa = a;
        var sb = b;
 
        if (sortOptions.ignoreCase) {
            sa = a.toLowerCase();
            sb = b.toLowerCase();
        }
        if (sortOptions.numeric) {
            var na = parseFloat(sa);
            var nb = parseFloat(sb);
 
            if (!isNaN(na) && !isNaN(nb)) {
                sa = na;
                sb = nb;
            }
        }
        if (sa == sb) {
            return 0;
        }
        return sa < sb ? ltValue : gtValue;
    }
    arr.sort(sortFn);
 
    var s = "";
    if (sortOptions.removeDuplicate) {
        var prevLine = null;
        for (var i = 0; i < arr.length; i++) {
            var lineData = arr[i];
 
            if (prevLine != lineData) {
                s += lineData + nl;
                prevLine = lineData;
            }
        }
        // remove last newline
        s = s.substring(0, s.length - nl.length);
    } else {
        s = arr.join(nl);
    }
 
    return s;
}
 
function sortView(view, sortOptions) {
    var scimoz = view.scintilla.scimoz;
    var sel = view.selection;
    var useSelection = sel.length != 0;
 
    var firstLine, lastLine;
    if (useSelection) {
        var selStartPos = scimoz.selectionStart;
        var selEndPos = scimoz.selectionEnd;
 
        firstLine = scimoz.lineFromPosition(selStartPos);
        lastLine = scimoz.lineFromPosition(selEndPos);
 
        var firstColumnPos = scimoz.positionFromLine(lastLine);
        // if selection ends before first column means the line isn't involved
        if (firstColumnPos == selEndPos) {
            --lastLine;
        }
    } else {
        firstLine = 0;
        lastLine = scimoz.lineCount - 1;
    }
 
    if (firstLine == lastLine) {
        return;
    }
 
    var lines = [];
    for (var i = firstLine; i <= lastLine; i++) {
        var startPos = scimoz.positionFromLine(i);
        var endPos = scimoz.getLineEndPosition(i);
 
        lines.push(scimoz.getTextRange(startPos, endPos));
    }
 
    var s = getSortedBuffer(lines, sortOptions,
                            eolText[view.koDoc.new_line_endings]);
 
    if (useSelection) {
        scimoz.selectionStart = scimoz.positionFromLine(firstLine);
        scimoz.selectionEnd = scimoz.getLineEndPosition(lastLine);
    } else {
        scimoz.selectAll();
    }
    scimoz.replaceSel(s);
}
 
var sortOptions = new SortOptions(); // see defaults above
sortView(ko.views.manager.currentView, sortOptions);

Another thing that’s nice to do is to be able to sort the tasks by context and project. That’s probably something that may differ quite a bit between users, so I’m not sure how usable these are for other people. But assuming that you set the context for a task using this syntax, @context, then it should work for you. Basically, it’s a reworking of the initial sort script, to account for contexts. I’m planning to add another one for sorting by project, which will basically be the same thing. In any case, here’s the one to sort based on context. Note that this does sort the individual tasks in a given context. So, you’ll have the tasks in a given context, in the correct order.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// All code below derived from Davide Ficano's Morekomodo extension
var eolText = [];
eolText[Components.interfaces.koIDocument.EOL_CR] = '\r';
eolText[Components.interfaces.koIDocument.EOL_CRLF] = '\r\n';
eolText[Components.interfaces.koIDocument.EOL_LF] = '\n';
function SortOptions() {
    this.removeDuplicate = false;
    this.ignoreCase = true;
    this.ascending = true;
    this.numeric = false;
}
 
function getSortedBuffer(arr, sortOptions, nl) {
    var contextDict = {};
    for (var i = 0; i < arr.length; i++) {
        if (arr[i].indexOf(" @") !== -1) {
           words = arr[i].split(' ');
           for (var j = 0; j < words.length; j++) {
            if (words[j].indexOf("@") == 0 ) {
                if (!contextDict[words[j]]) {
                    contextDict[words[j]] = [];
                    contextDict[words[j]].push(arr[i]); 
                }else{
                    contextDict[words[j]].push(arr[i]); 
                }
            }
           }
        }else{
                if( contextDict['no_context'] ){
                contextDict['no_context'].push(arr[i]);
                }else{
                    contextDict['no_context'] = [];
                    contextDict['no_context'].push(arr[i]);
                }
            }
    }
    //ko.logging.getLogger("sort_by_context").warn(words[j]);
    var ltValue = sortOptions.ascending ? -1 : 1;
    var gtValue = sortOptions.ascending ? 1 : -1;
 
    function sortFn(a, b) {
        var sa = a;
        var sb = b;
 
        if (sortOptions.ignoreCase) {
            sa = a.toLowerCase();
            sb = b.toLowerCase();
        }
        if (sortOptions.numeric) {
            var na = parseFloat(sa);
            var nb = parseFloat(sb);
 
            if (!isNaN(na) && !isNaN(nb)) {
                sa = na;
                sb = nb;
            }
        }
        if (sa == sb) {
            return 0;
        }
        return sa < sb ? ltValue : gtValue;
    }
    for(var sortContext in contextDict){
        contextDict[sortContext].sort(sortFn);
    }
 
    var s = "";
    if (sortOptions.removeDuplicate) {
        var prevLine = null;
        for (var i = 0; i < arr.length; i++) {
            var lineData = arr[i];
 
            if (prevLine != lineData) {
                s += lineData + nl;
                prevLine = lineData;
            }
        }
        // remove last newline
        s = s.substring(0, s.length - nl.length);
    } else {
        var context_array = [];
        for (var context in contextDict) {
            contextDict[context].join(nl);
            context_array.push(contextDict[context].join(nl));
        }
        s = context_array.join(nl);
    }
 
    return s;
}
 
function sortView(view, sortOptions) {
    var scimoz = view.scintilla.scimoz;
    var sel = view.selection;
    var useSelection = sel.length != 0;
 
    var firstLine, lastLine;
    if (useSelection) {
        var selStartPos = scimoz.selectionStart;
        var selEndPos = scimoz.selectionEnd;
 
        firstLine = scimoz.lineFromPosition(selStartPos);
        lastLine = scimoz.lineFromPosition(selEndPos);
 
        var firstColumnPos = scimoz.positionFromLine(lastLine);
        // if selection ends before first column means the line isn't involved
        if (firstColumnPos == selEndPos) {
            --lastLine;
        }
    } else {
        firstLine = 0;
        lastLine = scimoz.lineCount - 1;
    }
 
    if (firstLine == lastLine) {
        return;
    }
 
    var lines = [];
    for (var i = firstLine; i <= lastLine; i++) {
        var startPos = scimoz.positionFromLine(i);
        var endPos = scimoz.getLineEndPosition(i);
 
        ko.logging.getLogger("Line text is:").warn(scimoz.getTextRange(startPos, endPos));
        if (scimoz.getTextRange(startPos, endPos) !== "") {
            lines.push(scimoz.getTextRange(startPos, endPos));
        }
    }
 
    //alert (JSON.stringify(lines));
    var s = getSortedBuffer(lines, sortOptions,
                            eolText[view.koDoc.new_line_endings]);
 
    if (useSelection) {
        scimoz.selectionStart = scimoz.positionFromLine(firstLine);
        scimoz.selectionEnd = scimoz.getLineEndPosition(lastLine);
    } else {
        scimoz.selectAll();
    }
    scimoz.replaceSel(s);
}
 
var sortOptions = new SortOptions(); // see defaults above
sortView(ko.views.manager.currentView, sortOptions);

Note that you can also assign keyboard shortcuts to run these macros, so you never even have to leave the keyboard. Komodo’s extensibility is just awesome. It can literally be extended to do almost anything that you can imagine. I’m thinking about making this one my first screencast. When and if I do that, then I’ll post a link to the video here.