Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@jarmo
Forked from mattsears/README.md
Created October 7, 2012 10:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jarmo/3847813 to your computer and use it in GitHub Desktop.
Save jarmo/3847813 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
# Prints all the used tags
#
# Examples:
# @todo.tags
#
# Returns nothing
def tags
puts to_hash.keys.sort
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['TODOS_PATH'] || 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'
IO.popen("#{ENV["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
when 'tags'
puts "All tags:"
Todo.new.tags
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 " tags Lists all the used tags"
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')
class Todo
# make sure that no-ones .todos is overwritten...
def file
File.expand_path(".todos-test")
end
end
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
File.open(File.expand_path(".todos-test", File.dirname(__FILE__)), "w") do |f|
f.write "---
:@next:
- Take the dog for a walk
:@work:
- Pay lease bill
:@home:
- Buy Duck Typing from RubyRags
- Buy Ruby Nerd from RubyRags
"
end
@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
after do
test_file = File.expand_path(".todos-test", File.dirname(__FILE__))
File.delete(test_file) if File.exists? test_file
end
it "finds the file path of the todo list" do
@todo.file.must_equal File.expand_path('.todos-test')
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment