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!
-
Finished in 1st place with a final score of 3.7/5. (View the Gist)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.rbView full entryrequire '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)
-
Finished in 2nd place with a final score of 3.4/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.rbYou 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.comList 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 @workDeleting todos
Use the todo number to
deleteit.delalso 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 @workPrioritizing todos
To bump a todo higher on the list:
$todo bump 2 Bump: (2) Buy Duck Typing shirt from rubyrags.com @workHelp
Help is just a command away.
$ todo helpManually Edit
If you want to edit the underlying todo file directly, make sure your $EDITOR environment variable is set, and run:
$ todo editThen 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 @worktodo.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.rbView full entryrequire '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
-
Finished in 3rd place with a final score of 3.3/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.markdownMy 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,xxis 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 logOf 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.txtView full entry+ [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
-
Finished in 4th 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 tomatoesNext, lets have a look at the list
todo.rbproduce this output
[ 1] buy tomato seeds [ 2] buy a new green hose [ 3] buy potting soil [ 4] plant tomatoesNow, we've gone to the store and bought ourselves a nice new green garden hose. Time to check that one off:
./todo.rb done 2If we check the list,
./todo.rb, we see:[ 1] buy tomato seeds [ 2] buy potting soil [ 3] plant tomatoesThat'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
-fflag:./todo.rb -f work.txt Click the keys on the keyboard ./todo.rb -f dog.txt buy a new squeaky toyWhy 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
todofollowed by the task. For exampletodo 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.rbView full entry#!/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
-
Finished in 5th place with a final score of 3.2/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
VERSION0.2.1
.documentlib/**/*.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.lockGEM 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) rspecmilkmaid#!/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.rbrequire '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.rbrequire '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.txtView full entryCopyright (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.
-
Finished in 6th place with a final score of 3.1/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
.secretaryrceither 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.rbclass GroupCollection < Array def to_s self.map do |group| "#{%Q{(#{group.postfix}) } if group.postfix}#{group.name}" end.join("\n") end end
test_helper.rbrequire 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.rbclass 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.rbrequire 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.rbclass 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.rbrequire 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.rbclass 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.secretaryrcView full entrydirectory: /Users/nelvindriz/Workspace/Ruby/secretary/.secretary
-
Finished in 7th place with a final score of 3.0/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.rdocView full entry= 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. -
Finished in 8th place with a final score of 2.8/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.mdView full entryKansas - A simple command line todo list
Setup
curl -O https://raw.github.com/gist/1258077/9fd0955cc10c4d91da208fd1015b673377049940/kansasmv kansas /usr/local/binsudo chmod +x /usr/local/bin/kansasUsage
Get a list of available commands
kansas helpCreate a new todo:
kansas # Text for new todoKansas 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 Kansasthis will create a new todo with 1 being the priority.List todo's:
kansas listKansas 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 rowOr you can destroy all the todo's in your list
kansas destroy allYou will then be asked to confirm with a Y (case sensitive) -
Finished in 9th 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/doitConfiguration
By default Do It saves your todos in a textfile called
doit.txtin 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.rbView full entry#!/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