Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Todo.rb: A simple command-line TODO manager written in Ruby


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
Add: (1) Check out

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

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

List todos

Prints the todos is a nice, tabbed format.

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

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

Deleting todos

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

$ todo delete 1
Deleted: (1) Check out

# 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    @work

Prioritizing todos

To bump a todo higher on the list:

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


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   @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
# Overide: Quick and simple way to print Items
# Returns String for this Item
def to_s
"#{@text}: #{@context}"
# 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, []
# 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 <<
# Removes the todo
# Example:
# @todo.delete(1)
# Returns the deleted todo Item
def delete(index)
todo = @items.delete_at(index.to_i-1)
# 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'
# Prints all the active todos in a nice neat format
# Examples:
# @todo.list @work
# Returns nothing
def list
longest = || 0
@items.each_with_index do |todo, index|
printf "%s: %-#{longest.size+5}s %s\n", index+1, todo.text, todo.context
# 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))
# Accessor for the todo list file
# Returns String file path
def file
@file ||= File.exist?(FILE) ? FILE : "#{ENV['HOME']}/.todos"
# 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] =; h
# 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 <<"#{text} #{key}") if key.to_s != '@done'
# Implodes all the todo items save an empty file
# Returns nothing
def clear!
# Saves the current list of todos to disk
# Returns nothing
def save, "w") {|f| f.write(to_hash.to_yaml) }
# Creates a new todo file if none is present
# Returns nothing
def bootstrap
return if File.exist?(file)
if __FILE__ == $0
case ARGV[0]
when 'list','ls' => ARGV[1]).list
when 'add','a'
puts "Added: #{[1..-1].join(' '))}"
when 'delete', 'del', 'd'
puts "Deleted: #{[1])}"
when 'done'
puts "Done: #{[1])}"
when 'edit'
system("`echo $EDITOR` #{} &")
when 'clear'
puts "All #{!} todos cleared! #{!}"
when 'bump'
puts "Bump: #{[1])}"
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"
require 'minitest/autorun'
require 'minitest/pride'
require 'awesome_print'
require File.join(File.dirname(__FILE__), 'todo.rb')
describe Item do
before do
@item ='New todo item @home')
it 'assigns a text value for the todo' do
@item.text.must_equal 'New todo item'
it 'assigns a context from the todo value' do
@item.context.must_equal '@home'
describe Todo do
before do
@todo =
@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'
it "finds the file path of the todo list" do
@todo.file.must_equal File.expand_path('.todos')
it "adds the todo to the stack" do
@todo.items.size.must_equal 4
it "creates a hash of attributes from the todo items" do
:@next => ["Take the dog for a walk"],
:@work => ["Pay lease bill"],
:@home => ["Buy Duck Typing from RubyRags", "Buy Ruby Nerd from RubyRags"]
it 'deletes a todo' do
@todo.delete(2).text.must_equal "Pay lease bill"
@todo.items.size.must_equal 3
it 'completes a todo' do
@todo.done(2).context.must_equal "@done"
- Take the dog for a walk
- Pay lease bill
- Buy Duck Typing from RubyRags
- Buy Ruby Nerd from RubyRags

This comment has been minimized.

Copy link

@robertodecurnex robertodecurnex commented Oct 3, 2011

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


This comment has been minimized.

Copy link

@jarmo jarmo commented Oct 7, 2012

@robertodecurnex why not just use Dropbox for that?


This comment has been minimized.

Copy link

@hamidreza-s hamidreza-s commented Oct 18, 2013

I am using it. Thanks.


This comment has been minimized.

Copy link

@daturkel daturkel commented Mar 17, 2014

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