todo-curses 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "TodoCurses"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/bin/todo-curses CHANGED
@@ -1,47 +1,44 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'todo'
4
- include GLI::App
5
- include Ncurses
6
- include Ncurses::Form
7
-
8
- program_desc 'A simple interface for managing todo.txt files.'
9
-
10
- subcommand_option_handling :normal
11
- arguments :strict
12
-
13
- desc 'Starts an interactive editor for the given todo.txt list.'
14
- arg_name 'Describe arguments to list here'
15
- command :list do |c|
16
- c.action do |global_options,options,args|
17
- if args.size != 1 then
18
- printf("usage: #{$0} file\n");
19
- exit
20
- end
21
-
22
- TodoViewer.new(ARGV[0])
3
+ require 'optparse'
4
+ require 'methadone'
5
+ require 'TodoCurses'
6
+
7
+ class App
8
+ include Methadone::Main
9
+ include Methadone::CLILogging
10
+ include TodoCurses
11
+
12
+ main do |todo_path|
13
+ # your program code here
14
+ # You can access CLI options via
15
+ # the options Hash
23
16
  end
24
- end
25
17
 
26
- pre do |global,command,options,args|
27
- # Pre logic here
28
- # Return true to proceed; false to abort and not call the
29
- # chosen command
30
- # Use skips_pre before a command to skip this block
31
- # on that command only
32
- true
33
- end
18
+ # supplemental methods here
34
19
 
35
- post do |global,command,options,args|
36
- # Post logic here
37
- # Use skips_post before a command to skip this
38
- # block on that command only
39
- end
20
+ # Declare command-line interface here
40
21
 
41
- on_error do |exception|
42
- # Error logic here
43
- # return false to skip default error handling
44
- true
45
- end
22
+ # description "one line description of your app"
23
+ #
24
+ # Accept flags via:
25
+ # on("--flag VAL","Some flag")
26
+ # options[flag] will contain VAL
27
+ #
28
+ # Specify switches via:
29
+ # on("--[no-]switch","Some switch")
30
+ #
31
+ # Or, just call OptionParser methods on opts
32
+ #
33
+ # Require an argument
34
+ # arg :some_arg
35
+ #
36
+ # # Make an argument optional
37
+ # arg :optional_arg, :optional
46
38
 
47
- exit run(ARGV)
39
+ version TodoCurses::VERSION
40
+
41
+ use_log_level_option :toggle_debug_on_signal => 'USR1'
42
+
43
+ go!
44
+ end
Binary file
data/lib/TodoCurses.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'TodoCurses/list.rb'
2
+ require 'TodoCurses/logger.rb'
3
+ require 'TodoCurses/view.rb'
4
+ require 'TodoCurses/version.rb'
5
+ require 'TodoCurses/task.rb'
6
+
7
+ module TodoCurses
8
+ include Logger
9
+
10
+ if ARGV.size != 1
11
+ printf("usage: #{$PROGRAM_NAME} file\n")
12
+ exit
13
+ end
14
+
15
+ View.new ARGV[0]
16
+ end
@@ -1,4 +1,5 @@
1
- module Todo
1
+ module TodoCurses
2
+ # Handles todo.txt list files
2
3
  class List < Array
3
4
  # Initializes a Todo List object with a path to the corresponding todo.txt
4
5
  # file. For example, if your todo.txt file is located at:
@@ -7,7 +8,7 @@ module Todo
7
8
  #
8
9
  # You would initialize this object like:
9
10
  #
10
- # list = Todo::List.new "/home/sam/Dropbox/todo/todo-txt"
11
+ # list = TodoCurses::List.new "/home/sam/Dropbox/todo/todo-txt"
11
12
  #
12
13
  # Alternately, you can initialize this object with an array of strings or
13
14
  # tasks. If the array is of strings, the strings will be converted into
@@ -17,10 +18,10 @@ module Todo
17
18
  #
18
19
  # array = Array.new
19
20
  # array.push "(A) A string task!"
20
- # array.push Todo::Task.new("(A) An actual task!")
21
+ # array.push TodoCurses::Task.new("(A) An actual task!")
21
22
  #
22
- # list = Todo::List.new array
23
- def initialize list
23
+ # list = TodoCurses::List.new array
24
+ def initialize(list)
24
25
  if list.is_a? Array
25
26
  # No file path was given.
26
27
  @path = nil
@@ -29,25 +30,25 @@ module Todo
29
30
  list.each do |task|
30
31
  # If it's a string, make a new task out of it.
31
32
  if task.is_a? String
32
- self.push Todo::Task.new task
33
+ push TodoCurses::Task.new task
33
34
  # If it's a task, just add it.
34
- elsif task.is_a? Todo::Task
35
- self.push task
35
+ elsif task.is_a? TodoCurses::Task
36
+ push task
36
37
  end
37
38
  end
38
39
  elsif list.is_a? String
39
40
  @path = list
40
41
 
41
- # Read in lines from file, create Todo::Tasks out of them and push them
42
+ # Read in lines from file, create TodoCurses::Tasks out of them and push them
42
43
  # onto self.
43
44
  File.open(list) do |file|
44
- file.each_line { |line| self.push Todo::Task.new line }
45
+ file.each_line { |line| push TodoCurses::Task.new line }
45
46
  end
46
47
  end
47
48
  end
48
49
 
49
50
  # The path to the todo.txt file that you supplied when you created the
50
- # Todo::List object.
51
+ # TodoCurses::List object.
51
52
  def path
52
53
  @path
53
54
  end
@@ -56,32 +57,32 @@ module Todo
56
57
  #
57
58
  # Example:
58
59
  #
59
- # list = Todo::List.new "/path/to/list"
60
+ # list = TodoCurses::List.new "/path/to/list"
60
61
  # list.by_priority "A" #=> Will be a new list with only priority A tasks
61
- def by_priority priority
62
- Todo::List.new self.select { |task| task.priority == priority }
62
+ def by_priority(priority)
63
+ TodoCurses::List.new select { |task| task.priority == priority }
63
64
  end
64
65
 
65
66
  # Filters the list by context and returns a new list.
66
67
  #
67
68
  # Example:
68
69
  #
69
- # list = Todo::List.new "/path/to/list"
70
+ # list = TodoCurses::List.new "/path/to/list"
70
71
  # list.by_context "@context" #=> Will be a new list with only tasks
71
72
  # containing "@context"
72
- def by_context context
73
- Todo::List.new self.select { |task| task.contexts.include? context }
73
+ def by_context(context)
74
+ TodoCurses::List.new select { |task| task.contexts.include? context }
74
75
  end
75
76
 
76
77
  # Filters the list by project and returns a new list.
77
78
  #
78
79
  # Example:
79
80
  #
80
- # list = Todo::List.new "/path/to/list"
81
+ # list = TodoCurses::List.new "/path/to/list"
81
82
  # list.by_project "+project" #=> Will be a new list with only tasks
82
83
  # containing "+project"
83
- def by_project project
84
- Todo::List.new self.select { |task| task.projects.include? project }
84
+ def by_project(project)
85
+ TodoCurses::List.new select { |task| task.projects.include? project }
85
86
  end
86
87
  end
87
88
  end
@@ -1,6 +1,6 @@
1
1
  require 'logger'
2
2
 
3
- module Todo
3
+ module TodoCurses
4
4
  module Logger
5
5
  def self.included base
6
6
  base.extend(self)
@@ -15,7 +15,7 @@ module Todo
15
15
  end
16
16
 
17
17
  def logger
18
- Todo::Logger.logger
18
+ TodoCurses::Logger.logger
19
19
  end
20
20
  end
21
21
  end
@@ -1,9 +1,9 @@
1
1
  require 'date'
2
2
 
3
- module Todo
3
+ module TodoCurses
4
4
  class Task
5
5
  include Comparable
6
- include Todo::Logger
6
+ include TodoCurses::Logger
7
7
 
8
8
  # The regular expression used to match contexts.
9
9
  def self.contexts_regex
@@ -40,7 +40,7 @@ module Todo
40
40
  #
41
41
  # Example:
42
42
  #
43
- # task = Todo::Task.new("(A) A high priority task!")
43
+ # task = TodoCurses::Task.new("(A) A high priority task!")
44
44
  def initialize task
45
45
  @orig = task
46
46
  @completed_on = get_completed_date #orig.scan(self.class.done_regex)[1] ||= nil
@@ -54,7 +54,7 @@ module Todo
54
54
  #
55
55
  # Example:
56
56
  #
57
- # task = Todo::Task.new "(A) @context +project Hello!"
57
+ # task = TodoCurses::Task.new "(A) @context +project Hello!"
58
58
  # task.orig #=> "(A) @context +project Hello!"
59
59
  attr_reader :orig
60
60
 
@@ -62,7 +62,7 @@ module Todo
62
62
  #
63
63
  # Example:
64
64
  #
65
- # task = Todo::Task.new "(A) 2012-03-04 Task."
65
+ # task = TodoCurses::Task.new "(A) 2012-03-04 Task."
66
66
  # task.created_on
67
67
  # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
68
68
  #
@@ -75,7 +75,7 @@ module Todo
75
75
  #
76
76
  # Example:
77
77
  #
78
- # task = Todo::Task.new "x 2012-03-04 Task."
78
+ # task = TodoCurses::Task.new "x 2012-03-04 Task."
79
79
  # task.completed_on
80
80
  # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
81
81
  #
@@ -88,7 +88,7 @@ module Todo
88
88
  #
89
89
  # Example:
90
90
  #
91
- # task = Todo::Task.new "(A) This is a task. due:2012-03-04"
91
+ # task = TodoCurses::Task.new "(A) This is a task. due:2012-03-04"
92
92
  # task.due_on
93
93
  # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
94
94
  #
@@ -101,10 +101,10 @@ module Todo
101
101
  #
102
102
  # Example:
103
103
  #
104
- # task = Todo::Task.new "(A) Some task."
104
+ # task = TodoCurses::Task.new "(A) Some task."
105
105
  # task.priority #=> "A"
106
106
  #
107
- # task = Todo::Task.new "Some task."
107
+ # task = TodoCurses::Task.new "Some task."
108
108
  # task.priority #=> nil
109
109
  attr_reader :priority
110
110
 
@@ -129,7 +129,7 @@ module Todo
129
129
  #
130
130
  # Example:
131
131
  #
132
- # task = Todo::Task.new "(A) @test Testing!"
132
+ # task = TodoCurses::Task.new "(A) @test Testing!"
133
133
  # task.text #=> "Testing!"
134
134
  def text
135
135
  @text ||= orig.
@@ -146,7 +146,7 @@ module Todo
146
146
  #
147
147
  # Example:
148
148
  #
149
- # task = Todo::Task.new "(A) 2012-03-04 Task."
149
+ # task = TodoCurses::Task.new "(A) 2012-03-04 Task."
150
150
  # task.date
151
151
  # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
152
152
  #
@@ -165,7 +165,7 @@ module Todo
165
165
  #
166
166
  # Example:
167
167
  #
168
- # task = Todo::Task.new("This task is overdue! due:#{Date.today - 1}")
168
+ # task = TodoCurses::Task.new("This task is overdue! due:#{Date.today - 1}")
169
169
  # task.overdue?
170
170
  # #=> true
171
171
  def overdue?
@@ -177,11 +177,11 @@ module Todo
177
177
  #
178
178
  # Example:
179
179
  #
180
- # task = Todo::Task.new "x 2012-12-08 Task."
180
+ # task = TodoCurses::Task.new "x 2012-12-08 Task."
181
181
  # task.done?
182
182
  # #=> true
183
183
  #
184
- # task = Todo::Task.new "Task."
184
+ # task = TodoCurses::Task.new "Task."
185
185
  # task.done?
186
186
  # #=> false
187
187
  def done?
@@ -192,7 +192,7 @@ module Todo
192
192
  #
193
193
  # Example:
194
194
  #
195
- # task = Todo::Task.new "2012-12-08 Task."
195
+ # task = TodoCurses::Task.new "2012-12-08 Task."
196
196
  # task.done?
197
197
  # #=> false
198
198
  #
@@ -212,7 +212,7 @@ module Todo
212
212
  #
213
213
  # Example:
214
214
  #
215
- # task = Todo::Task.new "x 2012-12-08 2012-03-04 Task."
215
+ # task = TodoCurses::Task.new "x 2012-12-08 2012-03-04 Task."
216
216
  # task.done?
217
217
  # #=> true
218
218
  #
@@ -232,7 +232,7 @@ module Todo
232
232
  #
233
233
  # Example:
234
234
  #
235
- # task = Todo::Task.new "x 2012-12-08 Task."
235
+ # task = TodoCurses::Task.new "x 2012-12-08 Task."
236
236
  # task.done?
237
237
  # #=> true
238
238
  #
@@ -251,7 +251,7 @@ module Todo
251
251
  #
252
252
  # Example:
253
253
  #
254
- # task = Todo::Task.new "(A) 2012-12-08 Task"
254
+ # task = TodoCurses::Task.new "(A) 2012-12-08 Task"
255
255
  # task.to_s
256
256
  # #=> "(A) 2012-12-08 Task"
257
257
  def to_s
@@ -268,8 +268,8 @@ module Todo
268
268
  #
269
269
  # Example:
270
270
  #
271
- # task1 = Todo::Task.new "(A) Priority A."
272
- # task2 = Todo::Task.new "(B) Priority B."
271
+ # task1 = TodoCurses::Task.new "(A) Priority A."
272
+ # task2 = TodoCurses::Task.new "(B) Priority B."
273
273
  #
274
274
  # task1 > task2
275
275
  # # => true
@@ -0,0 +1,3 @@
1
+ module TodoCurses
2
+ VERSION = '0.0.4'
3
+ end
@@ -0,0 +1,261 @@
1
+ require 'ncursesw'
2
+
3
+ module TodoCurses
4
+ include Ncurses
5
+
6
+ # A curses based todo.txt file viewer
7
+ class View
8
+ # Run the ncurses application
9
+ def interact
10
+ while true
11
+ result = true
12
+ c = Ncurses.getch
13
+ case c
14
+ when 'j'.ord # Move down
15
+ result = scroll_down
16
+ when 'k'.ord # Move up
17
+ result = scroll_up
18
+ when 'J'.ord # Decrease priority
19
+ result = priority_down
20
+ when 'K'.ord # Increase priority
21
+ result = priority_up
22
+ when 'x'.ord # Toggle complete
23
+ toggle_item_completion
24
+ when 'n'.ord # New item
25
+ new_item
26
+ when 'h'.ord # Scroll to top
27
+ while scroll_up
28
+ end
29
+ when 'l'.ord # Scroll to bottom
30
+ while scroll_down
31
+ end
32
+ when 'q'.ord
33
+ break
34
+ else
35
+ @screen.mvprintw(0, 0, "[unknown key `#{Ncurses.keyname(c)}'=#{c}] ")
36
+ end
37
+ Ncurses.beep unless result
38
+ end
39
+ clean_done_tasks
40
+ close_ncurses
41
+ end
42
+
43
+ private
44
+
45
+ # Create a new fileviewer, and view the file.
46
+ def initialize(filename)
47
+ init_curses
48
+ load_file(filename)
49
+ interact
50
+ end
51
+
52
+ # Perform the curses setup
53
+ def init_curses
54
+ @screen = Ncurses.initscr
55
+ Ncurses.nonl
56
+ Ncurses.cbreak
57
+ Ncurses.noecho
58
+ @screen.scrollok(true)
59
+ end
60
+
61
+ # Loads the given file as a todo.txt array. Sets the view to the top
62
+ # and redraws the list view.
63
+ # @param filename [String] path to the text file to be loaded
64
+ def load_file(filename)
65
+ @done_file = File.dirname(filename) + '/done.txt'
66
+ @list = TodoCurses::List.new filename
67
+ @list.sort! { |x, y| y <=> x } # Reverse sort
68
+ items = []
69
+ last_priority = nil
70
+ last_selection = @menu.current_item.user_object if @menu
71
+ current_selection = nil
72
+
73
+ # Build the menu item list
74
+ @list.each do |item|
75
+ # Insert dividers on priority change
76
+ if item.priority != last_priority
77
+ divider_priority = item.priority.nil? ? 'N/A' : item.priority.to_s
78
+ divider = Ncurses::Menu::ITEM.new(divider_priority, '')
79
+ items << divider
80
+ last_priority = item.priority
81
+ end
82
+
83
+ # Build the todo menu item
84
+ menu_item = Ncurses::Menu::ITEM.new(item.to_s, '') # name, description
85
+ menu_item.user_object = item
86
+ items << menu_item
87
+
88
+ # Set the current selection
89
+ current_selection = menu_item if item.to_s == last_selection.to_s
90
+ end
91
+
92
+ # Build the final menu object
93
+ @menu = Ncurses::Menu::MENU.new items
94
+ @menu.set_menu_win(@screen)
95
+ @menu.set_menu_sub(@screen.derwin(@screen.getmaxx, @screen.getmaxy, 0, 0))
96
+ @menu.set_menu_format(@screen.getmaxy, 1)
97
+
98
+ # Set dividers to non-interactive
99
+ @menu.menu_items.select { |i| i.user_object.nil? }.each do |divider|
100
+ divider.item_opts_off Ncurses::Menu::O_SELECTABLE
101
+ end
102
+
103
+ # Show the menu
104
+ @screen.clear
105
+ @menu.post_menu
106
+
107
+ # Set selection position
108
+ @menu.set_current_item current_selection if current_selection
109
+ @menu.menu_driver(
110
+ Ncurses::Menu::REQ_DOWN_ITEM) if @menu.current_item.user_object.nil?
111
+
112
+ # Refresh
113
+ @screen.refresh
114
+ end
115
+
116
+ # Moves the current selection's priority up by one unless it is at Z.
117
+ def priority_up
118
+ item = @menu.current_item.user_object
119
+ item.priority_inc
120
+ save_list
121
+ end
122
+
123
+ # Moves the current selection's priority down by one unless it is at A.
124
+ def priority_down
125
+ item = @menu.current_item.user_object
126
+ item.priority_dec
127
+ save_list
128
+ end
129
+
130
+ # Scroll the display up by one line
131
+ # @return [Boolean] true if the action completed successfully.
132
+ def scroll_up
133
+ # Move to the next item if it's not the first in the list
134
+ unless @menu.menu_items[0].user_object.nil? &&
135
+ @menu.current_item.item_index < 2
136
+ result = @menu.menu_driver(Ncurses::Menu::REQ_UP_ITEM)
137
+ end
138
+ # Move to the next item if it's not a divider
139
+ result = @menu.menu_driver(
140
+ Ncurses::Menu::REQ_UP_ITEM) unless @menu.current_item.user_object
141
+ return true if result == Ncurses::Menu::E_OK
142
+ false
143
+ end
144
+
145
+ # Scroll the display down by one line
146
+ # @return [Boolean] true if the action completed successfully.
147
+ def scroll_down
148
+ result = @menu.menu_driver(Ncurses::Menu::REQ_DOWN_ITEM)
149
+ result = @menu.menu_driver(
150
+ Ncurses::Menu::REQ_DOWN_ITEM) unless @menu.current_item.user_object
151
+ return true if result == Ncurses::Menu::E_OK
152
+ false
153
+ end
154
+
155
+ # Collects a new todo item from the user and saves
156
+ # it to the text file.
157
+ def new_item
158
+ field = Ncurses::Form::FIELD.new(1, @screen.getmaxx - 1, 2, 1, 0, 0)
159
+ field.set_field_back(Ncurses::A_UNDERLINE)
160
+ fields = [field]
161
+ my_form = Ncurses::Form::FORM.new(fields)
162
+ my_form.user_object = 'My identifier'
163
+
164
+ # Calculate the area required for the form
165
+ rows = []
166
+ cols = []
167
+ my_form.scale_form(rows, cols)
168
+
169
+ # Create the window to be associated with the form
170
+ my_form_win = Ncurses::WINDOW.new(rows[0] + 3, cols[0] + 14, 1, 1)
171
+ my_form_win.keypad(TRUE)
172
+
173
+ # Set main window and sub window
174
+ my_form.set_form_win(my_form_win)
175
+ my_form.set_form_sub(my_form_win.derwin(rows[0], cols[0], 2, 12))
176
+
177
+ my_form.post_form
178
+
179
+ # Print field types
180
+ my_form_win.mvaddstr(4, 2, 'New item')
181
+ my_form_win.wrefresh
182
+
183
+ Ncurses::stdscr.refresh
184
+
185
+ new_item_text = capture_text_field_input(my_form_win, my_form, field)
186
+
187
+ # Save results
188
+ save_new_item(new_item_text)
189
+
190
+ # Clean up
191
+ my_form.unpost_form
192
+ my_form.free_form
193
+
194
+ field.free_field
195
+ # fields.each {|f| f.free_field}
196
+ end
197
+
198
+ # Adds a new item to the list and saves the file
199
+ # @param task [String] the task to be added
200
+ # @return [TodoCurses::List] the updated list
201
+ def save_new_item(task)
202
+ @list << TodoCurses::Task.new(task)
203
+ save_list
204
+ @list
205
+ end
206
+
207
+ # Saves the current state of the list. Overrides the current file.
208
+ # Reloads the newly saved file.
209
+ def save_list
210
+ File.open(@list.path, 'w') { |file| file << @list.join("\n") }
211
+ load_file @list.path
212
+ end
213
+
214
+ # Marks the currently selected menu item as complete and saves the list.
215
+ def toggle_item_completion
216
+ @menu.current_item.user_object.toggle!
217
+ save_list
218
+ end
219
+
220
+ # Saves done tasks to done.txt and removes them from todo.txt
221
+ def clean_done_tasks
222
+ done_tasks = @list.select { |task| !task.completed_on.nil? }
223
+ File.open(@done_file, 'a') do |file|
224
+ file << "\n"
225
+ file << done_tasks.join("\n")
226
+ end
227
+ remaining_tasks = @list.select { |task| task.completed_on.nil? }
228
+ File.open(@list.path, 'w') { |file| file << remaining_tasks.join("\n") }
229
+ end
230
+
231
+ # put the screen back in its normal state
232
+ def close_ncurses
233
+ Ncurses.echo
234
+ Ncurses.nocbreak
235
+ Ncurses.nl
236
+ Ncurses.endwin
237
+ end
238
+
239
+ # Captures text input into a form and returns the resulting string.
240
+ # @param window [Window] the form window
241
+ # @param form [FORM] the form to be captured
242
+ # @param field [FIELD] the form to be captured
243
+ # @return [String] the captured input
244
+ def capture_text_field_input(window, form, field)
245
+ while (ch = window.getch) != 13 # return is ascii 13
246
+ case ch
247
+ when Ncurses::KEY_LEFT
248
+ form.form_driver Ncurses::Form::REQ_PREV_CHAR
249
+ when Ncurses::KEY_RIGHT
250
+ form.form_driver Ncurses::Form::REQ_NEXT_CHAR
251
+ when Ncurses::KEY_BACKSPACE
252
+ form.form_driver Ncurses::Form::REQ_DEL_PREV
253
+ else
254
+ form.form_driver ch # If this is a normal character, it gets Printed
255
+ end
256
+ end
257
+ form.form_driver Ncurses::Form::REQ_NEXT_FIELD # Request next to set 0 buffer in field
258
+ Ncurses::Form.field_buffer(field, 0)
259
+ end
260
+ end
261
+ end