#!/usr/bin/env ruby # Version 0.4 (Dec 17, 2011) # # 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: ./tracks_template_cli -c 1 -f template_file.txt require 'net/https' require 'optparse' require 'cgi' require 'time' require 'readline' class Hash def to_query_string map { |k, v| if v.instance_of?(Hash) v.map { |sk, sv| "#{k}[#{sk}]=#{sv}" }.join('&') else "#{k}=#{v}" end }.join('&') end end module Gtd class API GTD_URI_TODOS = ENV['GTD_TODOS_URL'] || 'http://localhost:3000/todos.xml' GTD_URI_PROJECTS = ENV['GTD_PROJECTS_URL'] || 'http://localhost:3000/projects.xml' GTD_URI_CONTEXTS_PREFIX = ENV['GTD_CONTEXT_URL_PREFIX'] || 'http://localhost:3000/contexts/' GTD_URI_CONTEXTS = ENV['GTD_CONTEXT_URL'] || 'http://localhost:3000/contexts.xml' def postTodo(l, options = {}) uri = URI.parse(GTD_URI_TODOS) http = Net::HTTP.new(uri.host, uri.port) if uri.scheme == "https" # enable SSL/TLS http.use_ssl = true http.ca_path = "/etc/ssl/certs/" # Debian based path http.verify_mode = OpenSSL::SSL::VERIFY_PEER http.verify_depth = 5 end l.chomp! description = CGI.escapeHTML(l) context_id = options[:context_id] ? options[:context_id].to_i : 1 project_id = options[:project_id] ? options[:project_id].to_i : 1 props = "#{description}#{project_id}" if options[:show_from] props << "#{Time.at(options[:show_from]).xmlschema}" end if options[:note] props << "#{options[:note]}" end if options[:taglist] tags = options[:taglist].split(",") if tags.length() > 0 tags = tags.collect { |tag| "#{tag.strip}" unless tag.strip.empty?}.join('') props << "#{tags}" end end if not (options[:context].nil? || options[:context].empty?) props << "#{options[:context]}" else ## use the default context props << "#{context_id}" end if options[:depend] props << "#{options[:last_todo_id]}" end req = Net::HTTP::Post.new(uri.path, "Content-Type" => "text/xml") req.basic_auth ENV['GTD_LOGIN'], ENV['GTD_PASSWORD'] req.body = "#{props}" puts req.body if options[:verbose] resp = http.request(req) if resp.code == '302' || resp.code == '201' puts resp['location'] if options[:verbose] # return the todo id return resp['location'].split("/").last else p resp.body raise Gtd::Error end end def postProject(l, options = {}) uri = URI.parse(GTD_URI_PROJECTS) http = Net::HTTP.new(uri.host, uri.port) if uri.scheme == "https" # enable SSL/TLS http.use_ssl = true http.ca_path = "/etc/ssl/certs/" # Debian based path http.verify_mode = OpenSSL::SSL::VERIFY_PEER http.verify_depth = 5 end l.chomp! description = CGI.escapeHTML(l) props = "#{l}#{options[:context_id]}" req = Net::HTTP::Post.new(uri.path, "Content-Type" => "text/xml") req.basic_auth ENV['GTD_LOGIN'], ENV['GTD_PASSWORD'] req.body = "#{props}" resp = http.request(req) 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 Gtd::Error end end def queryContext(contextID) return false unless contextID.is_a? Integer uri = URI.parse(GTD_URI_CONTEXTS_PREFIX + contextID.to_s + ".xml") http = Net::HTTP.new(uri.host, uri.port) if uri.scheme == "https" # enable SSL/TLS http.use_ssl = true http.ca_path = "/etc/ssl/certs/" # Debian based path http.verify_mode = OpenSSL::SSL::VERIFY_PEER http.verify_depth = 5 end req = Net::HTTP::Get.new(uri.path) req.basic_auth ENV['GTD_LOGIN'], ENV['GTD_PASSWORD'] resp = http.request(req) case resp when Net::HTTPSuccess return true else return false end end end class Error < StandardError; end class InvalidParser < StandardError; end class ConsoleOptions 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) # lines = STDIN.read gtd = API.new if @filename != nil and 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 if @filename == nil file = STDIN else file = File.open(@filename) end ## check for existence of the context if !@options[:context_id] puts "ERROR: need to specify a context_id with -c option. Go here to find one: #{API::GTD_URI_CONTEXTS}" exit 1 end if !gtd.queryContext(@options[:context_id]) puts "Error: context_id #{options[:context_id]} doesn't exist" exit 1 end #lines.each_line do |line| while line = file.gets line = line.strip next if (line.empty? || line[0].chr == "#") if (line.split(' ')[0] == "token") ## defining a new token; ask for input newtok=line.split(' ')[1] print "Input required for "+newtok+": " @keywords[newtok]=gets.chomp next end # replace tokens @keywords.each do |key,val| line=line.sub(key,val) end # decide whether project or task if (line[0].chr == "." ) || (line[0].chr == "^") @options[:depend]= line[0].chr == "^" ? true : false; line = line[1..line.length] # find notes tmp= line.split("|") if tmp.length > 5 puts "Formatting error: found too many |" exit 1 end line=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[:note]=tmp[3] if !@options[:project_id] puts "Warning: no project specified for task \"#{line}\". Using default project." end @options[:last_todo_id]=gtd.postTodo(line, @options) else @options[:project_id]=gtd.postProject(line, @options) end end exit 0 rescue InvalidParser puts "Please specify a valid format parser." exit 1 rescue Error puts "An unknown error occurred" exit 1 end end end if $0 == __FILE__ app = Gtd::ConsoleOptions.new app.run(ARGV) end