├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin ├── nodo └── nodo-install ├── doc ├── nodo.1 └── schema.sql ├── lib ├── aux.js ├── command.js ├── config.js ├── database.js ├── init.js ├── list.js └── task.js ├── package.json ├── samples ├── nodo.db_sample └── nodorc_sample └── test ├── command.test.js └── task.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test/test.db 3 | node_modules 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .git* 3 | test/ 4 | doc/ 5 | grunt.js 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "stable" 5 | - "6" 6 | - "4" 7 | before_script: 8 | - npm install -g grunt-cli 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Rogério Vicente 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nodo [![Build Status](https://travis-ci.org/rogeriopvl/nodo.png)](https://travis-ci.org/rogeriopvl/nodo) 2 | 3 | ## About 4 | 5 | Nodo is a command line TODO application that uses a portable database file. Also, if you are a [Wunderlist][0] user, you can configure Nodo to be a command line interface to Wunderlist's database (only for version 1.* of Wunderlist). 6 | 7 | The name "Nodo" comes from the mix of the words Node and TODO. 8 | 9 | ### Screencast Demo 10 | [![Nodo Demo Video](https://i.vimeocdn.com/video/539197203.webp?mw=1920&mh=1080&q=70)](https://vimeo.com/42330826) 11 | 12 | ## Install 13 | 14 | Nodo is available as a package in the npm registry, so you can install it with: 15 | 16 | npm install -g nodo 17 | 18 | At install, Nodo creates a default configuration file (`~/.nodorc`) and a default local database (`~/.nodo.db`) with some sample tasks just to get you started. 19 | You can rename and/or move you database file as long as you update your config file to reflect it's current location. 20 | 21 | ### Using Wunderlist database (only for Wunderlist 1.*) 22 | 23 | *Important:* Nodo is not compatible with Wunderlist 2. And I don't plan to fix this in the near future unless there's high demand. 24 | 25 | I you wan't to use the Wunderlist database with Nodo you need to edit the config file and make sure that the database location parameter has the Wunderlist database file path. For instance in Mac OSX, the Wunderlist database file is at `~/Library/Wunderlist/wunderlist.db`, so just make your config file look like this: 26 | 27 | { 28 | "database": { 29 | "location": "~/Library/Wunderlist/wunderlist.db", 30 | } 31 | } 32 | 33 | And you'll be all set to organize your day like a hacker! 34 | 35 | *Important:* Nodo does not delete any data in the Wunderlist database. Even if you delete tasks, they are just marked as deleted, and can be recovered with the `nodo restore` command. 36 | 37 | ## First Run 38 | 39 | On the first run nodo asks your permission to anonymously track some usage patterns. This is very useful to improve nodo, but completely optional and anonymous. Only major commands like `show`, `help`, `list`, etc are tracked. Their respective arguments are not tracked. 40 | 41 | ## Usage 42 | 43 | Usage: nodo [arguments] 44 | 45 | Available actions and options: 46 | nodo show Show all lists and tasks todo 47 | nodo show all Same as above 48 | nodo show lists Show all lists and number of tasks in each one. 49 | nodo show Show content of list 50 | nodo show done Show all done tasks 51 | nodo show deleted Show all deleted tasks 52 | nodo show task Show detail of a task 53 | 54 | nodo add list Add a new list 55 | nodo add Add a new task to list 56 | 57 | nodo done Mark a task as done 58 | nodo undo Mark a task as not done 59 | 60 | nodo star Mark a task as important 61 | nodo unstar Mark a task as not important 62 | 63 | nodo move Moves a task to a list 64 | 65 | nodo delete list Delete list 66 | nodo delete task Delete task 67 | 68 | nodo restore Restore task 69 | nodo restore task Restore task 70 | nodo restore list Restore list 71 | 72 | ## Bug Report 73 | 74 | Nodo is in it's early versions. If you find any problems using Nodo, please report them back to me by opening an issue on Github. 75 | 76 | ## Credits 77 | 78 | Thanks to: 79 | 80 | * Pedro Faria, for his precious help in debugging Nodo on Linux. 81 | 82 | [0]: http://wunderlist.com 83 | -------------------------------------------------------------------------------- /bin/nodo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/init') 4 | -------------------------------------------------------------------------------- /bin/nodo-install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Post install script that creates config file and local database 5 | * with the schema and some default tasks. 6 | */ 7 | 8 | var config = require('../lib/config') 9 | var aux = require('../lib/aux') 10 | var fs = require('fs-extra') 11 | var path = require('path') 12 | require('colors') 13 | 14 | console.log('****************************************') 15 | console.log('* Running Nodo installation script...') 16 | console.log('****************************************') 17 | 18 | if (!fs.existsSync(config.path)) { 19 | console.log('Creating config file in ' + config.path + ' ...') 20 | 21 | var rcSample = path.join(__dirname, '..', 'samples', 'nodorc_sample') 22 | 23 | fs.copy(rcSample, config.path, function(err) { 24 | if (err) { 25 | console.log(err.toString()) 26 | process.exit(-1) 27 | } 28 | 29 | console.log('Done.'.green) 30 | 31 | // remove the module cache, to update the new generated configs 32 | delete require 33 | .cache[path.resolve(path.join(__dirname, '..', 'lib', 'config.js'))] 34 | var config = require('../lib/config') 35 | createDatabase(config.file.database.location) 36 | }) 37 | } else { 38 | console.log('Config file already exists. Skipping...'.yellow) 39 | createDatabase(config.file.database.location) 40 | } 41 | 42 | console.log('Nodo installation is complete.'.green) 43 | 44 | function createDatabase(dbPath) { 45 | var databaseLocation = aux.expand(dbPath) 46 | 47 | if (!fs.existsSync(databaseLocation)) { 48 | console.log('Creating database in ' + databaseLocation) 49 | 50 | var dbSample = path.join(__dirname, '..', 'samples', 'nodo.db_sample') 51 | fs.copy(dbSample, databaseLocation, function(err) { 52 | if (err) { 53 | console.log(err.toString()) 54 | process.exit(-1) 55 | } 56 | }) 57 | console.log('Done.'.green) 58 | } else { 59 | console.log('Database file already exists. Skipping...'.yellow) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /doc/nodo.1: -------------------------------------------------------------------------------- 1 | .TH "NODO" "" "September 2013" "" "" 2 | .SH "NAME" 3 | \fBNodo\fR 4 | .SH About 5 | .P 6 | Nodo is a command line TODO application that uses a portable database file\. Also, if you are a Wunderlist \fIhttp://wunderlist\.com\fR user, you can configure Nodo to be a command line interface to Wunderlist\'s database (only for version 1\.* of Wunderlist)\. 7 | .P 8 | The name "Nodo" comes from the mix of the words Node and TODO\. 9 | .SH Install 10 | .P 11 | Nodo is available as a package in the npm registry, so you can install it with: 12 | .P 13 | .RS 2 14 | .EX 15 | npm install \-g nodo 16 | .EE 17 | .RE 18 | .P 19 | At install, Nodo creates a default configuration file (\fB~/\.nodorc\fR) and a default local database (\fB~/\.nodo\.db\fR) with some sample tasks just to get you started\. 20 | You can rename and/or move you database file as long as you update your config file to reflect it\'s current location\. 21 | .SS Using Wunderlist database (only for Wunderlist 1\.*) 22 | .P 23 | \fIImportant:\fR Nodo is not compatible with Wunderlist 2\. And I don\'t plan to fix this in the near future unless there\'s high demand\. 24 | .P 25 | I you wan\'t to use the Wunderlist database with Nodo you need to edit the config file and make sure that the database location parameter has the Wunderlist database file path\. For instance in Mac OSX, the Wunderlist database file is at \fB~/Library/Wunderlist/wunderlist\.db\fR, so just make your config file look like this: 26 | .P 27 | .RS 2 28 | .EX 29 | { 30 | "database": { 31 | "location": "~/Library/Wunderlist/wunderlist\.db", 32 | } 33 | } 34 | .EE 35 | .RE 36 | .P 37 | And you\'ll be all set to organize your day like a hacker! 38 | .P 39 | \fIImportant:\fR Nodo does not delete any data in the Wunderlist database\. Even if you delete tasks, they are just marked as deleted, and can be recovered with the \fBnodo restore\fR command\. 40 | .SH First Run 41 | .P 42 | On the first run nodo asks your permission to anonymously track some usage patterns\. This is very useful to improve nodo, but completely optional and anonymous\. Only major commands like \fBshow\fR, \fBhelp\fR, \fBlist\fR, etc are tracked\. Their respective arguments are not tracked\. 43 | .SH Usage 44 | .P 45 | .RS 2 46 | .EX 47 | Usage: nodo [arguments] 48 | 49 | Available actions and options: 50 | nodo show Show all lists and tasks todo 51 | nodo show all Same as above 52 | nodo show lists Show all lists and number of tasks in each one\. 53 | nodo show Show content of list 54 | nodo show done Show all done tasks 55 | nodo show deleted Show all deleted tasks 56 | nodo show task Show detail of a task 57 | 58 | nodo add list Add a new list 59 | nodo add Add a new task to list 60 | 61 | nodo done Mark a task as done 62 | nodo undo Mark a task as not done 63 | 64 | nodo star Mark a task as important 65 | nodo unstar Mark a task as not important 66 | 67 | nodo move Moves a task to a list 68 | 69 | nodo delete list Delete list 70 | nodo delete task Delete task 71 | 72 | nodo restore Restore task 73 | nodo restore task Restore task 74 | nodo restore list Restore list 75 | .EE 76 | .RE 77 | .SH Bug Report 78 | .P 79 | Nodo is in it\'s early versions\. If you find any problems using Nodo, please report them back to me by opening an issue on Github\. 80 | .SH Credits 81 | .P 82 | Thanks to: 83 | .RS 2 84 | .IP \(bu 2 85 | Pedro Faria, for his precious help in debugging Nodo on Linux\. 86 | 87 | .RE 88 | 89 | -------------------------------------------------------------------------------- /doc/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS tasks ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | online_id INTEGER DEFAULT 0, 4 | name TEXT, 5 | list_id TEXT, 6 | note TEXT DEFAULT '', 7 | date INTEGER DEFAULT 0, 8 | done_date INTEGER DEFAULT 0, 9 | done INTEGER DEFAULT 0, 10 | position INTEGER DEFAULT 0, 11 | important INTEGER DEFAULT 0, 12 | version INTEGER DEFAULT 0, 13 | deleted INTEGER DEFAULT 0 14 | ); 15 | 16 | CREATE TABLE IF NOT EXISTS lists ( 17 | id INTEGER PRIMARY KEY AUTOINCREMENT, 18 | online_id INTEGER DEFAULT 0, 19 | name TEXT, 20 | position INTEGER DEFAULT 0, 21 | version INTEGER DEFAULT 0, 22 | deleted INTEGER DEFAULT 0, 23 | inbox INTEGER DEFAULT 0, 24 | shared INTEGER DEFAULT 0 25 | ); 26 | 27 | INSERT INTO lists (name, inbox) VALUES ('inbox', 1); 28 | INSERT INTO tasks (name, list_id) VALUES ('Sample task', 1); 29 | INSERT INTO tasks (name, list_id) VALUES ('Another sample task.', 1); 30 | -------------------------------------------------------------------------------- /lib/aux.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // this sucks, path.normalize should accept the home alias... 3 | // https://github.com/joyent/node/issues/2857 4 | expand: function (path) { 5 | if (path.indexOf('~/') === 0) { 6 | return path.replace(/~\//, process.env.HOME + '/') 7 | } 8 | return path 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | var Insight = require('insight') 2 | var List = require('../lib/list') 3 | var Task = require('../lib/task') 4 | var pkg = require('../package.json') 5 | require('colors') 6 | 7 | var Command = function () { 8 | this.insight = new Insight({ 9 | trackingCode: 'UA-43711392-1', 10 | packageName: pkg.name, 11 | packageVersion: pkg.version 12 | }) 13 | 14 | this.list = new List() 15 | this.task = new Task() 16 | } 17 | 18 | Command.prototype.run = function (args) { 19 | if (this.insight.optOut === undefined) { 20 | this.insight.track('downloaded') 21 | return this.insight.askPermission() 22 | } 23 | 24 | args.splice(0, 2) // removing interpreter and file name 25 | 26 | var command = args.shift() 27 | var major = args.shift() 28 | var minor = args.length < 1 ? null : args.join(' ') 29 | 30 | if ( 31 | typeof command === 'undefined' || 32 | !command || 33 | command === '-h' || 34 | command === '--help' || 35 | command === 'help' 36 | ) { 37 | this.insight.track('help') 38 | this.showHelp() 39 | process.exit(0) 40 | } else if ( 41 | command === '-v' || 42 | command === '--version' || 43 | command === 'version' 44 | ) { 45 | this.insight.track('version') 46 | this.showVersion() 47 | process.exit(0) 48 | } else { 49 | this.delegate(command, major, minor) 50 | } 51 | } 52 | 53 | Command.prototype.delegate = function (command, major, minor) { 54 | switch (command) { 55 | case 'show': 56 | this.show(major, minor) 57 | break 58 | case 'add': 59 | this.add(major, minor) 60 | break 61 | case 'delete': 62 | this.remove(major, minor) 63 | break 64 | case 'done': 65 | this.done(major, minor) 66 | break 67 | case 'undo': 68 | this.undo(major, minor) 69 | break 70 | case 'star': 71 | this.star(major, minor) 72 | break 73 | case 'unstar': 74 | this.unstar(major, minor) 75 | break 76 | case 'due': 77 | this.due(major, minor) 78 | break 79 | case 'move': 80 | this.move(major, minor) 81 | break 82 | case 'restore': 83 | this.restore(major, minor) 84 | break 85 | default: 86 | return this.showHelp() 87 | } 88 | } 89 | 90 | Command.prototype.show = function (major, minor) { 91 | var self = this 92 | if (major === 'lists') { 93 | this.insight.track('show', major) 94 | this.list.getAll(function (err, result) { 95 | var i 96 | if (err) { 97 | console.log(err.toString().red) 98 | } else { 99 | for (i in result.rows) { 100 | if (Object.hasOwnProperty.call(result.rows, i)) { 101 | console.log( 102 | result.rows[i].name + ' (' + result.rows[i].totalTasks + ')' 103 | ) 104 | } 105 | } 106 | } 107 | }) 108 | } else if (major === 'done') { 109 | this.insight.track('show', major) 110 | this.task.getDone(minor, function (err, result) { 111 | var i 112 | 113 | if (err) { 114 | console.log(err.toString().red) 115 | } else if (result.rows.length < 1) { 116 | console.log('No tasks are done yet. Are you slacking?'.yellow) 117 | } else { 118 | for (i in result.rows) { 119 | if (Object.hasOwnProperty.call(result.rows, i)) { 120 | var doneString = 121 | '(' + result.rows[i].id + ') ' + result.rows[i].name + ' | ' 122 | doneString += result.rows[i].listName.bold + ' | ' 123 | doneString += self.parseDate(result.rows[i].done_date) 124 | console.log(doneString) 125 | } 126 | } 127 | } 128 | }) 129 | } else if (major === 'all') { 130 | this.insight.track('show', major) 131 | this.showOverview() 132 | } else if (major === 'task') { 133 | this.insight.track('show', major) 134 | this.task.get(minor, function (err, result) { 135 | if (err) { 136 | console.log(err.toString().red) 137 | } else if (!result.row) { 138 | console.log('Task #' + minor + ' does not exist.') 139 | } else { 140 | console.log('Task #' + result.row.id) 141 | console.log('List: ' + result.row.listName) 142 | console.log('Name: ' + result.row.name) 143 | console.log('Important: ' + (result.row.important ? 'yes' : 'no')) 144 | console.log('Notes: ' + (result.row.notes ? result.row.notes : 'none')) 145 | console.log( 146 | 'Due Date: ' + 147 | (result.row.date ? self.parseDate(result.row.date) : 'none') 148 | ) 149 | console.log('Done: ' + (result.row.done ? 'yes' : 'no')) 150 | console.log( 151 | 'Done Date: ' + 152 | (result.row.done_date 153 | ? self.parseDate(result.row.done_date) 154 | : 'none') 155 | ) 156 | console.log('Deleted: ' + (result.row.deleted ? 'yes' : 'no')) 157 | } 158 | }) 159 | } else if (major === 'deleted') { 160 | this.insight.track('show', major) 161 | this.task.getDeleted(minor, function (err, result) { 162 | if (err) { 163 | console.log(err.toString().red) 164 | } else if (!result.rows || result.rows.length < 1) { 165 | console.log('There are no deleted tasks.'.yellow) 166 | } else { 167 | for (var i in result.rows) { 168 | console.log('(' + result.rows[i].id + ') ' + result.rows[i].name) 169 | } 170 | } 171 | }) 172 | } else if (major) { 173 | // lets assume its a list name 174 | this.list.getByName(major, function (err, result) { 175 | if (err) { 176 | console.log(err.toString().red) 177 | } else if (result.rows.length < 1) { 178 | console.log('This list is empty or does not exist.'.yellow) 179 | } else { 180 | for (var j in result.rows) { 181 | console.log('(' + result.rows[j].id + ') ' + result.rows[j].name) 182 | } 183 | } 184 | }) 185 | } else { 186 | this.insight.track('overview') 187 | // alias for show all 188 | this.showOverview() 189 | } 190 | } 191 | 192 | Command.prototype.add = function (major, minor) { 193 | if (major === 'list') { 194 | this.insight.track('add', major) 195 | if (minor) { 196 | this.list.add(minor, function (err, result) { 197 | if (err) { 198 | console.log(err.toString().red) 199 | } else if (result.lastId) { 200 | console.log('Added list ' + minor) 201 | } else { 202 | console.log('Database did not return.'.red) // TODO change this 203 | } 204 | }) 205 | } else { 206 | console.log('List name cannot be empty.'.yellow) 207 | } 208 | } else { 209 | this.insight.track('add', major) 210 | var newTask = { list: major, name: minor } 211 | this.task.add(newTask, function (err, result) { 212 | if (err) { 213 | console.log(err.toString().red) 214 | } else if (result.lastId) { 215 | console.log('Task #' + result.lastId + ' added to list ' + major.blue) 216 | } else { 217 | console.log('Database did not return.'.red) // TODO change this 218 | } 219 | }) 220 | } 221 | } 222 | 223 | Command.prototype.done = function (major, minor) { 224 | this.insight.track('done') 225 | this.task.setDone(major, 1, function (err, result) { 226 | if (err) { 227 | console.log(err.toString().red) 228 | } else if (result.affected && result.affected > 0) { 229 | console.log('Task #' + major + ' marked as done.') 230 | } else { 231 | if (major) { 232 | console.log('Task #' + major + ' does not exist.') 233 | } else { 234 | console.log('No task specified.') 235 | } 236 | } 237 | }) 238 | } 239 | 240 | Command.prototype.undo = function (major, minor) { 241 | this.insight.track('undo') 242 | this.task.setDone(major, 0, function (err, result) { 243 | if (err) { 244 | console.log(err.toString().red) 245 | } else if (result.affected && result.affected > 0) { 246 | console.log('Task #' + major + ' marked as not done.') 247 | } else { 248 | if (major) { 249 | console.log('Task #' + major + ' does not exist.') 250 | } else { 251 | console.log('No task specified.') 252 | } 253 | } 254 | }) 255 | } 256 | 257 | Command.prototype.star = function (major, minor) { 258 | this.insight.track('star') 259 | this.task.setStar(major, 1, function (err, result) { 260 | if (err) { 261 | console.log(err.toString().red) 262 | } else if (result.affected && result.affected > 0) { 263 | console.log('Task #' + major + ' marked as important.') 264 | } else { 265 | if (major) { 266 | console.log('Task #' + major + ' does not exist.') 267 | } else { 268 | console.log('No task specified.') 269 | } 270 | } 271 | }) 272 | } 273 | 274 | Command.prototype.unstar = function (major, minor) { 275 | this.insight.track('unstar') 276 | this.task.setStar(major, 0, function (err, result) { 277 | if (err) { 278 | console.log(err.toString().red) 279 | } else if (result.affected && result.affected > 0) { 280 | console.log('Task #' + major + ' marked as not important.') 281 | } else { 282 | if (major) { 283 | console.log('Task #' + major + ' does not exist.') 284 | } else { 285 | console.log('No task specified.') 286 | } 287 | } 288 | }) 289 | } 290 | 291 | Command.prototype.due = function (major, minor) { 292 | this.insight.track('due') 293 | var dueDate = Date.parse(minor) / 1000 294 | this.task.setDue(major, dueDate, function (err, result) { 295 | if (err) { 296 | console.log(err.toString().red) 297 | } else if (result.affected && result.affected > 0) { 298 | console.log('Task #' + major + ' marked to be finished at ' + minor) 299 | } else { 300 | if (major) { 301 | console.log('Task #' + major + ' does not exist.') 302 | } else { 303 | console.log('No task specified.') 304 | } 305 | } 306 | }) 307 | } 308 | 309 | Command.prototype.remove = function (major, minor) { 310 | if (major === 'list') { 311 | this.insight.track('remove', major) 312 | this.list.remove(minor, function (err, result) { 313 | if (err) { 314 | console.log(err.toString().red) 315 | } else if (result.affected && result.affected > 0) { 316 | console.log('List ' + minor + ' deleted.') 317 | } else { 318 | console.log('List ' + minor + ' does not exist.') 319 | } 320 | }) 321 | } else { 322 | this.insight.track('remove', 'task') 323 | // allow alias for delete task # 324 | var taskId = major === 'task' ? minor : major 325 | this.task.remove(taskId, function (err, result) { 326 | if (err) { 327 | console.log(err.toString().red) 328 | } else if (result.affected && result.affected > 0) { 329 | console.log('Task #' + taskId + ' deleted.') 330 | } else { 331 | console.log('Task #' + taskId + ' does not exist.') 332 | } 333 | }) 334 | } 335 | } 336 | 337 | Command.prototype.restore = function (major, minor) { 338 | if (major === 'list') { 339 | this.insight.track('restore', major) 340 | this.list.restore(minor, function (err, result) { 341 | if (err) { 342 | console.log(err.toString().red) 343 | } else if (result.affected && result.affected > 0) { 344 | console.log('List ' + minor + ' restored.') 345 | } else { 346 | console.log('List ' + minor + ' does not exist.') 347 | } 348 | }) 349 | } else { 350 | this.insight.track('restore', 'task') 351 | // allow alias for restore task # 352 | var taskId = major === 'task' ? minor : major 353 | this.task.restore(taskId, function (err, result) { 354 | if (err) { 355 | console.log(err.toString().red) 356 | } else if (result.affected && result.affected > 0) { 357 | console.log('Task #' + taskId + ' restored.') 358 | } else { 359 | console.log('Task #' + taskId + ' does not exist.') 360 | } 361 | }) 362 | } 363 | } 364 | 365 | Command.prototype.move = function (major, minor) { 366 | this.insight.track('move') 367 | this.task.move(major, minor, function (err, result) { 368 | if (err) { 369 | console.log(err.toString().red) 370 | } else if (result.affected > 0) { 371 | console.log('Task #' + major + ' moved to list ' + minor) 372 | } else { 373 | console.log('Task or list unknown.'.red) 374 | } 375 | }) 376 | } 377 | 378 | Command.prototype.showOverview = function () { 379 | this.insight.track('overview') 380 | this.task.getToDo(function (err, result) { 381 | if (err) { 382 | console.log(err.toString().red) 383 | } else if (result.rows.length > 0) { 384 | var i = null 385 | var currentList = null 386 | 387 | for (i in result.rows) { 388 | if (Object.hasOwnProperty.call(result.rows, i)) { 389 | if (currentList !== result.rows[i].listName) { 390 | currentList = result.rows[i].listName 391 | console.log( 392 | result.rows[i].listName.bold.underline + ':'.bold.underline 393 | ) 394 | } 395 | } 396 | var taskStr = '(' + result.rows[i].id + ') ' + result.rows[i].name 397 | if (result.rows[i].important === 1) { 398 | console.log(taskStr.yellow + ' ★'.yellow) 399 | } else { 400 | console.log(taskStr) 401 | } 402 | } 403 | } else { 404 | console.log('Nothing to show.') 405 | } 406 | }) 407 | } 408 | 409 | /** 410 | * Transforms a unix timestamp (seconds) to a date string 411 | * @param {Integer} timestamp 412 | * @return {String} the timestamp converted to a date string 413 | */ 414 | Command.prototype.parseDate = function (timestamp) { 415 | var dateObj = new Date(timestamp * 1000) 416 | var dateStr = dateObj.getDate() + '/' + (dateObj.getMonth() + 1) 417 | dateStr += '/' + dateObj.getFullYear() 418 | return dateStr 419 | } 420 | 421 | Command.prototype.showHelp = function () { 422 | console.log('Usage: nodo [arguments]') 423 | console.log('') 424 | console.log(' Available actions and options:') 425 | console.log( 426 | ' nodo show Show all lists and tasks todo' 427 | ) 428 | console.log(' nodo show all Same as above') 429 | console.log( 430 | ' nodo show lists Show all lists and number of tasks in each one.' 431 | ) 432 | console.log(' nodo show Show content of list') 433 | console.log(' nodo show done Show all done tasks') 434 | console.log(' nodo show done Show the last n done tasks') 435 | console.log(' nodo show deleted Show all deleted tasks') 436 | console.log(' nodo show task Show detail of a task') 437 | console.log('') 438 | console.log(' nodo add list Add a new list') 439 | console.log(' nodo add Add a new task to list') 440 | console.log('') 441 | console.log(' nodo done Mark a task as done') 442 | console.log(' nodo undo Mark a task as not done') 443 | console.log('') 444 | console.log( 445 | ' nodo star Star a task (will display with different color)' 446 | ) 447 | console.log(' nodo unstar Unstar a task') 448 | console.log('') 449 | console.log(' nodo move Moves a task to a list') 450 | console.log('') 451 | console.log(' nodo delete list Delete list') 452 | console.log(' nodo delete task Delete task') 453 | console.log('') 454 | console.log(' nodo restore Restore task') 455 | console.log(' nodo restore task Restore task') 456 | console.log(' nodo restore list Restore list') 457 | } 458 | 459 | Command.prototype.showVersion = function () { 460 | console.log('') 461 | console.log('d8b db .d88b. d8888b. .d88b.') 462 | console.log('888o 88 .8P Y8. 88 `8D .8P Y8.') 463 | console.log('88V8o 88 88 88 88 88 88 88') 464 | console.log('88 V8o88 88 88 88 88 88 88') 465 | console.log("88 V888 `8b d8' 88 .8D `8b d8") 466 | console.log("VP V8P `Y88P' Y8888D' `Y88P\n") 467 | console.log('The Simple Command Line Task Manager') 468 | console.log('\nVersion '.magenta + pkg.version.green) 469 | console.log('') 470 | } 471 | 472 | module.exports = Command 473 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var aux = require('../lib/aux') 3 | var config = {} 4 | 5 | config.path = process.env.HOME + '/.nodorc' 6 | 7 | var fileExists = fs.existsSync(config.path, 'utf-8') 8 | 9 | config.file = fileExists 10 | ? JSON.parse(fs.readFileSync(config.path, 'utf-8')) 11 | : false 12 | 13 | if (config.file) { 14 | config.file.database.location = aux.expand(config.file.database.location) 15 | } 16 | 17 | module.exports = config 18 | -------------------------------------------------------------------------------- /lib/database.js: -------------------------------------------------------------------------------- 1 | var config = require('../lib/config') 2 | var sqlite = require('sqlite3').verbose() 3 | var fs = require('fs') 4 | var database = false 5 | var fileExists = false 6 | 7 | if (config.file) { 8 | // this sucks, path.normalize should accept the home alias... 9 | var databaseLocation = config.file.database.location.replace( 10 | /~\//g, 11 | process.env.HOME + '/' 12 | ) 13 | fileExists = fs.existsSync(databaseLocation) 14 | } 15 | 16 | if (fileExists) { 17 | database = new sqlite.Database(databaseLocation) 18 | } 19 | 20 | module.exports = database 21 | -------------------------------------------------------------------------------- /lib/init.js: -------------------------------------------------------------------------------- 1 | var config = require('../lib/config') 2 | require('colors') 3 | 4 | // check for config file 5 | if (config.file === false) { 6 | console.log('Error: config file not found.'.red) 7 | process.exit(-1) 8 | } 9 | 10 | var database = require('../lib/database') 11 | 12 | if (database === false) { 13 | console.log( 14 | 'Error: database file not found: '.red + config.file.database.location 15 | ) 16 | process.exit(-1) 17 | } 18 | 19 | var Command = require('../lib/command') 20 | var cmd = new Command() 21 | cmd.run(process.argv) 22 | -------------------------------------------------------------------------------- /lib/list.js: -------------------------------------------------------------------------------- 1 | var database = require('../lib/database') 2 | 3 | var List = function () {} 4 | 5 | /** 6 | * Get all lists that are not deleted along with the number of not done tasks 7 | * for each one 8 | * @param {Function} callback 9 | * @api public 10 | */ 11 | List.prototype.getAll = function (callback) { 12 | var query = 'SELECT lists.name, COUNT(tasks.id) as totalTasks FROM lists ' 13 | query += 'LEFT OUTER JOIN tasks ON tasks.list_id=lists.id ' 14 | query += 'AND tasks.deleted=0 AND tasks.done=0 WHERE lists.deleted=0 ' 15 | query += 'GROUP BY lists.name' 16 | database.all(query, function (err, rows) { 17 | if (err) { 18 | return callback(err) 19 | } else { 20 | var result = {} 21 | result.rows = rows 22 | return callback(null, result) 23 | } 24 | }) 25 | } 26 | 27 | /** 28 | * Get all deleted lists 29 | * @param {Function} callback 30 | * @api public 31 | */ 32 | List.prototype.getDeleted = function (callback) { 33 | var query = 'SELECT * FROM lists WHERE deleted=1' 34 | database.all(query, function (err, rows) { 35 | if (err) { 36 | return callback(err) 37 | } else { 38 | var result = {} 39 | result.rows = rows 40 | return callback(null, result) 41 | } 42 | }) 43 | } 44 | 45 | /** 46 | * Get list with the given id 47 | * @param {Integer} listId the id of the list to retrieve 48 | * @param {Function} callback 49 | * @api public 50 | */ 51 | List.prototype.get = function (listId, callback) { 52 | var query = 'SELECT * FROM lists WHERE id=?' 53 | database.get(query, listId, function (err, row) { 54 | if (err) { 55 | return callback(err) 56 | } else { 57 | var result = {} 58 | result.row = row 59 | return callback(null, result) 60 | } 61 | }) 62 | } 63 | 64 | /** 65 | * Adds a new list 66 | * @param {String} listName the name of the new list 67 | * @param {Function} callback 68 | * @api public 69 | */ 70 | List.prototype.add = function (listName, callback) { 71 | var query = 'INSERT INTO lists (name) values (?)' 72 | database.run(query, listName, function (err) { 73 | if (err) { 74 | return callback(err) 75 | } else { 76 | var result = {} 77 | result.lastId = this.lastID 78 | return callback(null, result) 79 | } 80 | }) 81 | } 82 | 83 | /** 84 | * Get the tasks of a list by given its name 85 | * @param {String} name the name of the list to retrieve 86 | * @param {Function} callback 87 | * @api public 88 | */ 89 | List.prototype.getByName = function (listName, callback) { 90 | var query = 91 | 'SELECT tasks.id, tasks.name FROM tasks, lists WHERE lists.name=? ' 92 | query += 'AND lists.deleted=0 AND tasks.done=0 AND lists.id=tasks.list_id' 93 | database.all(query, listName, function (err, rows) { 94 | if (err) { 95 | return callback(err) 96 | } else { 97 | var result = {} 98 | result.rows = rows 99 | return callback(null, result) 100 | } 101 | }) 102 | } 103 | 104 | /** 105 | * Removes list with given name 106 | * @param {String} listName the name of the list to remove 107 | * @param {Function} callback 108 | * @api public 109 | */ 110 | List.prototype.remove = function (listName, callback) { 111 | var query = 'UPDATE lists SET deleted=1 WHERE name=?' 112 | database.run(query, listName, function (err, rows) { 113 | if (err) { 114 | return callback(err) 115 | } else { 116 | var result = {} 117 | result.affected = this.changes 118 | return callback(null, result) 119 | } 120 | }) 121 | } 122 | 123 | /** 124 | * Restores a deleted list 125 | * @param {String} listName the name of the list to restore 126 | * @param {Function} callback 127 | * @api public 128 | */ 129 | List.prototype.restore = function (listName, callback) { 130 | var query = 'UPDATE lists SET deleted=0 WHERE name=?' 131 | database.run(query, listName, function (err, rows) { 132 | if (err) { 133 | return callback(err) 134 | } else { 135 | var result = {} 136 | result.rows = rows 137 | return callback(null, result) 138 | } 139 | }) 140 | } 141 | 142 | module.exports = List 143 | -------------------------------------------------------------------------------- /lib/task.js: -------------------------------------------------------------------------------- 1 | var database = require('../lib/database') 2 | 3 | var Task = function () {} 4 | 5 | /** 6 | * Get all the tasks with their respective list names 7 | * @param {Function} callback 8 | * @api public 9 | */ 10 | Task.prototype.getToDo = function (callback) { 11 | var query = 12 | 'SELECT lists.name as listName, tasks.name, tasks.id, tasks.important ' 13 | query += 'FROM tasks, lists ' 14 | query += 'WHERE tasks.done=0 AND tasks.deleted=0 AND tasks.list_id=lists.id ' 15 | query += 'ORDER BY lists.id' 16 | database.all(query, function (err, rows) { 17 | if (err) { 18 | return callback(err) 19 | } else { 20 | var result = {} 21 | result.rows = rows 22 | return callback(null, result) 23 | } 24 | }) 25 | } 26 | 27 | /** 28 | * Get all the done not deleted tasks with their list names 29 | * @param {Integer} limit the number of most recent done tasks 30 | * @param {Function} callback 31 | * @api public 32 | */ 33 | Task.prototype.getDone = function (limit, callback) { 34 | var query = 35 | 'SELECT lists.name as listName, tasks.* FROM tasks, lists WHERE done=1 ' 36 | query += 37 | 'AND tasks.deleted=0 AND tasks.list_id=lists.id ORDER BY tasks.done_date DESC' 38 | if (limit) { 39 | query += ' LIMIT ' + limit 40 | } 41 | database.all(query, function (err, rows) { 42 | if (err) { 43 | return callback(err) 44 | } else { 45 | var result = {} 46 | result.rows = rows 47 | return callback(null, result) 48 | } 49 | }) 50 | } 51 | 52 | /** 53 | * Get all the deleted tasks 54 | * @param {Function} callback 55 | * @api public 56 | */ 57 | Task.prototype.getDeleted = function (limit, callback) { 58 | var query = 'SELECT * FROM tasks WHERE deleted=1' 59 | if (limit) { 60 | query += ' LIMIT ' + limit 61 | } 62 | database.all(query, function (err, rows) { 63 | if (err) { 64 | return callback(err) 65 | } else { 66 | var result = {} 67 | result.rows = rows 68 | return callback(null, result) 69 | } 70 | }) 71 | } 72 | 73 | /** 74 | * Get a task and its list name with the given id 75 | * @param {Integer} taskId the id of the task to retrieve 76 | * @api public 77 | */ 78 | Task.prototype.get = function (taskId, callback) { 79 | var query = 'SELECT lists.name as listName, tasks.* FROM tasks, lists ' 80 | query += 'WHERE tasks.id=? AND lists.id=tasks.list_id' 81 | database.get(query, taskId, function (err, row) { 82 | if (err) { 83 | return callback(err) 84 | } else { 85 | var result = {} 86 | result.row = row 87 | return callback(null, result) 88 | } 89 | }) 90 | } 91 | 92 | /** 93 | * Adds a new task 94 | * @param {Object} task the new task to be added 95 | * @param {Function} callback 96 | * @api public 97 | */ 98 | Task.prototype.add = function (task, callback) { 99 | database.get('SELECT id FROM lists WHERE name=?', task.list, function ( 100 | err, 101 | row 102 | ) { 103 | if (err) { 104 | return callback(err) 105 | } 106 | if (!row) { 107 | return callback('Unknown list.') 108 | } 109 | var query = 'INSERT INTO tasks (name, list_id, date) values(?, ?, ?)' 110 | database.run(query, [task.name, row.id, task.dueDate], function (err) { 111 | if (err) { 112 | return callback(err) 113 | } else { 114 | var result = {} 115 | result.lastId = this.lastID 116 | return callback(null, result) 117 | } 118 | }) 119 | return true // understand why? 120 | }) 121 | } 122 | 123 | /** 124 | * Marks a task with the given id as deleted 125 | * @param {Integer} taskId the id of the task to mark as deleted 126 | * @param {Function} callback 127 | * @api public 128 | */ 129 | Task.prototype.remove = function (taskId, callback) { 130 | var query = 'UPDATE tasks SET deleted=1 WHERE id=?' 131 | database.run(query, taskId, function (err) { 132 | if (err) { 133 | return callback(err) 134 | } else { 135 | var result = {} 136 | result.affected = this.changes 137 | return callback(null, result) 138 | } 139 | }) 140 | } 141 | 142 | /** 143 | * Removes deleted status from task 144 | * @param {Integer} taskId the id of the task restore 145 | * @param {Function} callback 146 | * @api public 147 | */ 148 | Task.prototype.restore = function (taskId, callback) { 149 | var query = 'UPDATE tasks SET deleted=0 WHERE id=?' 150 | database.run(query, taskId, function (err) { 151 | if (err) { 152 | return callback(err) 153 | } else { 154 | var result = {} 155 | result.affected = this.changes 156 | return callback(null, result) 157 | } 158 | }) 159 | } 160 | 161 | /** 162 | * Moves a task from its current list to another list 163 | * @param {Integer} taskId the id of the task to move 164 | * @param {String} listName the name of the list to move that task to 165 | * @param {Function} callback 166 | * @api public 167 | */ 168 | Task.prototype.move = function (taskId, listName, callback) { 169 | database.get('SELECT id FROM lists WHERE name=?', listName, function ( 170 | err, 171 | row 172 | ) { 173 | if (err) { 174 | return callback(err) 175 | } 176 | if (!row) { 177 | return callback('Unknown list') 178 | } 179 | var query = 'UPDATE tasks SET list_id=? WHERE id=?' 180 | database.run(query, [row.id, taskId], function (err) { 181 | if (err) { 182 | return callback(err) 183 | } else { 184 | var result = {} 185 | result.affected = this.changes 186 | return callback(null, result) 187 | } 188 | }) 189 | }) 190 | } 191 | 192 | /** 193 | * Mark a task with the given id as done 194 | * @param {Integer} taskId the id of the task to mark as done 195 | * @param {Integer} doneValue 1 if task is done 0 otherwise 196 | * @param {Function} callback 197 | * @api public 198 | */ 199 | Task.prototype.setDone = function (taskId, doneValue, callback) { 200 | // convert to unix timestamp 201 | var doneDate = doneValue === 1 ? Math.round(Date.now() / 1000) : 0 202 | var query = 'UPDATE tasks SET done=?, done_date=? WHERE id=?' 203 | database.run(query, [doneValue, doneDate, taskId], function (err) { 204 | if (err) { 205 | return callback(err) 206 | } else { 207 | var result = {} 208 | result.affected = this.changes 209 | return callback(null, result) 210 | } 211 | }) 212 | } 213 | 214 | /** 215 | * Mark a task with the given id as starred 216 | * @param {Integer} taskId the id of the task to mark as starred 217 | * @param {Integer} starValue 1 if task is starred 0 otherwise 218 | * @param {Function} callback 219 | * @api public 220 | */ 221 | Task.prototype.setStar = function (taskId, starValue, callback) { 222 | var query = 'UPDATE tasks SET important=? WHERE id=?' 223 | database.run(query, [starValue, taskId], function (err) { 224 | if (err) { 225 | return callback(err) 226 | } else { 227 | var result = {} 228 | result.affected = this.changes 229 | return callback(null, result) 230 | } 231 | }) 232 | } 233 | 234 | /** 235 | * Sets a due date to given task 236 | * @param {Integer} taskId the id of the task to set a due date 237 | * @param {Integer} dateValue the date to set due 238 | * @param {Function} callback 239 | * @api public 240 | */ 241 | Task.prototype.setDue = function (taskId, dateValue, callback) { 242 | var query = 'UPDATE tasks SET date=? WHERE id=?' 243 | database.run(query, [dateValue, taskId], function (err) { 244 | if (err) { 245 | return callback(err) 246 | } else { 247 | var result = {} 248 | result.affected = this.changes 249 | return callback(null, result) 250 | } 251 | }) 252 | } 253 | 254 | module.exports = Task 255 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodo", 3 | "description": "Command line todo app.", 4 | "author": "Rogério Vicente ", 5 | "keywords": [ 6 | "nodo", 7 | "todo", 8 | "wunderlist", 9 | "cli" 10 | ], 11 | "maintainers": [ 12 | { 13 | "name": "Rogério Vicente", 14 | "web": "http://rogeriopvl.com" 15 | } 16 | ], 17 | "version": "1.0.1", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/rogeriopvl/nodo" 21 | }, 22 | "devDependencies": { 23 | "marked-man": "^0.2.1", 24 | "nodeunit": "~0.8.1", 25 | "standard": "^10.0.2", 26 | "tape": "^4.6.3" 27 | }, 28 | "dependencies": { 29 | "colors": "^1.1.2", 30 | "fs-extra": "^3.0.1", 31 | "insight": "^0.8.4", 32 | "sqlite3": "^3.1.8" 33 | }, 34 | "engines": { 35 | "node": ">=4.0.0" 36 | }, 37 | "directories": { 38 | "lib": "./lib", 39 | "samples": "./samples" 40 | }, 41 | "bin": { 42 | "nodo": "./bin/nodo", 43 | "nodo-install": "./bin/nodo-install" 44 | }, 45 | "man": [ 46 | "./doc/nodo.1" 47 | ], 48 | "scripts": { 49 | "install": "./bin/nodo-install", 50 | "test": "tape test/*.js", 51 | "man": "marked-man README.md > doc/nodo.1" 52 | }, 53 | "licenses": [ 54 | { 55 | "type": "MIT", 56 | "url": "http://github.com/rogeriopvl/nodo/LICENSE" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /samples/nodo.db_sample: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rogeriopvl/nodo/027e75885d9103810596e3041c83c357560d3037/samples/nodo.db_sample -------------------------------------------------------------------------------- /samples/nodorc_sample: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "location": "~/.nodo.db" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/command.test.js: -------------------------------------------------------------------------------- 1 | var Command = require('../lib/command') 2 | 3 | module.exports = { 4 | /*testShowHelpWithNoParams: function(test){ 5 | var cmd = new Command(); 6 | var fakeArgs = ['node', 'foobar']; 7 | cmd.run(fakeArgs); 8 | test.ok(true); // TODO 9 | test.done(); 10 | }*/ 11 | } 12 | -------------------------------------------------------------------------------- /test/task.test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | // var database = require('../lib/database'); 3 | // var Task = require('../lib/task'); 4 | 5 | test('it works', function(t) { 6 | t.ok(true) 7 | t.end() 8 | }) 9 | --------------------------------------------------------------------------------