class Todo
Constants
- COLORS
- COLOR_CODES
- CONTEXT_TAG_PATTERN
- DATE_FORMAT
- DUE_DATE_DAYS_SIMPLE
- DUE_DATE_TAG_PATTERN
- ORDER
- PRIORITY_FLAG
- STATES
- TODO_FILE
Public Instance Methods
execute(arguments)
click to toggle source
# File bin/todo.rb, line 77 def execute(arguments) begin setup action = arguments.first args = arguments.drop(1) case action when 'add' raise action + ' command requires at least one parameter' if args.empty? add(args.join(' ')) when 'start' args.length > 0 ? change_state(args.first.to_i, 'started', args.drop(1).join(' ')) : list(nil, [':started']) when 'done' args.length > 0 ? change_state(args.first.to_i, 'done', args.drop(1).join(' ')) : list(nil, [':done']) when 'block' args.length > 0 ? change_state(args.first.to_i, 'blocked', args.drop(1).join(' ')) : list(nil, [':blocked']) when 'wait' args.length > 0 ? change_state(args.first.to_i, 'waiting', args.drop(1).join(' ')) : list(nil, [':waiting']) when 'reset' args.length > 0 ? change_state(args.first.to_i, 'new', args.drop(1).join(' ')) : list(nil, [':new']) when 'prio' raise action + ' command requires at least one parameter' if args.length < 1 set_priority(args.first.to_i, args.drop(1).join(' ')) when 'due' raise action + ' command requires at least one parameter' if args.length < 1 due_date(args.first.to_i, args.drop(1).join(' ')) when 'append' raise action + ' command requires at least two parameters' if args.length < 2 append(args.first.to_i, args.drop(1).join(' ')) when 'rename' raise action + ' command requires at least two parameters' if args.length < 2 rename(args.first.to_i, args.drop(1).join(' ')) when 'del' raise action + ' command requires exactly one parameter' if args.length != 1 delete(args.first.to_i) when 'note' raise action + ' command requires at least two parameters' if args.length < 2 add_note(args.first.to_i, args.drop(1).join(' ')) when 'delnote' raise action + ' command requires one or two parameters' if args.length < 1 || args.length > 2 delete_note(args.first.to_i, args[1]) when 'list' list(nil, args) when 'show' raise action + ' command requires exactly one parameter' if args.length != 1 show(args.first.to_i) when 'help' raise action + ' command has no parameters' if args.length > 0 puts usage when 'repl' raise action + ' command has no parameters' if args.length > 0 start_repl when 'cleanup' raise action + ' command requires at least one parameter' if args.empty? cleanup(args) else list(nil, arguments) end rescue StandardError => error puts "#{colorize('ERROR:', :red)} #{error}" end self end
Private Instance Methods
add(text)
click to toggle source
# File bin/todo.rb, line 237 def add(text) task = { state: 'new', title: text, modified: @today.strftime(DATE_FORMAT) } postprocess_tags(task) File.open(TODO_FILE, 'a:UTF-8') { |file| file.write(JSON.generate(task) + "\n") } list end
add_note(item, text)
click to toggle source
# File bin/todo.rb, line 341 def add_note(item, text) update_task(item, :show, lambda do |task| task[:note] ||= [] task[:note].push(text) end) end
append(item, text)
click to toggle source
# File bin/todo.rb, line 255 def append(item, text) update_task(item, :list, lambda do |task| task[:title] = [task[:title], text].join(' ') postprocess_tags(task) end) end
change_state(item, state, note = nil)
click to toggle source
# File bin/todo.rb, line 276 def change_state(item, state, note = nil) update_task(item, :list, lambda do |task| task[:state] = state if !note.nil? && !note.empty? task[:note] ||= [] task[:note].push(note) end end) end
cleanup(patterns)
click to toggle source
# File bin/todo.rb, line 383 def cleanup(patterns) tasks = load_tasks patterns = [':done'] + patterns.to_a items = filter_tasks(tasks, patterns) items.each_key { |num| tasks.delete(num) } write_tasks(tasks) puts "Deleted #{items.size} todo(s)" end
colorize(text, color)
click to toggle source
# File bin/todo.rb, line 401 def colorize(text, color) "\e[#{COLOR_CODES[color] || 37}m#{text}\e[0m" end
convert_due_date(date)
click to toggle source
# File bin/todo.rb, line 405 def convert_due_date(date) day_index = @due_date_days.index(date.to_s.downcase) || DUE_DATE_DAYS_SIMPLE.index(date.to_s.downcase) || @due_date_days.map { |day| day[0..2] }.index(date.to_s.downcase) return (@today + day_index).strftime(DATE_FORMAT) if day_index date.nil? || date.empty? ? nil : Date.strptime(date, DATE_FORMAT).strftime(DATE_FORMAT) end
delete(item)
click to toggle source
# File bin/todo.rb, line 269 def delete(item) tasks = load_tasks(item) tasks.delete(item) write_tasks(tasks) list end
delete_note(item, num = nil)
click to toggle source
# File bin/todo.rb, line 348 def delete_note(item, num = nil) update_task(item, :show, lambda do |task| if num.to_s.empty? task.delete(:note) else raise "#{num.to_i}: Note does not exist" if num.to_i <= 0 || task[:note].to_a.size < num.to_i task[:note].delete_at(num.to_i - 1) task.delete(:note) if task[:note].empty? end end) end
due_date(item, date = '')
click to toggle source
# File bin/todo.rb, line 297 def due_date(item, date = '') update_task(item, :list, lambda do |task| task[:due] = convert_due_date(date) task.delete(:due) if task[:due].nil? end) end
filter_tasks(tasks, patterns)
click to toggle source
# File bin/todo.rb, line 392 def filter_tasks(tasks, patterns) patterns = patterns.uniq tasks.select do |num, task| patterns.all? do |pattern| @queries[pattern] ? @queries[pattern].call(task) : /#{pattern}/ix.match(task[:title]) end end end
list(tasks = nil, patterns = nil)
click to toggle source
# File bin/todo.rb, line 304 def list(tasks = nil, patterns = nil) tasks ||= load_tasks task_indent = [tasks.keys.max.to_s.size, 4].max patterns ||= [] patterns += [':active'] if (patterns & [':active', ':done', ':blocked', ':started', ':new', ':all', ':waiting']).empty? items = filter_tasks(tasks, patterns).sort_by do |num, task| [ task[:priority] && task[:state] != 'done' ? 0 : 1, ORDER[task[:state] || 'default'] || ORDER['default'], task[:state] != 'done' ? task[:due] || 'n/a' : task[:modified], num ] end items.each do |num, task| state = task[:state] || 'default' display_state = colorize(STATES[state], COLORS[state]) title = task[:title].gsub(CONTEXT_TAG_PATTERN) do |tag| (tag.start_with?(' ') ? ' ' : '') + colorize(tag.strip, :cyan) end priority_flag = task[:priority] && state != 'done' ? colorize(PRIORITY_FLAG, :red) : ' ' due_date = '' if task[:due] && state != 'done' date_diff = (Date.strptime(task[:due], DATE_FORMAT) - @today).to_i if date_diff < 0 due_date = colorize("(#{date_diff.abs}d overdue)", :red) elsif date_diff == 0 || date_diff == 1 due_date = colorize("(#{DUE_DATE_DAYS_SIMPLE[date_diff]})", :yellow) else due_date = colorize("(#{@due_date_days[date_diff] || task[:due]})", :magenta) if date_diff > 1 end due_date = ' ' + due_date end puts "#{num.to_s.rjust(task_indent)}:#{priority_flag}#{display_state} #{title}#{due_date}" end puts 'No todos found' if items.empty? end
load_tasks(item_to_check = nil)
click to toggle source
# File bin/todo.rb, line 206 def load_tasks(item_to_check = nil) count = 0 tasks = {} if File.exist?(TODO_FILE) File.open(TODO_FILE, 'r:UTF-8') do |file| file.each_line do |line| next if line.strip == '' count += 1 tasks[count] = JSON.parse(line.chomp, :symbolize_names => true) end end end raise "#{item_to_check}: No such todo" if item_to_check && !tasks.has_key?(item_to_check) tasks end
rename(item, text)
click to toggle source
# File bin/todo.rb, line 262 def rename(item, text) update_task(item, :list, lambda do |task| task[:title] = text postprocess_tags(task) end) end
set_priority(item, note = nil)
click to toggle source
# File bin/todo.rb, line 286 def set_priority(item, note = nil) update_task(item, :list, lambda do |task| task[:priority] = !task[:priority] task.delete(:priority) if !task[:priority] if !note.nil? && !note.empty? task[:note] ||= [] task[:note].push(note) end end) end
setup()
click to toggle source
# File bin/todo.rb, line 181 def setup @today = Date.today next_7_days = (0..6).map { |day| @today + day } @due_date_days = next_7_days.map { |day| day.strftime('%A').downcase } due_dates_for_queries = next_7_days.map { |day| day.strftime(DATE_FORMAT) } recent_date = (@today - 7).strftime(DATE_FORMAT) @queries = { ':active' => lambda { |task| /(new|started|blocked|waiting)/.match(task[:state]) }, ':done' => lambda { |task| 'done' == task[:state] }, ':blocked' => lambda { |task| 'blocked' == task[:state] }, ':waiting' => lambda { |task| 'waiting' == task[:state] }, ':started' => lambda { |task| 'started' == task[:state] }, ':new' => lambda { |task| 'new' == task[:state] }, ':all' => lambda { |task| /\w+/.match(task[:state]) }, ':priority' => lambda { |task| task[:priority] }, ':note' => lambda { |task| task[:note] && !task[:note].empty? }, ':today' => lambda { |task| due_dates_for_queries[0] == task[:due] }, ':tomorrow' => lambda { |task| due_dates_for_queries[1] == task[:due] }, ':next7days' => lambda { |task| /(#{due_dates_for_queries.join('|')})/.match(task[:due]) }, ':overdue' => lambda { |task| task[:due] && task[:due] < due_dates_for_queries[0] }, ':due' => lambda { |task| task[:due] }, ':recent' => lambda { |task| recent_date <= task[:modified] } } end
show(item, tasks = nil)
click to toggle source
# File bin/todo.rb, line 360 def show(item, tasks = nil) tasks ||= load_tasks(item) tasks[item].each do |k, v| v = "\n" + v.each_with_index. map { |n, i| v.size > 1 ? "#{(i + 1).to_s.rjust(v.size.to_s.size)}: #{n}" : n }. join("\n") if v.is_a?(Array) puts "#{colorize(k.to_s.rjust(10) + ':', :cyan)} #{v}" end end
start_repl()
click to toggle source
# File bin/todo.rb, line 370 def start_repl command = '' while !['exit', 'quit'].include?(command) if ['clear', 'cls'].include?(command) print "\e[H\e[2J" else execute(command == 'repl' ? [] : command.split(/\s+/)) end print "\ntodo> " command = STDIN.gets.chomp.strip end end
update_task(item, post_action, update_function)
click to toggle source
# File bin/todo.rb, line 244 def update_task(item, post_action, update_function) tasks = load_tasks(item) update_function.call(tasks[item]) tasks[item][:modified] = @today.strftime(DATE_FORMAT) write_tasks(tasks) case post_action when :show then show(item, tasks) when :list then list(tasks) end end
usage()
click to toggle source
# File bin/todo.rb, line 142 def usage <<~USAGE Usage: todo <command> <arguments> Commands: * add <text> add new task * start <tasknumber> [text] mark task as started, with optional note * done <tasknumber> [text] mark task as completed, with optional note * block <tasknumber> [text] mark task as blocked, with optional note * wait <tasknumber> [text] mark task as waiting, with optional note * reset <tasknumber> [text] reset task to new state, with optional note * prio <tasknumber> [text] toggle high priority flag, with optional note * due <tasknumber> [date] set/unset due date (in YYYY-MM-DD format) * append <tasknumber> <text> append text to task title * rename <tasknumber> <text> rename task * del <tasknumber> delete task * note <tasknumber> <text> add note to task * delnote <tasknumber> [number] delete a specific or all notes from task * list <regex> [regex...] list tasks (only active tasks by default) * show <tasknumber> show all task details * repl enter read-eval-print loop mode * cleanup <regex> [regex...] cleanup completed tasks by regex * help this help screen With list command the following pre-defined queries can be also used: #{@queries.keys.each_with_index.map { |k, i| (i == 8 ? "\n" : '') + k }.join(', ')} Due dates can be also added via tags in task title: "due:YYYY-MM-DD" In addition to formatted dates, you can use date synonyms: "due:today", "due:tomorrow", and day names e.g. "due:monday" or "due:tue" Legend: #{STATES.select { |k, v| k != 'default' }.map { |k, v| "#{k} #{v}" }.join(', ') }, priority #{PRIORITY_FLAG} Todo file: #{TODO_FILE} USAGE end
write_tasks(tasks)
click to toggle source
# File bin/todo.rb, line 222 def write_tasks(tasks) File.open(TODO_FILE, 'w:UTF-8') do |file| tasks.keys.sort.each { |key| file.write(JSON.generate(tasks[key]) + "\n") } end end