todo-curses 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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