#!/usr/bin/env ruby # # Based on the tracks_cli by Vitalie Lazu (https://gist.github.com/45537) # # CLI ruby template client for Tracks: rails application for GTD methodology # https://github.com/TracksApp/tracks # Usage: # * You need to set ENV['GTD_LOGIN'], ENV['GTD_PASSWORD'] # * You need to pipe a file with new projets and actions to the script # * You can use the -k option to customize templates. See the example below. # # Default URLs are: # ENV['GTD_TODOS_URL'] --> 'http://localhost:3000/todos.xml' # ENV['GTD_PROJECTS_URL'] --> 'http://localhost:3000/projects.xml' # ENV['GTD_CONTEXT_URL_PREFIX'] --> 'http://localhost:3000/contexts/' # ENV['GTD_CONTEXT_URL'] --> 'http://localhost:3000/contexts.xml' # project := # dependent_action := ^|context|,..| # independent_action := .|context|,..| # to star an action add a tag 'starred' # Format of input file: # - A token to be replaced in the subsequent lines starts with the string token # - New Projects start at the beginning of the line # - New actions start with an '.' or an '^' at the beginning of the line. # - To add a note to an action, separate the action from its note by using '|'. You have to stay in the same line. # - Comments start with '#' # Simple test file. Remove the '# ' string at the beginning. # token [A] # token [BB] # # to [A] after [BB] # .task 1 in [A], [BB]|computer|starred,blue|my notes here # ^task 1.1 dependent on [A]|||only a note # .task 2 # # project 2 with [A] # .task in project 2 # Example of an input file. Remove the '# ' string at the beginning and save it in a file. # token [location] # token [start] # token [end] # Book trip to [location] # .Check visa requirements for [location]|starred|instantiate template_visa, if visa required # .Book flight to [location]||starting trip around [start], returning around [end] # .Print flight details to [location] # .Book hotel in [location]|checking around [start], leaving around [end] # .Book rental car in [location]|starting [start], returning [end] # .Print hotel booking details to [location] # .Set email vacation reminder|starting [start], returning [end]; Text: I'm off for a vacation. I'll respond to emails after returning ([end]). # .Mail others that I'll be away|starting [start], returning [end] # Pack stuff for trip to [location] # .Pack projector laptop adapter # .Pack socket adapter for country ([location]) # .Pack passport # .Pack flight and hotel detail printout # Get trip reimbursement for [location] # .Collect all [location] receipts in a clear plastic folder # .Set a reminder to check for reimbursement for [location] # .Mail folder to secretary # Instantiate this template: ruby tracks_template_cli -c 1 -f template_file.txt require 'optparse' require 'cgi' require 'readline' require File.expand_path(File.dirname(__FILE__) + '/tracks_cli/tracks_api') class TemplateParser def initialize @keywords = {} end def parse_keyword(token) print "Input required for " + token + ": " @keywords[token] = gets.chomp end def replace_tokens_in(line) @keywords.each { |key, val| line = line.sub(key, val) } line end def parse_todo(line) options = {} # first char is . or ^ the latter meaning this todo is dependent on the previous one options[:depend] = line[0].chr == "^" line = line[1..line.length] # remove first char tmp = line.split("|") if tmp.length > 5 puts "Formatting error: found too many |" exit 1 end tmp[0].chomp! options[:description] = tmp[0] tmp.each_with_index do |t, idx| t = t.strip.chomp t = nil if t.empty? tmp[idx] = t end options[:context] = tmp[1] options[:taglist] = tmp[2] options[:notes] = tmp[3] options end def parse(file, poster) while line = file.gets line = line.strip # skip line if empty or comment next if (line.empty? || line[0].chr == "#") # check if line defines a token if (line.split(' ')[0] == "token") parse_keyword line.split(' ')[1] next end # replace defined tokes in current line line = replace_tokens_in line # line is either todo/dependency or project if (line[0].chr == ".") || (line[0].chr == "^") if @last_project_id.nil? puts "Warning: no project specified for task \"#{line}\". Using default project." end poster.postTodo(parse_todo(line), @last_project_id) else @last_project_id = poster.postProject(line) end end end end class TemplatePoster def initialize(options) @options = options @tracks = TracksCli::TracksAPI.new({ uri: ENV['GTD_TODOS_URL'] || 'http://localhost:3000/todos.xml', login: ENV['GTD_LOGIN'], password: ENV['GTD_PASSWORD'], projects_uri: ENV['GTD_PROJECTS_URL'] || 'http://localhost:3000/projects.xml', contexts_uri: ENV['GTD_CONTEXT_URL'] || 'http://localhost:3000/contexts.xml', context_prefix: ENV['GTD_CONTEXT_URL_PREFIX'] || 'http://localhost:3000/contexts/' }) @context_id = options[:context_id] ? options[:context_id].to_i : 1 @project_id = options[:project_id] ? options[:project_id].to_i : 1 end def postTodo(parsed_todo, project_id) resp = @tracks.post_todo( description: CGI.escapeHTML(parsed_todo[:description]), context_name: parsed_todo[:context], context_id: @context_id, project_id: project_id || @project_id, show_from: parsed_todo[:show_from], notes: parsed_todo[:notes], is_dependend: parsed_todo[:depend], predecessor: @last_posted_todo_id) if resp.code == '302' || resp.code == '201' puts resp['location'] if @options[:verbose] # return the todo id @last_posted_todo_id = resp['location'].split("/").last return @last_posted_todo_id else p resp.body raise Error end end def postProject(project_description) project_description.chomp! resp = @tracks.post_project( description: CGI.escapeHTML(project_description), default_context_id: @context_id) if resp.code == '302' || resp.code == '201' puts resp['location'] if @options[:verbose] # return the project id return resp['location'].split("/").last else p resp.body raise Error end end def queryContext(context_id) return false unless context_id.is_a? Integer resp = @tracks.get_context(context_id) return resp.code == '200' end end class Error < StandardError end class InvalidParser < StandardError end class ConsoleOptionsForTemplate attr_reader :parser, :options, :keywords def initialize @options = {} @keywords = {} @parser = OptionParser.new do |cmd| cmd.banner = "Ruby Gtd Templates CLI" cmd.separator '' cmd.on('-h', '--help', 'Displays this help message') do puts @parser exit end cmd.on('-p [N]', Integer, "project id to set for new todo") do |v| @options[:project_id] = v end cmd.on('-k [S]', "keyword to be replaced") do |v| @keywords[v.split("=")[0]] = v.split("=")[1] end cmd.on('-v', "verbose on") do |v| @options[:verbose] = true end cmd.on('-f [S]', "filename of the template") do |v| @filename = v end cmd.on('-c [N]', Integer, 'default context id to set for new projects') do |v| @options[:context_id] = v end cmd.on('-w [N]', Integer, 'Postpone task for N weeks') do |v| @options[:show_from] = Time.now.to_i + 24 * 3600 * 7 * (v || 1) end cmd.on('-m [N]', Integer, 'Postpone task for N months') do |v| @options[:show_from] = Time.now.to_i + 24 * 3600 * 7 * 4 * (v || 1) end end end def run(args) @parser.parse!(args) @poster = TemplatePoster.new(@options) if !@filename.nil? && not File.exist?(@filename) puts "ERROR: file #{@filename} doesn't exist" exit 1 end if ENV['GTD_LOGIN'] == nil puts "ERROR: no GTD_LOGIN environment variable set" exit 1 end if ENV['GTD_PASSWORD'] == nil puts "ERROR: no GTD_PASSWORD environment variable set" exit 1 end file = @filename.nil? ? STDIN : File.open(@filename) ## check for existence of the context if @options[:context_id].nil? puts "ERROR: need to specify a context_id with -c option." exit 1 end if !@poster.queryContext(@options[:context_id]) puts "Error: context_id #{options[:context_id]} doesn't exist" exit 1 end TemplateParser.new.parse(file, @poster) exit 0 rescue InvalidParser puts "Please specify a valid format parser." exit 1 rescue Error puts "An unknown error occurred" exit 1 end end if $0 == __FILE__ ConsoleOptionsForTemplate.new.run(ARGV) end