Skip to content

Instantly share code, notes, and snippets.

@mattsears
Created October 3, 2011 13:16
Show Gist options
  • Save mattsears/1259080 to your computer and use it in GitHub Desktop.
Save mattsears/1259080 to your computer and use it in GitHub Desktop.
Todo.rb: A simple command-line TODO manager written in Ruby

Todo.rb

Todo.rb is a simple command-line tool for managing todos. It's minimal, straightforward, and you can use it with your favorite text editor.

Getting Started

Todo.rb doesn't require any third-party gems so you can copy the file anywhere and use it as long as it's executable:

$ chmod +x todo.rb

You may also create an alias to save keystrokes

alias todo='~/todo.rb'

Todo.rb can work with multiple todo files too. You can maintain one todo list (default: '~/.todos') for project-specific todo files.

Create a todo

We don't have to use quotes :-)

$ todo add Check out rubyrags.com
Add: (1) Check out rubyrags.com

# Create a todo with context
$ todo add Buy Duck Typing shirt from rubyrags.com @work
Add: (2) Buy Duck Typing shirt from rubyrags.com @work

$ todo add Buy Ruby Nerd shirt from rubyrags.com
Add: (3) Buy Ruby Nerd shirt from rubyrags.com

List todos

Prints the todos is a nice, tabbed format.

$ todo list
1. Check out rubyrags.com
2: Buy Ruby Nerd shirt from rubyrags.com       @work
3: Buy Duck Typing shirt from rubyrags.com     @work

$ todo list @work
1: Buy Ruby Nerd shirt from rubyrags.com       @work
2: Buy Duck Typing shirt from rubyrags.com     @work

Deleting todos

Use the todo number to delete it. del also works.

$ todo delete 1
Deleted: (1) Check out rubyrags.com

# Todo delete all the todos:
$ todo clear
All 3 todos cleared!

Completing todos

Use the todo number to complete the todo. This will simple archive the todo

$ todo done 2
Done: (2) Buy Duck Typing shirt from rubyrags.com    @work

Prioritizing todos

To bump a todo higher on the list:

$todo bump 2
Bump: (2) Buy Duck Typing shirt from rubyrags.com    @work

Help

Help is just a command away.

$ todo help

Manually Edit

If you want to edit the underlying todo file directly, make sure your $EDITOR environment variable is set, and run:

$ todo edit

Then you can see your todo list in a beautifully formated yaml file!

Ohai, Command Line!

Since it's the command line we have all the goodies available to use

$ todo list | grep Nerd
2: Buy Ruby Nerd shirt from rubyrags.com   @work
#!/usr/bin/env ruby
require 'yaml'
# Represents a single todo item. An Item contains just a text and
# a context.
#
class Item
attr_accessor :context, :text
# Creates a new Item instance in-memory.
#
# value - The text of the Todo. Context is extracted if exists.
#
# Returns the unpersisted Item instance.
def initialize(value)
@context = value.scan(/@[A-Z0-9.-]+/i).last || '@next'
@text = value.gsub(context, '').strip
end
# Overide: Quick and simple way to print Items
#
# Returns String for this Item
def to_s
"#{@text}: #{@context}"
end
end
# The Todo contains many Items. They exist as buckets in which to categorize
# individual Items. The relationship is maintained in a simple array.
#
class Todo
# Creates a new Todo instance in-memory.
#
# Returns the persisted Todo instance.
def initialize(options = {})
@options, @items = options, []
bootstrap
load_items
end
# The main todos in the user's home directory
FILE = File.expand_path('.todos')
# Allow to items to be accessible from the outside
attr_accessor :items
# Creates a new todo
#
# Example:
# @todo.add('lorem epsim etc @work')
#
# Returns the add todo Item
def add(todo)
@items << Item.new(todo)
save
@items.last
end
# Removes the todo
#
# Example:
# @todo.delete(1)
#
# Returns the deleted todo Item
def delete(index)
todo = @items.delete_at(index.to_i-1)
save
todo
end
# Marks a todo as done
#
# Example:
# @todo.done(1)
#
# Returns the done todo Item
def done(index)
item = @items[index.to_i-1]
item.context = '@done'
save
item
end
# Prints all the active todos in a nice neat format
#
# Examples:
# @todo.list @work
#
# Returns nothing
def list
longest = @items.map(&:text).max_by(&:length) || 0
@items.each_with_index do |todo, index|
printf "%s: %-#{longest.size+5}s %s\n", index+1, todo.text, todo.context
end
end
# Moves a todo up or down in priority
#
# Example:
# @todo.bump(2, +1)
#
def bump(index, position = 1)
@items.insert(position-1, @items.delete_at(index.to_i-1))
save
@items[position.to_i-1]
end
# Accessor for the todo list file
#
# Returns String file path
def file
@file ||= File.exist?(FILE) ? FILE : "#{ENV['HOME']}/.todos"
end
# Formats the current set of todos
#
# Returns a lovely hash
def to_hash
@items.group_by(&:context).inject({}) do |h,(k,v)|
h[k.to_sym] = v.map(&:text); h
end
end
# Loads the yaml todos file and creates a hash
#
# Returns the items loaded from the file
def load_items
YAML.load_file(file).each do |key, texts|
texts.each do |text|
if key.to_s == @options[:filter] || @options[:filter].nil?
@items << Item.new("#{text} #{key}") if key.to_s != '@done'
end
end
end
@items
end
# Implodes all the todo items save an empty file
#
# Returns nothing
def clear!
@items.clear
save
end
private
# Saves the current list of todos to disk
#
# Returns nothing
def save
File.open(file, "w") {|f| f.write(to_hash.to_yaml) }
end
# Creates a new todo file if none is present
#
# Returns nothing
def bootstrap
return if File.exist?(file)
save
end
end
if __FILE__ == $0
case ARGV[0]
when 'list','ls'
Todo.new(:filter => ARGV[1]).list
when 'add','a'
puts "Added: #{Todo.new.add(ARGV[1..-1].join(' '))}"
when 'delete', 'del', 'd'
puts "Deleted: #{Todo.new.delete(ARGV[1])}"
when 'done'
puts "Done: #{Todo.new.done(ARGV[1])}"
when 'edit'
system("`echo $EDITOR` #{Todo.new.file} &")
when 'clear'
puts "All #{Todo.new.clear!} todos cleared! #{Todo.new.clear!}"
when 'bump'
puts "Bump: #{Todo.new.bump(ARGV[1])}"
Todo.new.list
else
puts "\nUsage: todo [options] COMMAND\n\n"
puts "Commands:"
puts " add TODO Adds a todo"
puts " delete NUM Removes a todo"
puts " done NUM Completes a todo"
puts " list [CONTEXT] Lists all active todos"
puts " bump NUM Bumps priority of a todo"
puts " edit Opens todo file"
end
end
require 'minitest/autorun'
require 'minitest/pride'
require 'awesome_print'
require File.join(File.dirname(__FILE__), 'todo.rb')
describe Item do
before do
@item = Item.new('New todo item @home')
end
it 'assigns a text value for the todo' do
@item.text.must_equal 'New todo item'
end
it 'assigns a context from the todo value' do
@item.context.must_equal '@home'
end
end
describe Todo do
before do
@todo = Todo.new
@todo.clear!
@todo.add 'Take the dog for a walk'
@todo.add 'Pay lease bill @work'
@todo.add 'Buy Duck Typing from RubyRags @home'
@todo.add 'Buy Ruby Nerd from RubyRags @home'
end
it "finds the file path of the todo list" do
@todo.file.must_equal File.expand_path('.todos')
end
it "adds the todo to the stack" do
@todo.items.size.must_equal 4
end
it "creates a hash of attributes from the todo items" do
@todo.to_hash.must_equal({
:@next => ["Take the dog for a walk"],
:@work => ["Pay lease bill"],
:@home => ["Buy Duck Typing from RubyRags", "Buy Ruby Nerd from RubyRags"]
})
end
it 'deletes a todo' do
@todo.delete(2).text.must_equal "Pay lease bill"
@todo.items.size.must_equal 3
end
it 'completes a todo' do
@todo.done(2).context.must_equal "@done"
end
end
---
:@next:
- Take the dog for a walk
:@work:
- Pay lease bill
:@home:
- Buy Duck Typing from RubyRags
- Buy Ruby Nerd from RubyRags
@robertodecurnex
Copy link

The tags system is great. Add remote storage could make it the perfect solution.

@jarmo
Copy link

jarmo commented Oct 7, 2012

@robertodecurnex why not just use Dropbox for that?

@hamidreza-s
Copy link

I am using it. Thanks.

@daturkel
Copy link

You may be interested in my very minor fork (view the revision here).

I only changed two things:

  1. The plus character (+) is now valid in contexts.
  2. If you have over 9 items in your list, when you run the list command it'll pad the indices to align everything prettily.

Hope you enjoy it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment