class Rodo
Constants
- ESCDELAY
- VERSION
Attributes
current_day_index[RW]
cursor_line[RW]
cursor_x[RW]
debug[RW]
file_name[RW]
journal[RW]
mode[RW]
newly_added_line[RW]
win1[RW]
win1b[RW]
Public Instance Methods
build_windows()
click to toggle source
# File lib/rodo.rb, line 126 def build_windows @win1.close if @win1 if Curses.debug_win Curses.debug_win.close Curses.debug_win = nil end # Building a static window @win1 = Curses::Window.new(Curses.lines, Curses.cols / (@debug ? 2 : 1), 0, 0) @win1.keypad = true @win1b = @win1.subwin(@win1.maxy - 2, @win1.maxx - 3, 1, 2) if @debug debug_win = Curses::Window.new(Curses.lines, Curses.cols / 2, 0, Curses.cols / 2) debug_win.box debug_win.caption "Debug information" debug_win.refresh Curses.debug_win = debug_win.inset end end
get_cmd(win, pos_y, pos_x, command_prototyp_list)
click to toggle source
# File lib/rodo.rb, line 148 def get_cmd(win, pos_y, pos_x, command_prototyp_list) size_y = 10 size_x = [win.maxx - win.begx - 2 * pos_x - 2, 10].max cmd_win = Curses::Window.new(size_y + 2, size_x + 2, win.begy + pos_y, win.begx + pos_x) cmd_win.keypad = true cmd_win.box cmd_win.refresh inset = cmd_win.inset(1) inset.keypad = true result = inset.gets("> ".dup) { |s| if s =~ /^\s*\>\s*(.*)$/ cmd = $1 # Remove prompt list = command_prototyp_list.dup if cmd.strip != "" list.select! { |c| if c.instance_of? Hash if c.has_key?(:regex) c[:regex] =~ cmd elsif c.has_key?(:description) c[:description].include?(cmd) else true end else c.include?(cmd) end } end #Curses.debug_win.puts cmd #Curses.debug_win.puts list.inspect #Curses.debug_win.refresh list.push "No matching commands" if list.empty? list.each { |c| if c.instance_of? Hash if c.has_key?(:description) inset.puts c[:description] end else inset.puts c end } end } # Curses.debug_win.puts "Result: #{result}" return result end
init()
click to toggle source
# File lib/rodo.rb, line 44 def init Curses.ESCDELAY = 50 # 50 milliseconds (ESC is always a key, never a sequence until automatic) Curses.raw Curses.noecho # Don't print user input Curses.nonl Curses.stdscr.keypad = true # Control keys should be returned as keycodes Curses.init_screen Curses.curs_set(0) # Invisible cursor Curses.bracketed_paste # From CursesUtils: Enable Bracketed Paste Mode if !Curses.has_colors? Curses.abort "Curses doesn't have color support in this TTY." else Curses.start_color Curses.colors.times { |i| Curses.init_pair(i, i, 0) } end # If no file is given, assume the user wants to edit "~/plan.md" if ARGV.empty? @file_name = "plan.md" Dir.chdir Dir.home else @file_name = ARGV[0] end File.write(@file_name, "# #{Time.now.strftime("%Y-%m-%d")}\n") if !File.exist?(@file_name) @journal = Journal.from_s(File.read(@file_name)) @cursor = Cursor.new(@journal) @cursor.day = @journal.most_recent_index @mode = :scroll @newly_added_line = nil @debug = $DEBUG || $VERBOSE # || ENV["BUNDLE_BIN_PATH"] end
main_loop()
click to toggle source
# File lib/rodo.rb, line 81 def main_loop status = init() return if status == :close begin build_windows() loop do render_windows() char = @win1::get_char3 if char == Curses::KEY_RESIZE sleep(0.5) build_windows next end case char in paste: process_paste(paste) else status = process_input(char) return if status == :close end end ensure Curses.close_screen end end
postpone(lines, current_day, n)
click to toggle source
# File lib/rodo.rb, line 807 def postpone(lines, current_day, n) target_day = @journal.postpone_line(current_day, @cursor.line, n) # Adjust @cursor.day if a new day was created @cursor.day += 1 if current_day != @journal.days[@cursor.day] end
process_input(char)
click to toggle source
# File lib/rodo.rb, line 322 def process_input(char) current_day = @journal.days[@cursor.day] lines = current_day.lines case @mode when :journalling case char when CTRLC, CTRLC.chr return :close when Curses::KEY_UP @cursor.line -= 1 if @cursor.line > 0 when Curses::KEY_DOWN @cursor.line += 1 if @cursor.line < lines.size - 1 when "\u0001" # CTRL+A @cursor.x = 0 when "\u0005" # CTRL+E @cursor.x = lines[@cursor.line].length when Curses::KEY_LEFT if @cursor.x == 0 if @cursor.line > 0 @cursor.line -= 1 @cursor.x = lines[@cursor.line].length end else @cursor.x -= 1 if @cursor.x > 0 end when Curses::KEY_RIGHT if @cursor.x >= lines[@cursor.line].length if @cursor.line < lines.size - 1 @cursor.line += 1 @cursor.x = 0 end else @cursor.x += 1 if @cursor.x < lines[@cursor.line].length end when Curses::KEY_CTRL_LEFT if @cursor.x == 0 if @cursor.line > 0 @cursor.line -= 1 @cursor.x = lines[@cursor.line].length end end left = lines[@cursor.line][0...@cursor.x] if /\b?((^|\S+)\s*)$/ =~ left @cursor.x -= $1.length end when Curses::KEY_CTRL_RIGHT if @cursor.x >= lines[@cursor.line].length - 1 if @cursor.line < lines.size - 1 @cursor.line += 1 @cursor.x = 0 end end right = lines[@cursor.line][@cursor.x..-1] if /^(\s*\S+\b?\s*)/ =~ right @cursor.x += $1.length end when Curses::KEY_DC, "\u0004" # DEL, CTRL+D if @cursor.x >= lines[@cursor.line].size if @cursor.line < lines.size - 1 lines[@cursor.line] += lines[@cursor.line + 1] lines.delete_at(@cursor.line + 1) end else lines[@cursor.line].slice!(@cursor.x) end win1b.clear when Curses::KEY_BACKSPACE if @cursor.x == 0 && @cursor.line > 0 @cursor.line -= 1 @cursor.x = lines[@cursor.line].length lines[@cursor.line] += lines[@cursor.line + 1] lines.delete_at(@cursor.line + 1) elsif @cursor.x > 0 lines[@cursor.line].slice!(@cursor.x - 1) @cursor.x -= 1 end when "\v" # CTRL K if lines.size > 1 lines.delete_at(@cursor.line) @cursor.line -= 1 if @cursor.line >= lines.size else lines[0] = "".dup end @win1b.clear when ENTER, ENTER.chr left = lines[@cursor.line][0...@cursor.x] right = lines[@cursor.line][@cursor.x..-1] # If line to the left of cursor starts with "- [ ]" or with a star or dash if /^(?<lead>\s+[*-])(?<option>\s\[.\]\s?)?(?<rest>.*?)$/ =~ left && !(right =~ /^\s+[*-]/) if rest.strip.length == 0 and right.strip.length == 0 # line is empty, except for */-/[ ] right = nil # unindent if /^\s\s(?<lead2>.*)$/ =~ lead lead = lead2 else lead = "" option = "" end left = lead + option else if option =~ /^\s\[.\]/ option = " [ ]" end end option = "" if !option lead = lead + option.rstrip + " " right = lead + right.lstrip if right @cursor.x = lead.length else @cursor.x = 0 end lines[@cursor.line] = left if right != nil lines.insert(@cursor.line + 1, right) @cursor.line += 1 end when ESC, ESC.chr @mode = :scroll @journal.days[@cursor.day] = TodoDay.new(lines) # Reparse day after edit when /[[:print:]]/ lines[@cursor.line].insert(@cursor.x, char) @cursor.x += 1 when "\t", "\t".ord if lines[@cursor.line] =~ /\s*[-+*]/ lines[@cursor.line].sub!(/^/, " ") @cursor.x += 2 end when Curses::KEY_BTAB if lines[@cursor.line] =~ /( |\t)\s*[-+*]/ lines[@cursor.line].sub!(/^( |\t)/, "") @cursor.x -= 2 @cursor.x = 0 if @cursor.x < 0 end else if Curses.debug_win Curses.debug_win.puts "Char not handled: " + Curses::char_info(char) Curses.debug_win.refresh end end when :edit case char when CTRLC, CTRLC.chr return :close when "\u0001" # CTRL+A @cursor.x = 0 when "\u0005" # CTRL+E @cursor.x = lines[@cursor.line].length when Curses::KEY_LEFT @cursor.x -= 1 if @cursor.x > 0 when Curses::KEY_RIGHT @cursor.x += 1 if @cursor.x < lines[@cursor.line].length when Curses::KEY_CTRL_LEFT left = lines[@cursor.line][0...@cursor.x] if /((^|\w+?|\W+?)\s*)$/ =~ left @cursor.x -= $1.length end when Curses::KEY_CTRL_RIGHT right = lines[@cursor.line][@cursor.x..-1] if /^(\s*|\S+\s*)/ =~ right @cursor.x += $1.length end when Curses::KEY_DC, "\u0004" # DEL, CTRL+D if @cursor.x < lines[@cursor.line].size lines[@cursor.line].slice!(@cursor.x) end when Curses::KEY_BACKSPACE if @cursor.x > 0 lines[@cursor.line].slice!(@cursor.x - 1) @cursor.x -= 1 end when ENTER, ENTER.chr @mode = :scroll @newly_added_line = nil @journal.days[@cursor.day] = TodoDay.new(lines) # Reparse day after edit when ESC, ESC.chr # When pressing ESC after an insert, which didn't change anything then undo the insertion if @newly_added_line && lines[@cursor.line] == @newly_added_line lines.delete_at(@cursor.line) @cursor.line -= 1 if @cursor.line >= lines.size @win1b.clear end # Debug: # Curses.debug "Lines[@cursor.line] == #{lines[@cursor.line].inspect }, @newly_added_line == #{@newly_added_line.inspect}" @mode = :scroll @newly_added_line = nil @journal.days[@cursor.day] = TodoDay.new(lines) # Reparse day after edit when /[[:print:]]/ lines[@cursor.line].insert(@cursor.x, char) # Curses.setpos(@cursor.line + 2, lines[@cursor.line].length + 2) @cursor.x += 1 when "\t", "\t".ord if lines[@cursor.line] =~ /\s*[-+*]/ lines[@cursor.line].sub!(/^/, " ") @cursor.x += 2 end when Curses::KEY_BTAB if lines[@cursor.line] =~ /( |\t)\s*[-+*]/ lines[@cursor.line].sub!(/^( |\t)/, "") @cursor.x -= 2 @cursor.x = 0 if @cursor.x < 0 end else if Curses.debug_win Curses.debug_win.puts "Char not handled: " + Curses::char_info(char) Curses.debug_win.refresh end end when :scroll case char when 'q' return self.save when CTRLC, CTRLC.chr return :close when '~' @debug = !@debug build_windows when Curses::KEY_UP @cursor.line -= 1 if @cursor.line > 0 when Curses::KEY_DOWN @cursor.line += 1 if @cursor.line < lines.size - 1 when Curses::KEY_RIGHT then @cursor.day -= 1 if @cursor.day > 0 @win1b.clear when Curses::KEY_LEFT then @cursor.day += 1 if @cursor.day < @journal.days.size - 1 @win1b.clear when "\t", "\t".ord if lines[@cursor.line] =~ /\s*[-+*]/ lines[@cursor.line].sub!(/^/, " ") end when Curses::KEY_F1 cmd = get_cmd(@win1b, 1, 1, @command_prototyp_list) if cmd == :close # do nothing, because user closed window elsif cmd =~ /^\s*>\s*(.*)$/ cmd = $1 # Remove prompt # Search list of available commands for a match and run the cmd @command_prototyp_list.find { |command_prototype| case command_prototype in regex: r, do_cmd: c if r =~ cmd c.call(cmd, lines, current_day) next true else next false end in description: d if d.start_with? cmd.strip Curses.unget_char(cmd.strip) next true end next false in String if command_prototype.start_with? cmd.strip Curses.unget_char(cmd.strip) next true end next false else if Curses.debug_win Curses.debug_win.puts "Unknown command enter from F1: #{cmd}" Curses.debug_win.refresh end end } end when Curses::KEY_BTAB if lines[@cursor.line] =~ /( |\t)\s*[-+*]/ lines[@cursor.line].sub!(/^( |\t)/, "") end #when Curses::KEY_BACKSPACE then # buffer.remove_char_before_cursor #when ENTER then buffer.new_line when '.', 'x' if lines[@cursor.line] =~ /\[\s\]/ lines[@cursor.line].gsub!(/\[\s\]/, "[x]") elsif lines[@cursor.line] =~ /\[[xX]\]/ lines[@cursor.line].gsub!(/\[[xX]\]/, "[ ]") end #when /[[:print:]]/ then buffer.add_char(char) when '2' # ★ when 'e' # Edit @mode = :journalling @cursor.x = lines[@cursor.line].length when 'm' # Move @mode = :move when 'i' # Insert @cursor.line = 1 if @cursor.line == 0 @newly_added_line = current_day.line_prototype(@cursor.line) lines.insert(@cursor.line, @newly_added_line.dup) @mode = :edit @cursor.x = lines[@cursor.line].size when ENTER, ENTER.chr @mode = :edit @cursor.x = lines[@cursor.line].size when 'a' # append @newly_added_line = current_day.line_prototype(@cursor.line) @cursor.line += 1 lines.insert(@cursor.line, @newly_added_line.dup) @mode = :edit @cursor.x = lines[@cursor.line].size when 't' # t(oday) @cursor.day = @journal.close(current_day) @current_day = @journal.days[@cursor.day] @win1b.clear when 'k' # kill if lines.size > 1 lines.delete_at(@cursor.line) @cursor.line -= 1 if @cursor.line >= lines.size else lines[0] = "".dup end @win1b.clear when 'w' # waiting if lines[@cursor.line] =~ /\[\s\]/ line_to_wait_for = lines[@cursor.line].dup if !(line_to_wait_for =~ / - ⌛ since \d\d\d\d-\d\d-\d\d$/) && current_day.date line_to_wait_for = line_to_wait_for.rstrip + " - ⌛ since #{current_day.date_name}" end # Get target day (and create if it doesn't exist) and add there postpone_day = @journal.postpone_day(current_day, 7) postpone_day.lines.insert(1, line_to_wait_for) # Adjust @cursor.day if a new day was created @cursor.day += 1 if current_day != @journal.days[@cursor.day] # Add hourclass here lines[@cursor.line].gsub!(/\[\s\]/, "[⌛]") end when 'p' # postpone postpone(lines, current_day, 1) else if Curses.debug_win Curses.debug_win.puts "Char not handled: " + Curses::char_info(char) Curses.debug_win.refresh end end when :move case char when 'q' return self.save when CTRLC, CTRLC.chr return :close when Curses::KEY_UP if @cursor.line > 0 lines[@cursor.line], lines[@cursor.line - 1] = lines[@cursor.line - 1], lines[@cursor.line] @cursor.line -= 1 end when Curses::KEY_DOWN if @cursor.line < lines.size - 1 lines[@cursor.line], lines[@cursor.line + 1] = lines[@cursor.line + 1], lines[@cursor.line] @cursor.line += 1 end when "\t", "\t".ord, Curses::KEY_RIGHT if lines[@cursor.line] =~ /\s*[-+*]/ lines[@cursor.line].sub!(/^/, " ") end when Curses::KEY_BTAB, Curses::KEY_LEFT if lines[@cursor.line] =~ /( |\t)\s*[-+*]/ lines[@cursor.line].sub!(/^( |\t)/, "") end when ENTER, ENTER.chr, ESC, ESC.chr @mode = :scroll else Curses.debug "Char not handled: " + Curses::char_info(char) end else Curses.abort "Mode #{@mode} not handled" end return nil end
process_paste(pasted)
click to toggle source
# File lib/rodo.rb, line 269 def process_paste(pasted) pasted.gsub! /\r\n?/, "\n" # Curses.debug "Pasted: #{pasted.inspect}" current_day = @journal.days[@cursor.day] lines = current_day.lines # Todo Clean-up Pasted Special characters (bullets) pasted.gsub! /^\t/, " " # Replace initial tabs with 1 space pasted.gsub! /\t/, " " # Replace other tabs with 2 spaces pasted.gsub! /^(\s*)[•□○®◊§] /, "\\1- " case @mode when :journalling, :edit # When in journalling or edit mode, then pasting will split the current line pasted_lines = pasted.lines left = lines[@cursor.line][0...@cursor.x] right = lines[@cursor.line][@cursor.x..-1] pasted_lines.each_with_index { |line, i| # On the first line, append to text left of cursor if i == 0 lines[@cursor.line] = left + line else lines.insert(@cursor.line, line) end # On the last line, append existing text right of cursor if i == pasted_lines.size - 1 @cursor.x = lines[@cursor.line].length lines[@cursor.line] += right else @cursor.line += 1 end } when :scroll, :move # Insert each line after the current line pasted.each_line { |l| @cursor.line += 1 lines.insert(@cursor.line, l) } @cursor.x = lines[@cursor.line].size else Curses.abort("Case not handled: #{@mode}"); end end
render_windows()
click to toggle source
# File lib/rodo.rb, line 201 def render_windows current_day = @journal.days[@cursor.day] @win1.box if Curses.debug_win Curses.debug_win.setpos(0, 0) Curses.debug_win.puts "Cursor @ #{@cursor.line}" Curses.debug_win.puts "Mode: #{@mode}" Curses.debug_win.refresh end # Next / prev Navigation has_next_day = @cursor.day > 0 has_prev_day = @cursor.day < @journal.days.size - 1 nav_str = [] nav_str << "❮ #{@journal.days[@cursor.day + 1].date_name}" if has_prev_day nav_str << "#{@journal.days[@cursor.day - 1].date_name} ❯" if has_next_day nav_str = nav_str.join(" ") @win1b.setpos(0, @win1b.maxx - nav_str.length - 1) @win1b.addstr(nav_str) # Contents of current day: lines = current_day.lines overflows = 0 lines.each_with_index do |line, i| Curses.abort("Line #{i} is nil") if line == nil @win1b.setpos(i + overflows + 1, 0) if @cursor.line == i @win1b.attron(Curses.color_pair(255)) else @win1b.attron(Curses.color_pair(246)) end if (%i[scroll move].include?(@mode) && (@cursor.line != i || line.size != 0)) || (%i[edit journalling].include?(@mode) && @cursor.line != i) then @win1b.puts line else line = line + " " if Curses.debug_win Curses.debug_win.puts "Before Cursor: '#{line[...@cursor.x]}'" Curses.debug_win.puts "Cursor_x: '#{@cursor.x}'" Curses.debug_win.refresh end @cursor.x = 0 if ![:edit, :journalling].include?(@mode) @cursor.x = line.size - 1 if @cursor.x >= line.size @win1b.addstr line[...@cursor.x] if @cursor.x > 0 @win1b.attron(Curses::A_REVERSE) @win1b.addstr line[@cursor.x] @win1b.attroff(Curses::A_REVERSE) @win1b.addstr(line[(@cursor.x+1)..]) if @cursor.x < line.size @win1b.addstr("\n") end overflows += line.length / @win1b.maxx end # At the end switch color to normal again @win1b.attron(Curses.color_pair(246)) end
save()
click to toggle source
# File lib/rodo.rb, line 115 def save FileUtils.mkdir_p "_bak" FileUtils.cp(@file_name, File.join( File.dirname(@file_name), "_bak", File.basename(@file_name) + "-#{Time.now.strftime("%Y-%m-%dT%H-%M-%S")}.bak")) File.write(@file_name, @journal.to_s) return :close end