Command line TODO lists

How many times did you try to use apps to write down your TODOs just to find that they are either slow or too complex for this simple task ? This week, I challenge you to write a simple but useful command line TODO app, implemented in Ruby and small enough to fit in a Gist.

As minimum requirements it should allow users to create, destroy and list their TODOs. You can add a ton of features to improve the app – seriously, I’d love to see what you can come up with – but remember, usability is the main objective.

Please put your entry in a Gist with a README including a brief description of the features and some examples of how it can be used. Of course, don’t be afraid to explain why your solution should win.

You have a whole week to work on your solution, so what are you waiting for? Have fun!

Prize

This week’s winner gets to choose between $25 of 6sync hosting credit or one month of free Github Micro!

This contest is finished

Congratulations to this week's winners! The entries and the contestant names are shown below.

  • README.md

    Scatterbrain

    Scatterbrain is a simple todo app. It uses gist as your todo list, so you can easily use it from any computer.

    How to use

    ~/projects/scatterbrain % ruby scatterbrain.rb add "write todo app"
    Put your github username and password:
    Username: nashby
    Password: password
    TODO: 
    1. write todo app
    
    ~/projects/scatterbrain % ruby scatterbrain.rb list
    TODO: 
    1. write todo app
    
    ~/projects/scatterbrain % ruby scatterbrain.rb add "win contest"
    TODO: 
    1. write todo app
    2. win contest
    
    ~/projects/scatterbrain % ruby scatterbrain.rb delete 1     
    TODO: 
    1. win contest
    

    When you run it first time it will ask your github's username and password. Then Scatterbrain will create todo gist. And now if you run it from another computer and put your github's username and password, Scatterbrain will find your todo gist with all your tasks.

    scatterbrain.rb
    require 'httparty'
    require 'json'
    
    module Scatterbrain
      class Config
        def initialize(options = {})
          @path = options[:path] || "#{Dir.home}/.scatterbrain"
        end
    
        def load
          if File.file?(@path)
            YAML.load(File.open(@path).read)
          else
            config = {}
            puts "Put your github username and password:"
            print "Username: "
            config[:username] = STDIN.gets.strip
            print "Password: "
            config[:password] = STDIN.gets.strip
            gist = Gist.new(config)
            config[:id] = gist.find
            config[:id] ||= gist.create
    
            create(config)
            config
          end
        end
    
        def create(auth)
          File.open(@path, 'w') { |f| f.write(auth.to_yaml) }
        end
      end
    
      class Gist
        include HTTParty
        base_uri 'https://api.github.com'
    
        def initialize(options)
          @id   = options.delete(:id)
          @auth = options
        end
    
        def content
          YAML.load(self.class.get("/gists/#{@id}")['files']['scatterbrain']['content'])
        end
    
        def create
          content = []
          options = {:body => {:description => 'scatterbrain todo',
                               :public => false,
                               :files => {:scatterbrain => {:content => content.to_yaml}}}.to_json,
                     :basic_auth => @auth}
          @id = self.class.post('/gists', options)['id']
        end
    
        def edit(text, index = nil)
          cont = content
          if index
            cont.delete_at(index - 1)
          else
            cont = (content << text)
          end
          options = {:body => {:description => 'scatterbrain todo',
                               :files => {:scatterbrain => {:content => cont.to_yaml}}}.to_json,
                     :basic_auth => @auth}
          self.class.post("/gists/#{@id}", options)
        end
    
        def delete
          self.class.delete("/gists/#{@id}")
        end
    
        def find
          gist = self.class.get('/gists', {:basic_auth => @auth}).detect { |gist| gist['description'] == 'scatterbrain todo' }
          gist['id'] if gist
        end
      end
    
      class Base
        def initialize
          @gist = Gist.new(Config.new.load)
        end
    
        def add(task)
          @gist.edit(task)
          list
        end
    
        def delete(index)
          @gist.edit(nil, index.to_i)
          list
        end
    
        def list
          puts "TODO: "
          @gist.content.each_with_index { |task, i| puts "#{i + 1}. #{task}" }
        end
      end
    end
    
    sbrain = Scatterbrain::Base.new
    sbrain.send(*ARGV)
    
    View full entry
    Finished in 1st place with a final score of 3.7/5. (View the Gist)
  • README.md

    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
    
    todo.rb
    #!/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
    
    todo_test.rb
    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
    
    View full entry
    Finished in 2nd place with a final score of 3.4/5. (View the Gist)
  • 5-bash_completion.bash
    _todo()
      { 
        local cur words 
        COMPREPLY=()
        words="${COMP_WORDS[@]:1}"
        cur="${COMP_WORDS[COMP_CWORD]}"
     
        local nexts=$( cat ~/.todo | grep -F "${words}" | grep '^\+' | cut -b34- | while read LINE; do elems=( ${LINE} ); echo "${elems[COMP_CWORD-1]}" ; done )
        COMPREPLY=( $(compgen -W "${nexts}" -- "${cur}" ) )
        return 0
      }
      
    complete -F _todo ++
    complete -F _todo xx
    
    # saved in /etc/bash_completion.d/todo
    # make sure you source it to get the completion working
    
    4-sub.bash
    #! /usr/bin/env bash
    echo "-" \[$(date)\] $@ >> ~/.todo
    
    # saved as /usr/bin/xx, chmod +x
    
    7-change.bash
    #! /usr/bin/env bash
    
    # Delete + Add editing - a bit nicer than mid-stream editing
    # Searches ~/.todo for the last '+' record matching $1, 
    # Adds a new '-' record for the task,
    # Then adds a new '+' record changing $1 to $2, or appending $2 if -a option passed.
    #
    #  Examples:
    #    ++ freelance general, take photo with @thomas
    #       >> + [Tue Sep 20 12:10:50 EDT 2011] freelance general, take photo with @thomas
    #    == "photo with @thomas" "photo with @elisa"
    #       >> - [Tue Sep 20 12:10:55 EDT 2011] freelance general, take photo with @thomas
    #       >> + [Tue Sep 20 12:10:55 EDT 2011] freelance general, take photo with @elisa
    #
    #    ++ personal, start the great american novel
    #       >> + [Wed Sep 21 13:40:09 EDT 2011] personal, start the great american novel
    #    == -a 'great american novel' 'tomorrow'
    #       >> - [Wed Sep 21 13:45:32 EDT 2011] personal, start the great american novel
    #       >> + [Wed Sep 21 13:45:32 EDT 2011] personal, start the great american novel tomorrow
    #
    # saved as /usr/bin/==, chmod +x
    # I chose '==' since that's the same key as '++' without the shift
    
    APPEND=0
    while getopts ":a" OPTION
      do 
        case $OPTION in
          a)
            APPEND=1
            shift
        esac
      done
    
    pat="^\+\s\[.+\]\s(.*)($1)(.*)$"
    xx $(  cat ~/.todo | sed -r -n "s/${pat}/\1\2\3/1p" | tail -1 )
    if [ "$APPEND" -eq "1" ]
    then
        ++ $( cat ~/.todo | sed -r -n "s/${pat}/\1\2\3 $2/1p" | tail -1 ) 
    else
      ++ $( cat ~/.todo | sed -r -n "s/${pat}/\1$2\3/1p" | tail -1 )
    fi
    
    3-add.bash
    #! /usr/bin/env bash
    echo "+" \[$(date)\] $@ >> ~/.todo
    
    # saved as /usr/bin/++, chmod +x
    
    1-README.markdown

    My key requirements were

    • it has to be easy to add or remove something from a todo list wherever you are in the filesystem
    • it should be easy to add or remove a task based on previous tasks added
    • it should not make changes to a file that you are editing manually
    • the syntax should be as close to simply writing a note to yourself as possible.

    The commands to add and remove tasks are one-liner bash scripts to echo the command line to ~/.todo, basically. So

    $ ++ personal, start the great american novel
    $ xx freelance myproject, refactor the frobosh modules
    

    ++ is add, xx is remove. You can name them whatever you like, of course.

    Anything before the first comma is treated as categories, anything after is the task itself.

    I decided I wanted to use bash autocompletion to much the same effect as select menu autocompletion -- as you type it narrows down the choices to previously-entered tasks.

    So this gives you basically a log file. Then I wrote a little Ruby program to parse this and output to yaml (among other things).

    And I wrote another little bash tool called == for append-editing:

    $ ++ personal, start the great american novel
    $ == -a 'novel$' 'tomorrow'  # this adds a '-' record, then a new '+' record to the log
    

    Of course for total flexibility you could just use sed to edit the file in-place, too.

    So with these building blocks you could easily do something like

    • set a directory watcher on the .todo file and automatically redisplay a current todo list as it's edited (see example below)
    • serve up the .todo file through a web app and provide a GUI for adding tasks
    • stream it to loggly
    • parse it into commands to send to your arduino-controlled personal robot over IRC

    etc....

    It's not rocket science and I've taken ideas from others, but it could be useful. I'm especially happy about the autocompletion, which is such a timesaver. Also it gave me a chance to struggle with bash which I've been wanting to do.


    Note that the ruby program below is just the basic idea. I have expanded it to include parsing out various things from the task description, using a markup format kinda similar to todo.txt, but simplified. I have person tags (@eric), other tags (=done), time estimates (~1h30m), and absolute or relative dates (due mon, started 1-Oct-2011), all of which can appear anywhere in the task description. (The only fixed thing is the categories which appear in the front.)

    I've also split out the display methods into presenter classes and run them through Tilt templates. But all of this is a bit too much to put in a gist, so I left it simple, I'll put up the full code after CodeBrawl ends...

    8-dirwatch.rb
    #! /usr/bin/env ruby
    
    # Using rb-inotify to refresh the todo list in the console, as it gets updated
    
    require File.expand_path('../lib/todo',File.dirname(__FILE__))   # the todo.rb file above
    require 'rb-inotify'
    
    notif = INotify::Notifier.new
    
    Signal.trap("INT") { notif.stop; exit }
    
    modify_proc = lambda {
      lines = []
      File.open(File.expand_path('~/.todo')) {|f| lines = f.readlines}  
      t = Todo.parse(lines)
      system('clear')
      puts "TODO", 
           t.flat_to_yaml
    }
    
    
    watch_proc = lambda {
      notif.watch(File.expand_path('~/.todo'), :close_write) do 
        modify_proc[] 
        watch_proc[]
      end
    }
    
    modify_proc[]
    watch_proc[]
    
    notif.run
    
    2-todo.rb
    # Simple parser for ~/.todo file
    # Format is like
    #
    #   + [Tue Sep 20 12:10:13 EDT 2011] personal, walk the dog ~20m
    #   + [Tue Sep 20 12:10:50 EDT 2011] freelance general, take photo with @thomas
    #   - [Tue Sep 20 12:13:00 EDT 2011] freelance general, take photo
    #   
    # + == added tasks
    # - == finished tasks
    #
    # The description is split into two parts; anything before the first comma is
    # treated as a hierarchy of categories, anything after is the task description
    #
    # More features are planned such as extracting @-tags (@thomas above)
    # and time estimates (~15m) from the strings
    #
    # Note tasks are matched on the beginning of the description; so the finished
    # task 'freelance general, take photo' matches the previous
    #      'freelance general, take photo with @thomas'
    #
    # The ~/.todo file itself is the product of a few simple bash programs, see
    # other gist files for details.
    #
    # See bottom of this file for usage examples.
    #
    require 'time'
    require 'yaml'
    require 'set'
    
    class Todo
    
      def self.parse(lines, &config)
        new *lines.map {|line| LogEntry.parse(line) }.compact, &config
      end
      
      attr_reader :tasks
      attr_accessor :mark_done
      
      def initialize(*entries)
        @tasks = []
        yield self if block_given?
        entries.sort_by(&:logtime).each do |e|
          if e.add?; add e.task, e.categories; end
          if e.sub?; sub e.task, e.categories; end
          if e.change?; change e.task, e.categories; end
        end
      end
       
      def add(task, categories=[])
        @tasks << Task.new(task,categories)
      end
    
      def sub(task, categories=[], tag=self.mark_done)
        task = Task.new(task,categories)
        if tag then
          @tasks.select {|t| task == t}.each do |t|
            t.tag tag
          end
        else
          @tasks.delete_if {|t| task == t}
        end
      end
    
      def change(task, categories=[])
        sub task, categories, nil
        add task, categories
      end
       
      def aggregate(meth=nil)
        tasks.inject({}) {|memo,task|
          trav = memo
          task.categories.each do |cat| 
            trav = ( trav[cat] ||= {} )
          end
          (trav['tasks'] ||= []) << (meth ? task.send(meth) : task)
          memo
        }
      end
    
      def flat_aggregate(meth=nil)
        tasks.inject(Hash.new {|h,k| h[k]=[]}) {|memo, task|
          memo[task.categories.join(' ')] << (meth ? task.send(meth) : task)
          memo
        }
      end
      
      def category(cat)
        aggregate[cat]
      end
      alias [] category
      
      # TODO move these to view class
      def flat_to_yaml
        YAML.dump(Hash[flat_aggregate(:name_with_tags).sort])
      end
    
      def to_yaml
        YAML.dump(aggregate(:name_with_tags))
      end
      
      class Task
        attr_reader :categories, :tags
        attr_accessor :name
    
         def initialize(task, categories=[])
           @name, @categories = task, categories
           @tags = Set.new
         end
    
         def recategorize(*cats)
           @categories = cats
         end
         
         def categorize(cat)
           @categories.pop
           @categories << cat
         end
         
         def tag(t)
           @tags << t
         end
         
         def name_with_tags
           "#{self.name}" +
            (self.tags.empty? ? "" : " (#{self.tags.to_a.join('; ')})")     
         end
         
         def to_s
           "#{self.categories.join(":")}: #{self.name}" +
            (self.tags.empty? ? "" : ": (#{self.tags.to_a.join('; ')})")
         end
    
         def ==(other)
           (self.categories == other.categories) and
    	     (/^#{self.name}/ =~ other.name)
         end
      end
    
      class LogEntry
        attr_reader :action, :logtime, :task, :categories
    
        def self.parse(line)
          return unless /^(\+|\-|\*)\s\[(.*)\]\s([^,]+),\s*(.*)$/ =~ line
          new $1, $2, $3, $4
        end
    
        def initialize(action,logtime,categories,task)
          @action = action
          @logtime = Time.parse(logtime)
          @categories = categories.split(' ')
          @task = task
        end
        
        def add?; @action == '+'; end
        def sub?; @action == '-'; end
        def change?; @action == '*'; end
    
      end
    
    end
    
    
    if $0 == __FILE__
    
       lines = []
       File.open(File.expand_path('~/.todo')) {|f| lines = f.readlines}
    
       # todo list where completed tasks are removed
       t = Todo.parse(lines)
       puts t.to_yaml
       puts
       puts t.flat_to_yaml
       
       # todo list where completed tasks are tagged "done"
       t2 = Todo.parse(lines) {|todo| todo.mark_done = "done"}
       puts t2.to_yaml
       puts
       puts t2.flat_to_yaml
       
    end
    
    6-example.todo.txt
    + [Tue Sep 20 12:10:13 EDT 2011] personal, walk the dog ~20m
    + [Tue Sep 20 12:10:50 EDT 2011] freelance general, take photo with @thomas
    - [Tue Sep 20 12:13:00 EDT 2011] freelance general, take photo
    
    View full entry
    Finished in 3rd place with a final score of 3.3/5. (View the Gist)
  • readme.md

    todo.rb

    todo.rb is a dead simple command line todo list manager written in Ruby.

    Making it Go - A Simple Tutorial

    todo.rb is simple, first lets add some items to our list

    ./todo.rb buy tomato seeds
    ./todo.rb buy a new green hose
    ./todo.rb buy potting soil
    ./todo.rb plant tomatoes
    

    Next, lets have a look at the list

    todo.rb
    

    produce this output

    [  1] buy tomato seeds
    [  2] buy a new green hose
    [  3] buy potting soil
    [  4] plant tomatoes
    

    Now, we've gone to the store and bought ourselves a nice new green garden hose. Time to check that one off:

    ./todo.rb done 2
    

    If we check the list, ./todo.rb, we see:

    [  1] buy tomato seeds
    [  2] buy potting soil
    [  3] plant tomatoes
    

    That's it, that's the easiest way to manage your list with todo.rb

    Managing Multiple lists

    Managing multiple lists is simple, too. To manage a list specify the location with -f flag:

    ./todo.rb -f work.txt Click the keys on the keyboard
    ./todo.rb -f dog.txt buy a new squeaky toy
    

    Why does this Rock?

    It's easy- it's easy to use and it's easy to modify. It's also portable. You can pickup your todo file and move it at any time. Want to add todo's when you're away from the command line? That's no big deal- keep the file in dropbox and you can edit it on your phone, tablet, or anywhere else you can edit a text file.

    On the code front- it's well organized Ruby with rdoc style comments.

    Some Notes on a todo.txt Workflow

    Personally, I use a todo.txt file on my desktop. I like that it's lightweight. On a mac you can use Quicklook to get a quick view of what's inside a file. To do this, click on the todo.txt file and then hit the space bar.

    I've also created an Alfred action for appending to the todo.txt file. This action can be added by downloading the file and dragging it into extension pane. From there you can use todo followed by the task. For example todo clean the fish tank.

    Who took that Dog Photo?

    The dog picture was taken by Randy Son Of Robert on Flickr and is used under a creative commons 2.0 CC by attribution license.

    Shut up and take my money - The license

    You're money is no good here, cowboy. All code, documentation, and the alfred extension are distributed under an MIT license. Please fork it, hack it, fix it, share it, break it, now upgrade it. As they say, technologic.


    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:

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    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.

    todo.rb
    #!/usr/bin/env ruby
    
    # License::   Distributed under MIT license
    
    require 'optparse'
    
    # TodoItem represents a todo item it is primarily used as a structured container
    # for todo information
    
    class TodoItem
      attr_accessor :description, :line_number
      
      def initialize(todo, options = {})
        @description = todo
        @line_number = options[:line_number]
      end
      
      # Represents the todo item as a string with the line number taking up 3 spaces
      # and the description immediately following it
      def to_s
        "[#{"% 3d" % (line_number)}] #{description}"
      end
    end
    
    
    
    # TodoList is a container class for many TodoItems it includes methods for
    # adding todos, removing todos, and getting todos at a given index. It also
    # interacts with the filesystem to read and save the file
    
    class TodoList
      attr_accessor :todo_list, :file_location
      
      # Reads todos from a file if the file exists and sets up variables for the
      # class
      def initialize(file_location)
        @file_location = file_location
        @todo_list = []
        if File.exist? file_location
          File.open(@file_location).each_with_index do |line, line_number|
            @todo_list << TodoItem.new(line.chomp, {:line_number => line_number + 1})
          end
        end
      end
      
      # Appends a new todo to the list by getting next line number and then appending
      # the todo to the @todo_list
      def append(todo)
        todo.line_number = next_line_number()
        @todo_list << todo
        save_list()
      end
      
      # Removes a todo from the list
      def remove(todo)
        @todo_list.delete_at(todo.line_number - 1)
        update_todo_line_numbers()
        save_list()
      end
      
      # Gets a todo at an index if that index is a number.  If the index isn't a number
      # then nil is returned.  If there is no todo at an index nil is returned.
      def todo_at_index(index)
        begin
          index = Integer(index)
          index-=1
          @todo_list[index]
        rescue
          return nil
        end
      end
      
      # Represents the todo list as a String with each todo on its on line and the def the 
      # newest todo at the bottom of the list
      def to_s
        return "" if @todo_list.empty?
        return @todo_list.first.to_s if @todo_list.size == 1
        @todo_list.reduce { |todo, string|  "#{todo.to_s}\n#{string}" }
      end
      
      private
      
      def next_line_number
        return 0 if @todo_list.empty?
        @todo_list.last.line_number + 1
      end
      
      def update_todo_line_numbers
        @todo_list.each_with_index{ |todo, index| todo.line_number = index}
      end
      
      def save_list
        File.open(@file_location, "wb") do |file|
          @todo_list.each { |todo| file.write(todo.description + "\n")}
        end
      end
    end
    
    
    options = {:file => "todo.txt"}
    
    OptionParser.new do |opts|
      opts.on("-f", "--file F", String, "Use a custom todo file") do |f|
        options[:file] = f
      end
    end.parse!
    
    todo_list = TodoList.new(options[:file])
    
    if ARGV.count == 0
      puts todo_list
    else
      if ARGV[0].downcase == "done"   
        todo = todo_list.todo_at_index(ARGV[1])
        if todo.nil?
          puts "Couldn't find todo at location #{ARGV[1]}"
        else
          todo_list.remove(todo)
        end
      else
        todo = TodoItem.new(ARGV.join(" "))
        todo_list.append(todo)
      end
    end
    
    View full entry
    Finished in 4th place with a final score of 3.3/5. (View the Gist)
  • Gemfile
    source "http://rubygems.org"
    gem "main", "~> 4.7.7"
    gem "moocow", "~> 1.1.0"
    gem "paint", "~> 0.8.3"
    
    # Add dependencies to develop your gem here.
    # Include everything needed to run rake, tests, features, etc.
    group :development do
      gem "rspec"
      gem "bundler"
      gem "jeweler"
      gem "ZenTest"
    end
    
    VERSION
    0.2.1
    .document
    lib/**/*.rb
    bin/*
    - 
    features/**/*.feature
    LICENSE.txt
    
    .gitignore
    # rcov generated
    coverage
    
    # rdoc generated
    rdoc
    
    # yard generated
    doc
    .yardoc
    
    # bundler
    .bundle
    
    # jeweler generated
    pkg
    
    # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 
    #
    # * Create a file at ~/.gitignore
    # * Include files you want ignored
    # * Run: git config --global core.excludesfile ~/.gitignore
    #
    # After doing this, these files will be ignored in all your git projects,
    # saving you from having to 'pollute' every project you touch with them
    #
    # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
    #
    # For MacOS:
    #
    #.DS_Store
    
    # For TextMate
    #*.tmproj
    #tmtags
    
    # For emacs:
    #*~
    #\#*
    #.\#*
    
    # For vim:
    #*.swp
    
    # For redcar:
    #.redcar
    
    # For rubinius:
    #*.rbc
    
    Gemfile.lock
    GEM
      remote: http://rubygems.org/
      specs:
        ZenTest (4.6.2)
        arrayfields (4.7.4)
        chronic (0.6.4)
        diff-lcs (1.1.3)
        fattr (2.2.0)
        git (1.2.5)
        gli (1.3.3)
        httparty (0.8.0)
          multi_json
          multi_xml
        jeweler (1.6.4)
          bundler (~> 1.0)
          git (>= 1.2.5)
          rake
        main (4.7.7)
          arrayfields (~> 4.7.4)
          chronic (~> 0.6.2)
          fattr (~> 2.2.0)
          map (~> 4.3.0)
        map (4.3.0)
        moocow (1.1.0)
          gli (>= 0.1.5)
          httparty (>= 0.3.1)
        multi_json (1.0.3)
        multi_xml (0.4.1)
        paint (0.8.3)
        rake (0.9.2)
        rspec (2.6.0)
          rspec-core (~> 2.6.0)
          rspec-expectations (~> 2.6.0)
          rspec-mocks (~> 2.6.0)
        rspec-core (2.6.4)
        rspec-expectations (2.6.0)
          diff-lcs (~> 1.1.2)
        rspec-mocks (2.6.0)
    
    PLATFORMS
      ruby
    
    DEPENDENCIES
      ZenTest
      bundler
      jeweler
      main (~> 4.7.7)
      moocow (~> 1.1.0)
      paint (~> 0.8.3)
      rspec
    
    milkmaid
    #!/usr/bin/env ruby
    
    $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
    
    require 'main'
    require 'paint'
    require 'milkmaid'
    
    def milkmaid
      @milkmaid ||= Milkmaid.new
    end
    
    Main {
      description "Milkmaid command-line client for Remember the Milk\n" +
        "If this is your first time running this, use the auth command to connect to your account."
    
      def run
        help!
      end
    
      mode :list do
        description "Lists all inactive tasks. Tasks that repeat are marked with a (R).\n" +
          "If there is a due date, it appears to the right of the task. Tasks are color\n" +
          "coded by priority the same when viewed on the web. Tasks due today are in bold\n" +
          "while tasks that are overdue are underlined.\n" +
          "Use this command to assign a number to tasks so you may perform other actions."
    
        def run
          begin
            milkmaid.incomplete_tasks.each_with_index do |taskseries, i|
              task = Milkmaid::last_task taskseries
              text = "#{i+1}: #{taskseries['name']}"
              text << "(R)" unless taskseries['rrule'].nil?
              text << " #{Time.parse(Milkmaid::last_task(taskseries)['due']).getlocal.strftime(
              "%A %b %d, %Y %I:%M %p")}" unless task['due'].empty?
              color = {
                '1'=>[234, 82, 0], 
                '2'=>[0, 96, 191], 
                '3'=>[53, 154, 255], 
                'N'=>:nothing
              }
              mode1 = mode2 = nil
              case Date.today <=> Date.parse(task['due'])
              when 0
                mode1 = :bold
              when 1
                mode1 = :bold
                mode2 = :underline
              end unless task['due'].empty?
              puts Paint[text, color[task['priority']], mode1, mode2]
            end
          rescue RTM::NoTokenException
            puts "Authentication token not found. Run `#{__FILE__} auth start`"
          end
        end
      end
    
      mode :complete do
        description "Marks a task as complete."
    
        argument(:tasknum) {
          cast :int
          description "The number of the task as printed by the list command."
        }
    
        def run
          begin
            milkmaid.complete_task params['tasknum'].value
          rescue Milkmaid::TaskNotFound
            puts "Task ##{params['tasknum'].value} not found. Run `#{__FILE__} list` " +
              "to load a list of tasks."
          rescue RTM::NoTokenException
            puts "Authentication token not found. Run `#{__FILE__} auth start`"
          end
        end
      end
    
      mode :postpone do
        description "Extends the due date of a task."
    
        argument(:tasknum) {
          cast :int
          description "The number of the task as printed by the list command."
        }
    
        def run
          begin
            milkmaid.postpone_task params['tasknum'].value
          rescue Milkmaid::TaskNotFound
            puts "Task ##{params['tasknum'].value} not found. Run `#{__FILE__} list` " +
              "to load a list of tasks."
          rescue RTM::NoTokenException
            puts "Authentication token not found. Run `#{__FILE__} auth start`"
          end
        end
      end
    
      mode :delete do
        description "Gets rid of a task."
    
        argument(:tasknum) {
          cast :int
          description "The number of the task as printed by the list command."
        }
    
        def run
          begin
            milkmaid.delete_task params['tasknum'].value
          rescue Milkmaid::TaskNotFound
            puts "Task ##{params['tasknum'].value} not found. Run `#{__FILE__} list` " +
              "to load a list of tasks."
          rescue RTM::NoTokenException
            puts "Authentication token not found. Run `#{__FILE__} auth start`"
          end
        end
      end
    
      mode :add do
        argument(:taskname) 
    
        def run
          begin
            milkmaid.add_task params['taskname'].value
          rescue RTM::NoTokenException
            puts "Authentication token not found. Run `#{__FILE__} auth start`"
          end
        end
      end
            
      mode :auth do
        description "Auth commands are used to connect Milkmaid to your account."
    
        mode :start do
          description "Use this command to generate an authentication link.\n" +
            "Follow the link before issuing the auth finish command."
    
          def run
            puts '1. Visit the URL to authorize the application to access your account.'
            puts "2. Run `#{__FILE__} auth finish`"
            puts
            puts milkmaid.auth_start
          end
        end
    
        mode :finish do
          description "Use this command to complete authentication to your account."
    
          def run
            begin
              milkmaid.auth_finish
              puts 'Authentication token saved.'
            rescue RTM::VerificationException
              puts "Invalid frob. Did you visit the link from `#{__FILE__} auth start`?"
            rescue RuntimeError
              puts "Frob does not exist. Did you run `#{__FILE__} auth start`?"
            end
          end
        end
      end
    }
    
    spec_helper.rb
    $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
    $LOAD_PATH.unshift(File.dirname(__FILE__))
    require 'rspec'
    require 'milkmaid'
    
    # Requires supporting files with custom matchers and macros, etc,
    # in ./support/ and its subdirectories.
    Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
    
    RSpec.configure do |config|
      
    end
    
    milkmaid_spec.rb
    require 'spec_helper'
    
    describe "Milkmaid" do
      let(:lib) { Milkmaid.new }
      let(:auth_double) { double('auth').as_null_object }
      let(:rtm_double) { double('rtm').as_null_object }
      let(:timeline_double) { double('timeline').as_null_object }
      let(:tasks_double) { double('tasks') }
    
      before do
        RTM::RTM.stub(:new) { rtm_double }
        rtm_double.stub(:auth) { auth_double }
        YAML.stub(:load_file).and_return({:token=>'tsttoken'})
        File.stub(:open)
        rtm_double.stub_chain(:timelines, :create) { timeline_double }
        rtm_double.stub(:tasks) { tasks_double }
      end
    
      context "when config dotfile exists" do
        it "loads the configuration dotfile" do
          ENV['HOME'] = 'testhome'
          YAML.should_receive(:load_file).with('testhome/.milkmaid') { 
            {:frob=>'testfrob',
             :token=>'testtoken'} }
          auth_double.should_receive(:frob=).with('testfrob')
          rtm_double.should_receive(:token=).with('testtoken')
          lib
        end
    
        it "does not attempt to create a timeline when no auth token exists" do
          YAML.stub(:load_file).and_return({})
          rtm_double.should_not_receive(:timelines)
          lib
        end
      end
    
      context "when config dotfile does not exist" do
        it "does not crash" do
          YAML.stub(:load_file).and_raise(Errno::ENOENT)
          lib
        end
      end
    
      describe "listing tasks" do
        let(:a) {{"name"=>"a", "id"=>"ats", "task"=>{"completed"=>"", "priority"=>"1", 
                                          "id"=>"at", "due"=>""}}}
        let(:b) {{"name"=>"b", "id"=>"bts", "task"=>[
                                          {"completed"=>"2009-10-01", "priority"=>"1",
                                          "id"=>"bt", "due"=>"2011-10-02T02:52:58Z"},
                                          {"completed"=>"", "priority"=>"1",
                                          "id"=>"bt", "due"=>"2011-10-02T02:52:58Z"}]}}
        let(:c) {{"name"=>"c", "id"=>"cts", "task"=>{"completed"=>"", "priority"=>"N",
                                          "id"=>"ct", "due"=>"2012-10-02T02:52:58Z"}}}
        let(:d) {{"name"=>"d", "id"=>"dts", "task"=>{"completed"=>"", "priority"=>"N",
                                          "id"=>"dt", "due"=>"2011-10-02T02:52:58Z"}}}
        before do
          RTM::RTM.stub_chain(:new, :tasks, :get_list) { 
            {"tasks"=>{"list"=>[{"id"=>"21242147", "taskseries"=>[
              a, b, c, d,
              {"name"=>"done task", "task"=>{"completed"=>"2011-10-02T02:52:58Z"}}
            ]}, 
            {"id"=>"21242148"}, 
            {"id"=>"21242149"}, 
            {"id"=>"21242150"}, 
            {"id"=>"21242151"}, 
            {"id"=>"21242152"}], 
            "rev"=>"4k555btb3vcwscc8g44sog8kw4ccccc"}, 
            "stat"=>"ok"}
          }
        end
    
        it "returns all incomplete tasks in order of priority then due date" do
          lib.incomplete_tasks.should == [b, a, d, c]
        end
    
        it "assigns and stores a local ID number to each task for easy addressing" do
          should_store_in_configuration({
            :token=>'tsttoken',
            '1list_id'=>'21242147', '1taskseries_id'=>'bts', '1task_id'=>'bt',
            '2list_id'=>'21242147', '2taskseries_id'=>'ats', '2task_id'=>'at',
            '3list_id'=>'21242147', '3taskseries_id'=>'dts', '3task_id'=>'dt',
            '4list_id'=>'21242147', '4taskseries_id'=>'cts', '4task_id'=>'ct'
          })
          lib.incomplete_tasks
        end
      end
    
      describe "working with existing tasks" do
        it "raises an error when unable to find the desired task in config" do
          lambda { lib.complete_task 1 }.should raise_error(Milkmaid::TaskNotFound)
          lambda { lib.postpone_task 2 }.should raise_error(Milkmaid::TaskNotFound)
          lambda { lib.delete_task 3 }.should raise_error(Milkmaid::TaskNotFound)
        end
    
        it "marks the task as complete" do
          should_call_rtm_api(:complete, 1)
          lib.complete_task 1
        end
    
        it "postpones a task" do
          should_call_rtm_api(:postpone, 2)
          lib.postpone_task 2
        end
    
        it "deletes a task" do
          should_call_rtm_api(:delete, 3)
          lib.delete_task 3
        end
      end
    
      it "adds a task to the inbox using Smart Add" do
        tasks_double.should_receive(:add).with(:name=>'TestName', :parse=>'1',
                                               :timeline=>timeline_double)
        lib.add_task 'TestName'
      end
    
      describe "authentication" do
        before do
          auth_double.stub(:url) { 'http://testurl' }
          auth_double.stub(:frob) { 'testfrob' }
          File.stub(:open)
        end
    
    
        describe "setup" do
          before do
            YAML.stub(:load_file).and_raise(Errno::ENOENT)
          end
    
          it "directs the user to setup auth" do
            lib.auth_start.should == 'http://testurl'
          end
    
          it "stores the frob in configuration" do
            should_store_in_configuration({:frob=>"testfrob"})
            lib.auth_start
          end
        end
    
        describe "completion" do
          it "stores the auth token in the dotfile" do
            YAML.stub(:load_file) { {:frob=>'testfrob'} }
            auth_double.stub(:get_token) {'testtoken'}
            should_store_in_configuration({
                                        :frob=>'testfrob',
                                        :token=>'testtoken'})
            lib.auth_finish
          end
        end
      end
    end
    
    def should_store_in_configuration(config)
      io_double = double('io')
      File.should_receive(:open).and_yield(io_double)
      YAML.should_receive(:dump).with(config, io_double)
    end
    
    def should_call_rtm_api(method, tasknum)
      YAML.stub(:load_file) {{
        :token=>'tsttoken',
        "#{tasknum}list_id"=>"#{tasknum}l",
        "#{tasknum}taskseries_id"=>"#{tasknum}ts",
        "#{tasknum}task_id"=>"#{tasknum}t"
      }}
      tasks_double.should_receive(method).with(
        :list_id=>"#{tasknum}l", :taskseries_id=>"#{tasknum}ts", 
        :task_id=>"#{tasknum}t", :timeline=>timeline_double)
    end
    
    milkmaid.rb
    require 'moocow'
    
    class Milkmaid
      def initialize
        @rtm = RTM::RTM.new(RTM::Endpoint.new('31308536ffed80061df846c3a4564a27', 'c1476318e3483441'))
        @auth = @rtm.auth
        begin
          @config_file = File.join(ENV['HOME'], '.milkmaid')
          @config = YAML.load_file(@config_file)
          @auth.frob = @config[:frob]
          @rtm.token = @config[:token]
          @timeline = @rtm.timelines.create['timeline'] unless @config[:token].nil?
        rescue Errno::ENOENT
          @config = {}
        end
      end
    
      def incomplete_tasks
        entries = []
        list_id = nil
        @rtm.tasks.get_list['tasks']['list'].as_array.each do |items|
          list_id = items['id']
          if !items['taskseries'].nil?
            items['taskseries'].as_array.each do |taskseries|
              taskseries['list_id'] = list_id
              entries << taskseries if Milkmaid::last_task(taskseries)['completed'].empty?
            end
          end
        end
        entries.sort! do |a, b|
          taska = Milkmaid::last_task a
          taskb = Milkmaid::last_task b
          result = taska['priority'] <=> taskb['priority']
          if result == 0
            if taska['due'].empty?
              1
            elsif taskb['due'].empty?
              -1
            else
              Time.parse(taska['due']) <=> Time.parse(taskb['due'])
            end
          else
            result
          end
        end
        entries.each_with_index do |taskseries, i|
          @config["#{i+1}list_id"] = taskseries['list_id']
          @config["#{i+1}taskseries_id"] = taskseries['id']
          @config["#{i+1}task_id"] = Milkmaid::last_task(taskseries)['id']
        end
        save_config
        entries
      end
    
      def complete_task(tasknum)
        check_task_ids tasknum
        call_rtm_api :complete, tasknum
      end
    
      def postpone_task(tasknum)
        check_task_ids tasknum
        call_rtm_api :postpone, tasknum
      end
    
      def delete_task(tasknum)
        check_task_ids tasknum
        call_rtm_api :delete, tasknum
      end
    
      def add_task(name)
        @rtm.tasks.add :name=>name, :parse=>'1', :timeline=>@timeline
      end
      
      def auth_start
        url = @auth.url
        @config[:frob] = @auth.frob
        save_config
        url
      end
    
      def auth_finish
        @config[:token] = @auth.get_token
        save_config
      end
    
      class TaskNotFound < StandardError
      end
    
      def self.last_task(taskseries)
        taskseries['task'].as_array.last
      end
    
      private
      def save_config
        File.open(@config_file, 'w') { |f| YAML.dump(@config, f) }
      end
    
      def check_task_ids(tasknum)
        raise TaskNotFound if @config["#{tasknum}list_id"].nil? || 
                              @config["#{tasknum}taskseries_id"].nil? ||
                              @config["#{tasknum}task_id"].nil?
      end
    
      def call_rtm_api(method, tasknum)
        @rtm.tasks.send method, :list_id=>@config["#{tasknum}list_id"],
                            :taskseries_id=>@config["#{tasknum}taskseries_id"],
                            :task_id=>@config["#{tasknum}task_id"],
                            :timeline=>@timeline
      end
    end
    
    README.rdoc
    = Milkmaid
    
    Milkmaid is a command-line client for Remember the Milk.
    
    ==Features
    * View a list of incomplete tasks, and their due dates
    * Complete a task
    * Postpone a task
    * Add a new task, setting properties using the Smart Add feature
    
    == Installing
    
    $ gem install milkmaid
    
    == Authorizing
    
    Before you can connect to your tasks you must authorize the client.
    
    1. $ milkmaid auth start
    2. Follow the generated link
    3. $ milkmaid auth finish
    
    Now you are ready to work with your tasks.
    
    == Commands
    
    View a list of incomplete tasks and get task numbers. You will need a task
    number to do things to tasks like completing or postponing them. The task
    number appears at the beginning.
    
    $ milkmaid list
    
    
    Complete a task
    
    $ milkmaid complete 
    
    
    Postpone a task
    
    $ milkmaid postpone 
    
    
    Delete a task
    
    $ milkmaid delete 
    
    Add a task (Use single quotes to contain the task name)
    
    Adding a task supports Remember the Milk's Smart Add feature. With it you
    can set properties such as the due date, priority, etc.
    
    $ milkmaid add ''
    
    == Version history
    
    === 0.2.1
    
    * Fixed auth scenarios
    
    
    === 0.2.0
    
    * New delete command
    * Friendlier usage information
    
    === 0.1.1
    
    * Fixed an issue when listing tasks with recurrences that were completed
    
    === 0.1.0
    
    * Initial version
    
    
    == Contributing to Milkmaid
     
    * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
    * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
    * Fork the project
    * Start a feature/bugfix branch
    * Commit and push until you are happy with your contribution
    * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
    * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
    
    == Copyright
    
    Copyright (c) 2011 Codebrawl Participan. See LICENSE.txt for
    further details.
    
    
    .rspec
    --color
    
    LICENSE.txt
    Copyright (c) 2011 Codebrawl Participant
    
    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:
    
    The above copyright notice and this permission notice shall be
    included in all copies or substantial portions of the Software.
    
    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.
    
    View full entry
    Finished in 5th place with a final score of 3.2/5. (View the Gist)
  • README.md

    This is an entry to Code Brawl: Command Line TODO Lists

    (http://codebrawl.com/contests/command-line-todo-lists/)

    As for configuration you need a .secretaryrc either in your home folder or in the current directory. This will be the basis of the location of your lists.

    Here's a sample usage which is actually what's outputted when you simply do a ./secretary.

    Secretary is a command line task list manager.
    How to use:
    
    Add a Task:
    $ ./secretary add 'Task'        # Add a task to default list
    $ ./secretary add 'Task' group  # Add a task to list `group`
    
    Create a Group:
    $ ./secretary group GName gn    # Add a group with a postfix id `gn`
    
    List Down Groups:
    $ ./secretary group list        # List all groups except `done`
    $ ./secretary group list "*"    # List all groups
    
    List Down Tasks:
    $ ./secretary list              # List down tasks on default list
    $ ./secretary list GName        # List down tasks under group `GName`
    $ ./secretary list "*"          # List down all tasks
    
    Mark a Task "Done":
    $ ./secretary done 1            # Move task 1 from `default` list to done list
    $ ./secretary done 1-gn         # Move task 1 from `GName` list to done list
    
    Delete a Task:
    $ ./secretary delete 1          # Delete task 1 from `default` list to done list
    $ ./secretary delete 1-gn       # Delete task 1 from `GName` list to done list
    
    group_collection.rb
    class GroupCollection < Array
      def to_s
        self.map do |group|
          "#{%Q{(#{group.postfix}) } if group.postfix}#{group.name}"
        end.join("\n")
      end
    end
    
    test_helper.rb
    require File.expand_path('secretary')
    require 'tmpdir'
    require 'minitest/spec'
    require 'minitest/autorun'
    
    class MiniTest::Spec
      class << self
        alias_method :you         , :it
        alias_method :prepare     , :before
        alias_method :finish      , :after
      end
    end
    
    module Kernel
      alias_method :job_description, :describe
    end
    
    entry.rb
    class Entry
      attr_accessor :subject, :content, :file_path
    
      def initialize(subject, content, directory)
        @subject = subject
        @content = content
        @file_path = File.join(directory, subject.gsub(/[^a-zA-z0-9]/,'_'))
        save_file unless load_file
      end
    
      def load_file
        if(File.exists?(file_path))
          yaml = YAML.load_file(file_path)
          subject, content = yaml[:subject], yaml[:content]
        else
          false
        end
      end
    
      def save_file
        File.open(file_path, 'w+') do |file|
          YAML.dump({
            :subject => subject,
            :content => content
          }, file)
        end
      end
    
      def destroy
        File.delete(file_path)
      end
    
      def to_s
        subject
      end
    end
    
    secretary.rb
    require File.expand_path('entry')
    require File.expand_path('entry_collection')
    require File.expand_path('group')
    require File.expand_path('group_collection')
    require 'yaml'
    
    class Secretary
      class NotFound < StandardError; end
      attr_accessor :environment
    
    class << self
      def directory
        @@directory
      end
    
      def directory=(directory)
        @@directory = directory
      end
    end
    
      def initialize(environment = :development)
        @environment = environment
        Group.init
        load_resources
      end
    
      def load_resources
        Dir.glob(File.join(Secretary.directory, '*')).each do |path|
          name, postfix = File.basename(path).split('_')
          next if postfix.nil? or postfix.eql?('d')
          Group.new(name, :postfix => postfix)
        end
      end
    
      def add(type, content, options = {})
        case type
        when :group
          Group.new(content, options)
        when :task
          group = Group.find(options)
          group.first.add(content)
        end
      end
    
      # list(:task)                    # list tasks from default group
      # list(:task, :group => 'doing') # list tasks from group 'doing'
      # list(:group)                   # list all groups except 'done' group
      # list(:group, :match => '*')    # list all groups
      # list                           # list tasks from default group
      #
      def list(type = :task, options = {})
        case type
        when :group
          groups = Group.list(options)
          groups = GroupCollection.new(groups)
          show(groups)
        when :task
          groups = Group.find(options)
          groups = groups.map do |g|
              g.entries.to_s unless g.entries.empty?
            end
          show(groups.compact.join("\n"))
        end
      end
    
      def show(object)
        puts object.to_s unless environment.eql?(:test)
        object.to_s
      end
    
      def delete(identifier)
        index, identifier = identifier.split('-')
        options = identifier ? {:postfix => identifier, :group => identifier} : {}
        group = Group.find(options)
        group.first.entries.destroy(index.to_i - 1)
      end
    
      def done(identifier)
        entry = delete(identifier)
        raise NotFound.new('Entry not found!') if entry.nil?
        Group.find({:group => 'done'}).first.add(entry.subject, entry.content)
        show("'#{identifier}' moved to done.")
      end
    
      def fire!
        Group.purge!
      end
    end
    
    group.rb
    class Group
    class << self
      attr_accessor :instances
    
      def init
        @instances ||= GroupCollection.new
        Group.default
        Group.done
      end
    
      def purge!
        @default = @done = @instances = nil
      end
    
      def default
        @default ||= Group.new('Default')
      end
    
      def done
        @done ||= Group.new('Done', :postfix => 'd')
      end
    
      def list(options = {})
        match = options[:match] || '((?!done).)*'
        @instances.find_all do |group|
          group.name.match(/^#{match}$/i)
        end
      end
    
      def find(options = {})
        return [Group.default] if options.empty?
        @instances.find_all do |group|
          group.name.match(/^#{options[:group]}$/i) or
          (group.postfix and group.postfix.match(/^#{options[:postfix]}$/i))
        end
      end
    
      def save_instance(instance)
        @instances << instance
      end
    end
    
      class DuplicationError < StandardError; end
      attr_accessor :entries, :postfix, :name, :directory
    
      def initialize(name, options = {})
        @name    = name
        @postfix = options[:postfix]
        @entries = EntryCollection.new(@postfix)
        if !name.eql?('Default') and
           (options[:postfix].nil? or options[:postfix].length.zero? or
            !Group.find({:postfix => options[:postfix]}).empty?)
          raise DuplicationError.new('Postfix already in use!')
        end
        Group.save_instance(self)
        load_directory
      end
    
      def load_directory
        @directory = File.join(Secretary.directory, "#{name}#{%Q{_#{postfix}} if postfix}")
        Dir.mkdir(directory) unless File.directory?(directory)
        Dir.glob(File.join(directory, '*')).each do |path|
          self.add(YAML.load_file(path)[:subject])
        end
      end
    
      def add(subject, content = 'Insert Your Content Here')
        entries << Entry.new(subject, content, directory)
      end
    
      def to_s
        name
      end
    end
    
    secretary_test.rb
    require File.expand_path('test_helper')
    
    job_description Secretary do
      attr_accessor :secretary
      prepare do
        @dir       = Dir.mktmpdir
        Secretary.directory = @dir
        @secretary = Secretary.new(:test)
      end
    
      finish do
        @secretary.fire!
        FileUtils.remove_entry_secure(@dir)
      end
    
      you 'must be able to record tasks' do
        secretary.add(:task, 'My First Task')
        secretary.list(:task).must_equal '(1) My First Task'
        Dir.glob("#{@dir}/**/*").count.must_equal(3)
      end
    
      you 'must be able to prevent duplicates in creating task groups' do
        # `d` is reserved for Done Group
        assert_raises(Group::DuplicationError) do
          secretary.add(:group, 'Duplicate', :postfix => 'd')
        end
    
        # Duplicate Postfix
        secretary.add(:group, 'Doing', :postfix => 'do')
        assert_raises(Group::DuplicationError) do
          secretary.add(:group, 'Doodling', :postfix => 'do')
        end
      end
    
      you "must be able to organize tasks as to what's currently being done" do
        secretary.add(:group, 'Doing' , :postfix => 'do')
        secretary.add(:task , 'Eating', :group => 'doing')
        secretary.list.must_be_empty
        secretary.list(:task, :group => 'doing').must_equal '(1-do) Eating'
      end
    
      you 'must be able to list all tasks from all groups' do
        secretary.add(:group, 'Doing', :postfix => 'do')
        secretary.add(:task, 'Do Something')
        secretary.add(:task, 'Eat', :group => 'doing')
        secretary.list(:task, :group => '*').must_equal %q{
    (1) Do Something
    (1-do) Eat
        }.strip
      end
    
      you 'must be able to show a list of groups' do
        secretary.add(:group, 'Doing', :postfix => 'do')
        secretary.list(:group, :match => '*').must_equal %q{
    Default
    (d) Done
    (do) Doing
        }.strip
      end
    
      you 'must be able to mark a task done' do
        secretary.add(:task, 'Buy Milk')
        secretary.list.must_equal '(1) Buy Milk'
        secretary.done('1')
        secretary.list.must_be_empty
      end
    
      you 'must be able to delete a task' do
        secretary.add(:task, 'Buy Milk')
        secretary.list.must_equal '(1) Buy Milk'
        secretary.delete('1')
        secretary.list.must_be_empty
      end
    
      you 'must be able to keep track of what had been audited' do
        secretary.add(:task, 'Buy Milk')
        secretary.add(:group, 'Kitchen', :postfix => 'kt')
        secretary.add(:task, 'Prepare Potatoes', :group => 'kitchen')
        secretary.list(:group, :match => '*').split(/\n/).count.must_equal(3)
        secretary.list(:task, :group => '*').split(/\n/).count.must_equal(2)
        secretary.fire!
    
        new_secretary = Secretary.new(:test)
        new_secretary.list(:group, :match => '*').split(/\n/).count.must_equal(3)
        new_secretary.list(:task, :group => '*').split(/\n/).count.must_equal(2)
      end
    end
    
    entry_collection.rb
    class EntryCollection < Array
      attr_reader :postfix
      def initialize(postfix = nil)
        @postfix = postfix
      end
    
      def to_s
        count = 0
        self.map do |entry|
          count += 1
          "(#{count}#{%Q[-#{postfix}] if postfix}) #{entry}"
        end.join("\n")
      end
    
      def destroy(index)
        self[index].destroy
        self.delete_at(index)
      end
    end
    
    secretary
    #!/usr/bin/env ruby
    
    require File.expand_path('secretary')
    require 'yaml'
    
    action, content, *options = ARGV
    
    path = File.join(Dir.home, '.secretaryrc')
    path = '.secretaryrc' if File.exist?(File.join('.secretaryrc'))
    
    directory = YAML.load(File.open(path).read)['directory']
    Dir.mkdir(directory) unless File.directory?(directory)
    
    Secretary.directory = directory
    secretary = Secretary.new
    options = {} if options.empty?
    
    begin
    
    case action
    when 'add'
      options = {:group => options[0]} if options[0]
      secretary.add(:task, content, options)
    when 'group'
      if content.eql?('list')
        options = {:match => options[0]} if options[0]
        secretary.list(:group, options)
      else
        options = {:postfix => options[0]} if options[0]
        secretary.add(:group, content, options)
      end
    when 'list'
      options = {:group => content} if content
      secretary.list(:task, options)
    when 'done'
      secretary.done(content)
    when 'delete'
      secretary.delete(content)
    else
      puts <<-HELP
    Secretary is a command line task list manager.
    How to use:
    
    Add a Task:
    $ ./secretary add 'Task'        # Add a task to default list
    $ ./secretary add 'Task' group  # Add a task to list `group`
    
    Create a Group:
    $ ./secretary group GName gn    # Add a group with a postfix id `gn`
    
    List Down Groups:
    $ ./secretary group list        # List all groups except `done`
    $ ./secretary group list "*"    # List all groups
    
    List Down Tasks:
    $ ./secretary list              # List down tasks on default list
    $ ./secretary list GName        # List down tasks under group `GName`
    $ ./secretary list "*"          # List down all tasks
    
    Mark a Task "Done":
    $ ./secretary done 1            # Move task 1 from `default` list to done list
    $ ./secretary done 1-gn         # Move task 1 from `GName` list to done list
    
    Delete a Task:
    $ ./secretary delete 1          # Delete task 1 from `default` list to done list
    $ ./secretary delete 1-gn       # Delete task 1 from `GName` list to done list
      HELP
    end
    
    rescue => error
      puts error
    end
    
    .secretaryrc
    directory: /Users/nelvindriz/Workspace/Ruby/secretary/.secretary
    
    View full entry
    Finished in 6th place with a final score of 3.1/5. (View the Gist)
  • marmot.rb
    #!/usr/bin/ruby -w
    require "readline"
    
    class String
      def title
        self.split('|')[0]
      end
      def status
        self.split('|')[1]
      end
    end
    
    class Marmot
    
      def initialize(name)
        @db = "#{name}.mt"
        File.open(@db, "a")
      end
    
      def method_missing(m, *args, &block)
        puts "No method #{m} available type \"help\" for options"
      end
    
      def help(args = nil)
        puts "-------------------------   MARMOT -----------------------"
        puts "|                The command line TODO list              |"
        puts "----------------------------------------------------------"
        puts
        puts "Tab completion available for commands and task names"
        puts
        puts "Available commands:"
        puts
        puts "add task_name        adds \"task_name\" to the list"
        puts "done task_name       marks \"task name\" as done"
        puts "help                 displays this message"
        puts "list                 List all items in the list"
        puts "start task_name      sets status of \"task_name\" to \"In progress\""
        puts "exit                 exits Marmot"
        puts
      end
    
      def list(args = nil)
        puts "No items on list" if items.size == 0
        items.sort.map{|i| puts "    #{i.title}  :  #{i.status}"}
      end
    
      def add(task)
        file = File.open(@db, "a+")
        file.puts task.gsub("\"","")+"|Not Started"
        file.close
        puts "#{task} added to list"
      end
    
      def start(task = nil)
        change_status(task, "In progress")
        puts "Started"
      end
    
      def done(task = nil)
        current = items
        del = current.map{|i| i.title}.delete(task)
        file = File.open(@db, "w")
        current.each{|i| file.puts i unless i.title == task}
        file.close
        puts del ? "#{task} done" : "Not found"
      end
    
      def items(args = nil)
        File.open(@db,"r").readlines.map{|i| i.chomp}
      end
    
      private
        def change_status(task, new_status)
          current = items
          file = File.open(@db, "w")
          current.each{|i| file.puts "#{i.title}|#{task==i.title ? new_status : i.status}"}
          file.close
        end
    
    
    end
    
    Readline.basic_word_break_characters = '#'
    
    marmot = Marmot.new('todo')
    puts
    marmot.help
    cmd = ''
    
    while cmd != "exit"
    
      completion_proc =  Proc.new{|prefix|
        items = marmot.methods - Object.methods - ['method_missing'] << 'exit'
        ['done', 'start'].each{|cmd|
          items << marmot.items.map{|i| "#{cmd} #{i.title}"} if prefix.index(cmd) == 0
        }
        [items].flatten.sort.select{|i| i.index(prefix) == 0}
      }
    
      Readline.completion_proc = completion_proc
    
      parts  = Readline.readline( "-->> ", true ).chomp.split
      cmd = parts.first
      args = parts.slice(1..-1).join(' ')
      marmot.send(cmd, args) unless cmd == 'exit'
    end
    
    puts 'Thanks for using Marmot'
    
    README.rdoc
    = Marmot
    
    Marmot is a simple command-line todo list written for CodeBrawl. 
    
    It includes tab completion so rather than trying to lookup an item ID in a list you can type the first few characters and tab to complete
    
    The storage is simple file storage.
    
      ./marmot.rb
      -------------------------   MARMOT -----------------------
      |                The command line TODO list              |
      ----------------------------------------------------------
    
      Tab completion available for commands and task names
    
      Available commands:
    
      add task_name        adds "task_name" to the list
      done task_name       marks "task name" as done
      help                 displays this message
      list                 List all items in the list
      start task_name      sets status of "task_name" to "In progress"
      exit                 exits Marmot
    
      -->> li  --> completes to list
      No items on list
      -->> 
      add    done   exit   help   items  list   start  
      ->> add ironing
      ironing added to list
      -->> add cooking
      cooking added to list
      -->> add cleaning
      cleaning added to list
      -->> add clever stuff with Marmot
      clever stuff with Marmot added to list
      -->> start ir --> completes to ironing
      Started
      -->> list
        cleaning  : Not Started
        clever stuff with Marmot  :  Not Started
        cooking  :  Not Started
        ironing  :  In progress
      -->> done 
      done ironing     done cooking       done clever stuff with marmot
      -->>done i --> completes to ironing
      ironing done
      -->> list
        cleaning   : Not Started
        clever stuff with Marmot   : Not Started
        cooking  : Not Started
      -->> done cl
        done cleaning     done clever stuff with marmot
      -->> done clev --> completes
      -->> list
        cooking  : Not Started
        cleaning  : Not Started
      -->> exit
        Thank you for using Marmot.
    
    View full entry
    Finished in 7th place with a final score of 3.0/5. (View the Gist)
  • kansas
    #!/usr/bin/env ruby
    
    
    class Kansas
      KANSAS_DIR = Dir.home + "/.kansas"
      ARCHIVE_DIR = KANSAS_DIR + "/archive"
    
      def initialize(command)
        @todos = []
        @command = command
    
        kansas_dir
        build_list
    
        case @command
          when "create"
            create_todo(ARGV[1])
          when "list"
            puts "Todo List"
            list_todos
            puts " "
          when "destroy"
            if ARGV[1] == "all"
              destroy_all_todos
            else
              destroy_todo(ARGV[1].to_i)
            end
          else
            puts "Commands"
            puts "------------------------------------------------"
            puts "list                  - To list all your todos."
            puts "create # new todo - To create a new todo. # is priority, it can be 0 thru 9. 0 being high priority."
            puts "destroy #             - To destroy a todo."
          end
      end
    
      def kansas_dir
        Dir.mkdir(KANSAS_DIR) unless Dir.exists?(KANSAS_DIR)
      end
    
      def create_todo(priority)
        if priority.to_i > 9
          priority = "9" 
        end
        
        todo = ARGV[2..ARGV.size].join(" ")
    
        file_name = KANSAS_DIR + "/" + priority + "_" + Time.now.to_i.to_s + ".txt" 
    
        File.open(file_name, "w") { |f| f.puts todo }
    
        puts "Created new todo: #{todo}"
      end
    
      def list_todos
        count = 1
        @todos.each do |f|
          priority = File.basename(f)[0]
          todo = File.read(f)
    
          puts "#{count}.#{" " unless count > 9} (#{priority}) #{todo}"
          count += 1
        end   
      end
    
      def build_list
        @todos.clear
    
        Dir[KANSAS_DIR + '/*'].each do |f|
          @todos << f
        end
      end
    
      def destroy_all_todos
        # Clear ARGV to avoid issue with gets.chomp
        ARGV.clear
        puts "Are you sure you want to destroy all todo\'s? Y/N?"
        
        confirmed = gets.chomp
    
        case confirmed
          when "Y"
            @todos.each_index { |t| destroy_todo(t + 1) }
          else
            puts "Cancelled \"destroy all\""
            exit
        end
      end
    
      def destroy_todo(index)
        todo = @todos[index - 1]
        todo_body = File.read(todo)
        File.delete(todo)
    
        puts "Destroyed todo: #{todo_body}"
      end
    end
    
    Kansas.new(ARGV[0])
    
    README.md

    Kansas - A simple command line todo list

    Setup

    curl -O https://raw.github.com/gist/1258077/9fd0955cc10c4d91da208fd1015b673377049940/kansas

    mv kansas /usr/local/bin

    sudo chmod +x /usr/local/bin/kansas

    Usage

    Get a list of available commands kansas help

    Create a new todo:

    kansas # Text for new todo

    Kansas supports giving your todo a priority 0-9

    (replace # with a number 0-9, numbers greater than 9 are automatically changed to 9)

    Example: kansas 1 Write a better README for Kansas this will create a new todo with 1 being the priority.

    List todo's:

    kansas list

    Kansas will list all todo's in order of priority and creation date

    Destroy todo:

    kansas destroy #

    Replace # with the id number of the todo you want to destroy. Get the id number from kansas list, the first number on the row

    Or you can destroy all the todo's in your list

    kansas destroy all You will then be asked to confirm with a Y (case sensitive)

    View full entry
    Finished in 8th place with a final score of 2.8/5. (View the Gist)
  • README.markdown

    Do It - Simple Todo List

    Do It is a simple todo list application written for the command-line. Right now it doesn't have any fancy features or anything and a simple text-file is probably handier to use for keeping track of your todos. Do It is simply my entry for the Code Brawl competition Command line TODO lists.

    Background

    This project is my first in more than one way. It's my first entry to Code Brawl and it's my first command-line application. I didn't enter the competition to win it, I entered because it would give me motivation to just jump into it and try to write my first CLI-application.

    It turned out I couldn't spare a whole of time this week, but at least I got the essential in and I'm planning on improving this application further and then make it my first gem - just for the sake of learning and experimenting.

    I put up two rules for myself when starting to write Do It; I wanted to write clear and organized code and only use the standard library.

    Please feel free to criticise everything about the application. Code, usage, apperance (or well, ain't much to talk about there), etc. I'm always eager to improve so feedback is always appricated.

    Installation

    sudo curl -o /usr/local/bin/doit https://raw.github.com/gist/1252195/14927a66df89abe0ef5a2f5876bec7c2d6eb62ee/doit.rb
    sudo chmod +x /usr/local/bin/doit
    

    Configuration

    By default Do It saves your todos in a textfile called doit.txt in your home folder. You can change the location of that file by editing /usr/local/bin/doit (if you followed the installation instructions - otherwise use the path of where you put it).

    Author

    Developed by Teddy Zetterlund.

    License

    Licened under the MIT License.

    doit.rb
    #!/usr/bin/env ruby
    
    module Doit
      VERSION = '0.0.5'
    
      USAGE = <<EOF
    Do It - Simple Todo List
    
    Usage: #{File.basename $0, '.rb'} COMMAND [ARGS...]
    
    COMMAND is one of:
    
      * list - Show the tasks in an ordered list.
        usage: doit list
    
      * add - Add a new task to the list.
        usage: doit add [TASK]
    
      * remove - Removes task with the specified ID from the list.
        usage: doit remove [ID]
    
      * clear - Removes all tasks from the list and gives fresh start.
        usage: doit clear
    
      EXAMPLES
    
      # Create a new task called "Do the laundry".
      doit add Do the laundry
    
      # Remove task number 4 from the list of tasks.
      doit remove 4
    
      For questions and feedback, contact me at http://twitter.com/teddyzetterlund
    EOF
    
      class Store
        STORAGE_FILE = ENV['HOME'] + '/doit.txt'
    
        def self.read
          File.new(STORAGE_FILE, 'w') unless File.exists?(STORAGE_FILE)
          File.open(STORAGE_FILE, 'r+')
        end
    
        def self.write(list)
          File.open(STORAGE_FILE, 'w') do |f|
            list.map{|task| f << task}
          end
        end
      end
    
      class List < Array
        def initialize
          Doit::Store.read.each_line do |task|
            self << task
          end
        end
    
        def add(args)
          text = args.join(' ').strip
    
          unless text.empty?
            task = Doit::Task.new(text)
            self << task
            true
          else
            false
          end
        end
    
        def remove(task_id)
          if task_id.match(/\d/)
            self.delete_at(task_id.to_i-1)
          end
        end
      end
    
      class Task
        attr_accessor :description
    
        def initialize(description)
          @description = description
        end
    
        def to_s
          "#{description}\n"
        end
      end
    
      class CLI
        ALLOWED_COMMANDS = [:list, :add, :remove, :clear]
    
        def initialize
          @arguments = ARGV
          @commands = []
    
          @list = Doit::List.new
        end
    
        def run
          process_command
        end
    
        def list(args)
          @list.each_with_index do |task, i|
            puts "#{i+1}. #{task}"
          end
        end
    
        def add(*args)
          @list.add(args) ? save! : output_usage
        end
    
        def remove(args)
          @list.remove(args[0]) ? save! : output_usage
        end
    
        def clear(args)
          @list.clear
          save!
        end
    
        private
    
        def process_command 
          @arguments.each do |arg|
            unless arg.index('-') === 0
              @commands << arg
            end
          end
    
          command = @commands.empty? ? :list : @commands[0].to_sym
    
          if ALLOWED_COMMANDS.include?(command)
            self.send(command, @commands[1..@commands.size])
          else
            output_usage
          end
        end
    
        def output_usage
          puts USAGE
        end
    
        def save!
          Doit::Store.write(@list)
          true
        end
      end
    
    end
    
    doit = Doit::CLI.new
    doit.run
    
    View full entry
    Finished in 9th place with a final score of 2.8/5. (View the Gist)