todo-curses 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2dd49a99988df6a2152060b4120949b41d8594f3
4
+ data.tar.gz: 54d30df0e6700095ba7603ff6846391124527a1c
5
+ SHA512:
6
+ metadata.gz: 32d463bbd95f1c0cb53f9fe5e6b867d571fd665d9d76717ef50e324382591f037664058a0c77ff8898593f6069af8b4058c5fee086bfa07049cdd3564cb77b48
7
+ data.tar.gz: 84291c4338fae4c798d3c73b57ae18b031f42dcfc238b49a73e9f32a3fed4383b161481811300ed9de2714ad678a7a6e4ff6d22bf1c95102738979941c545b4c
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ todo.txt
2
+ todo.txt.bak
3
+ done.txt
4
+ log.txt
5
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'ncursesw'
5
+
data/Gemfile.lock ADDED
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ todo (0.0.1)
5
+ gli (= 2.13.4)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ aruba (0.6.1)
11
+ childprocess (>= 0.3.6)
12
+ cucumber (>= 1.1.1)
13
+ rspec-expectations (>= 2.7.0)
14
+ builder (3.2.2)
15
+ childprocess (0.5.5)
16
+ ffi (~> 1.0, >= 1.0.11)
17
+ cucumber (1.3.18)
18
+ builder (>= 2.1.2)
19
+ diff-lcs (>= 1.1.3)
20
+ gherkin (~> 2.12)
21
+ multi_json (>= 1.7.5, < 2.0)
22
+ multi_test (>= 0.1.1)
23
+ diff-lcs (1.2.5)
24
+ ffi (1.9.10)
25
+ gherkin (2.12.2)
26
+ multi_json (~> 1.3)
27
+ gli (2.13.4)
28
+ multi_json (1.11.2)
29
+ multi_test (0.1.1)
30
+ ncursesw (1.4.9)
31
+ rake (10.4.2)
32
+ rdoc (4.2.0)
33
+ rspec-expectations (3.3.1)
34
+ diff-lcs (>= 1.2.0, < 2.0)
35
+ rspec-support (~> 3.3.0)
36
+ rspec-support (3.3.0)
37
+
38
+ PLATFORMS
39
+ ruby
40
+
41
+ DEPENDENCIES
42
+ aruba
43
+ ncursesw
44
+ rake
45
+ rdoc
46
+ todo!
47
+
48
+ BUNDLED WITH
49
+ 1.11.2
data/README.rdoc ADDED
@@ -0,0 +1,69 @@
1
+ = TODO-Curses
2
+
3
+ This is a super crude first pass at a terminal application for
4
+ todo.txt files. A lot of the features are based on how
5
+ {todotxt.net}[todotxt.net] handles things. I really liked the overall
6
+ design of the application, just not the fact that it didn't run in my
7
+ linux terminal. To solve this, I decided to roll my own using Ruby and
8
+ Ncurses. There was already a pretty robust library for handling todo.txt
9
+ files, and Ncurses was something I'd been meaning to learn for a while.
10
+
11
+ No doubt there's a ton of nasty code in here. Please help me along if you're
12
+ interested. I'm sure there's a lot that can be refactored.
13
+
14
+ == Current features
15
+
16
+ - Open todo.txt files and view a scrollable list of items
17
+ - Move to the next item with `j`
18
+ - Move to the prev item with `k`
19
+ - Create new items with `n`
20
+ - Toggle done / not done state with `x`
21
+ - Move priority down with `shift+j`
22
+ - Move priority up with `shift+k`
23
+ - Completed tasks are archived to done.txt on exit
24
+
25
+ == Planned features
26
+
27
+ - Use ctrl instead of shift for priority change
28
+ - Color code priorities
29
+ - Add a spacer between priority groups
30
+ - Priority view with `ctrl+1`
31
+ - Project view with `ctrl+2`
32
+ - Strip out application wrapper; not needed
33
+ - Prep for release as a gem
34
+ - If no argument is given, open the default file. Default tbd.
35
+
36
+ == Ideas for later
37
+
38
+ Shift+J: Cycle through displays (Priority, project, etc.)
39
+ F: filter tasks (free-text, one filter condition per line)
40
+ T: append text to selected tasks
41
+ O or Ctrl+O: open todo.txt file
42
+ C or Ctrl+N: new todo.txt file
43
+
44
+ == Things not included
45
+
46
+ A: archive tasks
47
+ Ctrl+C: copy task to clipboard
48
+ Ctrl+Shift+C: copy task to edit field
49
+ Win+Alt+T: hide/unhide windows
50
+ 0: clear filter
51
+ 1-9: apply numbered filter preset
52
+
53
+ == Keyboard shortcut ideas
54
+
55
+ N: new task
56
+ J: next task
57
+ K: prev task
58
+ X: toggle task completion
59
+ D or Del or Backspace: delete task (with confirmation)
60
+ E or F2: update task
61
+ I: set priority
62
+ . or F5: reload tasks from file
63
+ ?: show help
64
+ Shift+K: increase priority
65
+ Shift+J: decrease priority
66
+ Alt+Left/Right: clear priority
67
+ Ctrl+Alt+Up: increase due date by 1 day
68
+ Ctrl+Alt+Down: decrease due date by 1 day
69
+ Ctrl+Alt+Left/Right: remove due date
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ require 'rake/clean'
2
+ require 'rubygems'
3
+ require 'rubygems/package_task'
4
+ require 'rdoc/task'
5
+ require 'cucumber'
6
+ require 'cucumber/rake/task'
7
+ Rake::RDocTask.new do |rd|
8
+ rd.main = "README.rdoc"
9
+ rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
10
+ rd.title = 'Your application title'
11
+ end
12
+
13
+ spec = eval(File.read('todo.gemspec'))
14
+
15
+ Gem::PackageTask.new(spec) do |pkg|
16
+ end
17
+ CUKE_RESULTS = 'results.html'
18
+ CLEAN << CUKE_RESULTS
19
+ desc 'Run features'
20
+ Cucumber::Rake::Task.new(:features) do |t|
21
+ opts = "features --format html -o #{CUKE_RESULTS} --format progress -x"
22
+ opts += " --tags #{ENV['TAGS']}" if ENV['TAGS']
23
+ t.cucumber_opts = opts
24
+ t.fork = false
25
+ end
26
+
27
+ desc 'Run features tagged as work-in-progress (@wip)'
28
+ Cucumber::Rake::Task.new('features:wip') do |t|
29
+ tag_opts = ' --tags ~@pending'
30
+ tag_opts = ' --tags @wip'
31
+ t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty -x -s#{tag_opts}"
32
+ t.fork = false
33
+ end
34
+
35
+ task :cucumber => :features
36
+ task 'cucumber:wip' => 'features:wip'
37
+ task :wip => 'features:wip'
38
+ require 'rake/testtask'
39
+ Rake::TestTask.new do |t|
40
+ t.libs << "test"
41
+ t.test_files = FileList['test/*_test.rb']
42
+ end
43
+
44
+ task :default => [:test,:features]
data/bin/todo-curses ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin # XXX: Remove this begin/rescue before distributing your app
4
+ require 'todo'
5
+ rescue LoadError
6
+ STDERR.puts "In development, you need to use `bundle exec bin/todo` to run your app"
7
+ STDERR.puts "At install-time, RubyGems will make sure lib, etc. are in the load path"
8
+ STDERR.puts "Feel free to remove this message from bin/todo now"
9
+ exit 64
10
+ end
11
+
12
+ include GLI::App
13
+ include Ncurses
14
+ include Ncurses::Form
15
+
16
+ program_desc 'A simple interface for managing todo.txt files.'
17
+
18
+ subcommand_option_handling :normal
19
+ arguments :strict
20
+
21
+ desc 'Starts an interactive editor for the given todo.txt list.'
22
+ arg_name 'Describe arguments to list here'
23
+ command :list do |c|
24
+ c.action do |global_options,options,args|
25
+ if args.size != 1 then
26
+ printf("usage: #{$0} file\n");
27
+ exit
28
+ end
29
+
30
+ TodoViewer.new(ARGV[0])
31
+ end
32
+ end
33
+
34
+ pre do |global,command,options,args|
35
+ # Pre logic here
36
+ # Return true to proceed; false to abort and not call the
37
+ # chosen command
38
+ # Use skips_pre before a command to skip this block
39
+ # on that command only
40
+ true
41
+ end
42
+
43
+ post do |global,command,options,args|
44
+ # Post logic here
45
+ # Use skips_post before a command to skip this
46
+ # block on that command only
47
+ end
48
+
49
+ on_error do |exception|
50
+ # Error logic here
51
+ # return false to skip default error handling
52
+ true
53
+ end
54
+
55
+ exit run(ARGV)
@@ -0,0 +1,6 @@
1
+ When /^I get help for "([^"]*)"$/ do |app_name|
2
+ @app_name = app_name
3
+ step %(I run `#{app_name} help`)
4
+ end
5
+
6
+ # Add more step definitions here
@@ -0,0 +1,15 @@
1
+ require 'aruba/cucumber'
2
+
3
+ ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
4
+ LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
5
+
6
+ Before do
7
+ # Using "announce" causes massive warnings on 1.9.2
8
+ @puts = true
9
+ @original_rubylib = ENV['RUBYLIB']
10
+ ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
11
+ end
12
+
13
+ After do
14
+ ENV['RUBYLIB'] = @original_rubylib
15
+ end
@@ -0,0 +1,8 @@
1
+ Feature: My bootstrapped app kinda works
2
+ In order to get going on coding my awesome app
3
+ I want to have aruba and cucumber setup
4
+ So I don't have to do it myself
5
+
6
+ Scenario: App just runs
7
+ When I get help for "todo"
8
+ Then the exit status should be 0
@@ -0,0 +1,283 @@
1
+ # TODO: Refactor into proper size functions
2
+ # A curses based todo.txt file viewer
3
+ class TodoViewer
4
+
5
+ # Run the ncurses application
6
+ def interact
7
+ while true
8
+ result = true
9
+ c = Ncurses.getch
10
+ case c
11
+ when 'J'.ord
12
+ result = priority_down
13
+ when 'K'.ord
14
+ result = priority_up
15
+ when 'x'.ord
16
+ toggle_item_completion
17
+ when 'n'.ord
18
+ new_item
19
+ when 'j'.ord
20
+ result = scroll_down
21
+ when 'k'.ord
22
+ result = scroll_up
23
+ # when '\s'.ord # white space
24
+ # for i in 0..(@screen.getmaxy - 2)
25
+ # if( ! scroll_down )
26
+ # if( i == 0 )
27
+ # result = false
28
+ # end
29
+ # break
30
+ # end
31
+ # end
32
+ # when Ncurses::KEY_PPAGE
33
+ # for i in 0..(@screen.getmaxy - 2)
34
+ # if( ! scroll_up )
35
+ # if( i == 0 )
36
+ # result = false
37
+ # end
38
+ # break
39
+ # end
40
+ # end
41
+ when 'h'.ord
42
+ while( scroll_up )
43
+ end
44
+ when 'l'.ord
45
+ while( scroll_down )
46
+ end
47
+ when 'q'.ord
48
+ break
49
+ else
50
+ @screen.mvprintw(0,0, "[unknown key `#{Ncurses.keyname(c)}'=#{c}] ")
51
+ end
52
+ if( !result )
53
+ Ncurses.beep
54
+ end
55
+ # TODO: Catch ctrl+c for graceful exit
56
+ end
57
+
58
+ # TODO: Confirm exit
59
+ clean_done_tasks
60
+ close_ncurses
61
+ end
62
+
63
+ private
64
+
65
+ # Create a new fileviewer, and view the file.
66
+ def initialize(filename)
67
+ @log = Logger.new 'log.txt'
68
+ @log.debug 'Run Started'
69
+ init_curses
70
+ load_file(filename)
71
+ interact
72
+ # TODO: Save a copy of the todo.txt list to backup file.
73
+ end
74
+
75
+ # Perform the curses setup
76
+ def init_curses
77
+ @screen = Ncurses.initscr
78
+ Ncurses.nonl
79
+ Ncurses.cbreak
80
+ Ncurses.noecho
81
+ @screen.scrollok(true)
82
+ end
83
+
84
+ # Loads the given file as a todo.txt array. Sets the view to the top
85
+ # and redraws the list view.
86
+ # @param filename [String] path to the text file to be loaded
87
+ def load_file(filename)
88
+ @done_file = File.dirname(filename) + '/done.txt'
89
+ @list = Todo::List.new filename
90
+ @list.sort! { |x,y| y <=> x } # Reverse sort
91
+ items = []
92
+ last_priority = nil
93
+ last_selection = @menu.current_item.user_object if @menu
94
+ current_selection = nil
95
+
96
+ # Build the menu item list
97
+ @list.each do |item|
98
+ # Insert dividers on priority change
99
+ if item.priority != last_priority
100
+ divider_priority = item.priority.nil? ? 'N/A' : item.priority.to_s
101
+ divider = Ncurses::Menu::ITEM.new(divider_priority, '')
102
+ items << divider
103
+ last_priority = item.priority
104
+ end
105
+
106
+ # Build the todo menu item
107
+ menu_item = Ncurses::Menu::ITEM.new(item.to_s, '') # name, description
108
+ menu_item.user_object = item
109
+ items << menu_item
110
+
111
+ # Set the current selection
112
+ current_selection = menu_item if item.to_s == last_selection.to_s
113
+ end
114
+
115
+ # Build the final menu object
116
+ # TODO: Possible memory leak from resetting object over top?
117
+ @menu = Ncurses::Menu::MENU.new items
118
+ @menu.set_menu_win(@screen)
119
+ @menu.set_menu_sub(@screen.derwin(@screen.getmaxx, @screen.getmaxy, 0, 0))
120
+ @menu.set_menu_format(@screen.getmaxy, 1)
121
+
122
+ # Set dividers to non-interactive
123
+ @menu.menu_items.select{ |i| i.user_object.nil? }.each do |divider|
124
+ divider.item_opts_off Menu::O_SELECTABLE
125
+ end
126
+
127
+ # Show the menu
128
+ @screen.clear
129
+ @menu.post_menu
130
+
131
+ # Set selection position
132
+ @menu.set_current_item current_selection if current_selection
133
+ @menu.menu_driver(Ncurses::Menu::REQ_DOWN_ITEM) if @menu.current_item.user_object.nil?
134
+
135
+ # Refresh
136
+ @screen.refresh
137
+ end
138
+
139
+ # Moves the current selection's priority up by one unless it is at Z.
140
+ def priority_up
141
+ item = @menu.current_item.user_object
142
+ item.priority_inc
143
+ save_list
144
+ end
145
+
146
+ # Moves the current selection's priority down by one unless it is at A.
147
+ def priority_down
148
+ item = @menu.current_item.user_object
149
+ item.priority_dec
150
+ save_list
151
+ end
152
+
153
+ # Scroll the display up by one line
154
+ # @return [Boolean] true if the action completed successfully.
155
+ def scroll_up
156
+ # Move to the next item if it's not the first in the list
157
+ unless @menu.menu_items[0].user_object.nil? &&
158
+ @menu.current_item.item_index < 2
159
+ result = @menu.menu_driver(Ncurses::Menu::REQ_UP_ITEM)
160
+ end
161
+ # Move to the next item if it's not a divider
162
+ result = @menu.menu_driver(Ncurses::Menu::REQ_UP_ITEM) unless @menu.current_item.user_object
163
+ return true if result == E_OK
164
+ false
165
+ end
166
+
167
+ # Scroll the display down by one line
168
+ # @return [Boolean] true if the action completed successfully.
169
+ def scroll_down
170
+ result = @menu.menu_driver(Ncurses::Menu::REQ_DOWN_ITEM)
171
+ result = @menu.menu_driver(Ncurses::Menu::REQ_DOWN_ITEM) unless @menu.current_item.user_object
172
+ return true if result == E_OK
173
+ false
174
+ end
175
+
176
+ # Collects a new todo item from the user and saves
177
+ # it to the text file.
178
+ def new_item
179
+ field = FIELD.new(1, @screen.getmaxx-1, 2, 1, 0, 0)
180
+ field.set_field_back(A_UNDERLINE)
181
+ fields = [field]
182
+ my_form = FORM.new(fields);
183
+ my_form.user_object = "My identifier"
184
+
185
+ # Calculate the area required for the form
186
+ rows = Array.new()
187
+ cols = Array.new()
188
+ my_form.scale_form(rows, cols);
189
+
190
+ # Create the window to be associated with the form
191
+ my_form_win = WINDOW.new(rows[0] + 3, cols[0] + 14, 1, 1);
192
+ my_form_win.keypad(TRUE);
193
+
194
+ # Set main window and sub window
195
+ my_form.set_form_win(my_form_win);
196
+ my_form.set_form_sub(my_form_win.derwin(rows[0], cols[0], 2, 12));
197
+
198
+ my_form.post_form();
199
+
200
+ # Print field types
201
+ my_form_win.mvaddstr(4, 2, "New item")
202
+ my_form_win.wrefresh();
203
+
204
+ stdscr.refresh();
205
+
206
+ new_item_text = capture_text_field_input(my_form_win, my_form, field)
207
+
208
+ # Save results
209
+ save_new_item(new_item_text)
210
+
211
+ # Clean up
212
+ my_form.unpost_form
213
+ my_form.free_form
214
+
215
+ field.free_field
216
+ # fields.each {|f| f.free_field()}
217
+ end
218
+
219
+ # Adds a new item to the list and saves the file
220
+ # @param task [String] the task to be added
221
+ # @return [Todo::List] the updated list
222
+ def save_new_item(task)
223
+ @list << Todo::Task.new(task)
224
+ save_list
225
+ @list
226
+ end
227
+
228
+ # Saves the current state of the list. Overrides the current file.
229
+ # Reloads the newly saved file.
230
+ def save_list
231
+ File.open(@list.path, 'w') { |file| file << @list.join("\n") }
232
+ load_file @list.path
233
+ end
234
+
235
+ # Marks the currently selected menu item as complete and saves the list.
236
+ def toggle_item_completion
237
+ @menu.current_item.user_object.toggle!
238
+ save_list
239
+ end
240
+
241
+ # Saves done tasks to done.txt and removes them from todo.txt
242
+ def clean_done_tasks
243
+ done_tasks = @list.select { |task| !task.completed_on.nil? }
244
+ File.open(@done_file, 'a') { |file|
245
+ file << "\n"
246
+ file << done_tasks.join("\n")
247
+ }
248
+ remaining_tasks = @list.select { |task| task.completed_on.nil? }
249
+ File.open(@list.path, 'w') { |file| file << remaining_tasks.join("\n") }
250
+ end
251
+
252
+ # put the screen back in its normal state
253
+ def close_ncurses
254
+ Ncurses.echo()
255
+ Ncurses.nocbreak()
256
+ Ncurses.nl()
257
+ Ncurses.endwin()
258
+ end
259
+
260
+ # Captures text input into a form and returns the resulting string.
261
+ # @param window [Window] the form window
262
+ # @param form [FORM] the form to be captured
263
+ # @param field [FIELD] the form to be captured
264
+ # @return [String] the captured input
265
+ def capture_text_field_input(window, form, field)
266
+ # Capture typing...
267
+ while((ch = window.getch()) != 13) # return is ascii 13
268
+ case ch
269
+ when KEY_LEFT
270
+ form.form_driver(REQ_PREV_CHAR);
271
+ when KEY_RIGHT
272
+ form.form_driver(REQ_NEXT_CHAR);
273
+ when KEY_BACKSPACE
274
+ form.form_driver(REQ_DEL_PREV);
275
+ else
276
+ # If this is a normal character, it gets Printed
277
+ form.form_driver(ch);
278
+ end
279
+ end
280
+ form.form_driver REQ_NEXT_FIELD # Request next to set 0 buffer in field
281
+ Ncurses::Form.field_buffer(field, 0)
282
+ end
283
+ end
@@ -0,0 +1,3 @@
1
+ module Todo
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,87 @@
1
+ module Todo
2
+ class List < Array
3
+ # Initializes a Todo List object with a path to the corresponding todo.txt
4
+ # file. For example, if your todo.txt file is located at:
5
+ #
6
+ # /home/sam/Dropbox/todo/todo.txt
7
+ #
8
+ # You would initialize this object like:
9
+ #
10
+ # list = Todo::List.new "/home/sam/Dropbox/todo/todo-txt"
11
+ #
12
+ # Alternately, you can initialize this object with an array of strings or
13
+ # tasks. If the array is of strings, the strings will be converted into
14
+ # tasks. You can supply a mixed list of string and tasks if you wish.
15
+ #
16
+ # Example:
17
+ #
18
+ # array = Array.new
19
+ # array.push "(A) A string task!"
20
+ # array.push Todo::Task.new("(A) An actual task!")
21
+ #
22
+ # list = Todo::List.new array
23
+ def initialize list
24
+ if list.is_a? Array
25
+ # No file path was given.
26
+ @path = nil
27
+
28
+ # If path is an array, loop over it, adding to self.
29
+ list.each do |task|
30
+ # If it's a string, make a new task out of it.
31
+ if task.is_a? String
32
+ self.push Todo::Task.new task
33
+ # If it's a task, just add it.
34
+ elsif task.is_a? Todo::Task
35
+ self.push task
36
+ end
37
+ end
38
+ elsif list.is_a? String
39
+ @path = list
40
+
41
+ # Read in lines from file, create Todo::Tasks out of them and push them
42
+ # onto self.
43
+ File.open(list) do |file|
44
+ file.each_line { |line| self.push Todo::Task.new line }
45
+ end
46
+ end
47
+ end
48
+
49
+ # The path to the todo.txt file that you supplied when you created the
50
+ # Todo::List object.
51
+ def path
52
+ @path
53
+ end
54
+
55
+ # Filters the list by priority and returns a new list.
56
+ #
57
+ # Example:
58
+ #
59
+ # list = Todo::List.new "/path/to/list"
60
+ # 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 }
63
+ end
64
+
65
+ # Filters the list by context and returns a new list.
66
+ #
67
+ # Example:
68
+ #
69
+ # list = Todo::List.new "/path/to/list"
70
+ # list.by_context "@context" #=> Will be a new list with only tasks
71
+ # containing "@context"
72
+ def by_context context
73
+ Todo::List.new self.select { |task| task.contexts.include? context }
74
+ end
75
+
76
+ # Filters the list by project and returns a new list.
77
+ #
78
+ # Example:
79
+ #
80
+ # list = Todo::List.new "/path/to/list"
81
+ # list.by_project "+project" #=> Will be a new list with only tasks
82
+ # containing "+project"
83
+ def by_project project
84
+ Todo::List.new self.select { |task| task.projects.include? project }
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,21 @@
1
+ require 'logger'
2
+
3
+ module Todo
4
+ module Logger
5
+ def self.included base
6
+ base.extend(self)
7
+ end
8
+
9
+ def self.logger= new_logger
10
+ @@logger = new_logger
11
+ end
12
+
13
+ def self.logger
14
+ @@logger ||= ::Logger.new(STDOUT)
15
+ end
16
+
17
+ def logger
18
+ Todo::Logger.logger
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,339 @@
1
+ require 'date'
2
+
3
+ module Todo
4
+ class Task
5
+ include Comparable
6
+ include Todo::Logger
7
+
8
+ # The regular expression used to match contexts.
9
+ def self.contexts_regex
10
+ /(?:\s+|^)@\w+/
11
+ end
12
+
13
+ # The regex used to match projects.
14
+ def self.projects_regex
15
+ /(?:\s+|^)\+\w+/
16
+ end
17
+
18
+ # The regex used to match priorities.
19
+ def self.priority_regex
20
+ /(?:^|\s+)\(([A-Za-z])\)\s+/
21
+ end
22
+
23
+ # The regex used to match creation date.
24
+ def self.created_on_regex
25
+ /(?:^|-\d{2}\s|\)\s)(\d{4}-\d{2}-\d{2})\s/
26
+ end
27
+
28
+ # The regex used to match completion.
29
+ def self.done_regex
30
+ /^x\s+(\d{4}-\d{2}-\d{2})\s+/
31
+ end
32
+
33
+ # The regex used to match due date.
34
+ def self.due_on_regex
35
+ /(?:due:)(\d{4}-\d{2}-\d{2})(?:\s+|$)/i
36
+ end
37
+
38
+ # Creates a new task. The argument that you pass in must be the string
39
+ # representation of a task.
40
+ #
41
+ # Example:
42
+ #
43
+ # task = Todo::Task.new("(A) A high priority task!")
44
+ def initialize task
45
+ @orig = task
46
+ @completed_on = get_completed_date #orig.scan(self.class.done_regex)[1] ||= nil
47
+ @priority, @created_on = orig_priority, orig_created_on
48
+ @due_on = get_due_on_date
49
+ @contexts ||= orig.scan(self.class.contexts_regex).map { |item| item.strip }
50
+ @projects ||= orig.scan(self.class.projects_regex).map { |item| item.strip }
51
+ end
52
+
53
+ # Returns the original content of the task.
54
+ #
55
+ # Example:
56
+ #
57
+ # task = Todo::Task.new "(A) @context +project Hello!"
58
+ # task.orig #=> "(A) @context +project Hello!"
59
+ attr_reader :orig
60
+
61
+ # Returns the task's creation date, if any.
62
+ #
63
+ # Example:
64
+ #
65
+ # task = Todo::Task.new "(A) 2012-03-04 Task."
66
+ # task.created_on
67
+ # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
68
+ #
69
+ # Dates _must_ be in the YYYY-MM-DD format as specified in the todo.txt
70
+ # format. Dates in any other format will be classed as malformed and this
71
+ # attribute will be nil.
72
+ attr_reader :created_on
73
+
74
+ # Returns the task's completion date if task is done.
75
+ #
76
+ # Example:
77
+ #
78
+ # task = Todo::Task.new "x 2012-03-04 Task."
79
+ # task.completed_on
80
+ # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
81
+ #
82
+ # Dates _must_ be in the YYYY-MM-DD format as specified in the todo.txt
83
+ # format. Dates in any other format will be classed as malformed and this
84
+ # attribute will be nil.
85
+ attr_reader :completed_on
86
+
87
+ # Returns the task's due date, if any.
88
+ #
89
+ # Example:
90
+ #
91
+ # task = Todo::Task.new "(A) This is a task. due:2012-03-04"
92
+ # task.due_on
93
+ # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
94
+ #
95
+ # Dates _must_ be in the YYYY-MM-DD format as specified in the todo.txt
96
+ # format. Dates in any other format will be classed as malformed and this
97
+ # attribute will be nil.
98
+ attr_reader :due_on
99
+
100
+ # Returns the priority, if any.
101
+ #
102
+ # Example:
103
+ #
104
+ # task = Todo::Task.new "(A) Some task."
105
+ # task.priority #=> "A"
106
+ #
107
+ # task = Todo::Task.new "Some task."
108
+ # task.priority #=> nil
109
+ attr_reader :priority
110
+
111
+ # Returns an array of all the @context annotations.
112
+ #
113
+ # Example:
114
+ #
115
+ # task = Todo:Task.new "(A) @context Testing!"
116
+ # task.context #=> ["@context"]
117
+ attr_reader :contexts
118
+
119
+ # Returns an array of all the +project annotations.
120
+ #
121
+ # Example:
122
+ #
123
+ # task = Todo:Task.new "(A) +test Testing!"
124
+ # task.projects #=> ["+test"]
125
+ attr_reader :projects
126
+
127
+ # Gets just the text content of the todo, without the priority, contexts
128
+ # and projects annotations.
129
+ #
130
+ # Example:
131
+ #
132
+ # task = Todo::Task.new "(A) @test Testing!"
133
+ # task.text #=> "Testing!"
134
+ def text
135
+ @text ||= orig.
136
+ gsub(self.class.done_regex, '').
137
+ gsub(self.class.priority_regex, '').
138
+ gsub(self.class.created_on_regex, '').
139
+ gsub(self.class.contexts_regex, '').
140
+ gsub(self.class.projects_regex, '').
141
+ gsub(self.class.due_on_regex, '').
142
+ strip
143
+ end
144
+
145
+ # Returns the task's creation date, if any.
146
+ #
147
+ # Example:
148
+ #
149
+ # task = Todo::Task.new "(A) 2012-03-04 Task."
150
+ # task.date
151
+ # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
152
+ #
153
+ # Dates _must_ be in the YYYY-MM-DD format as specified in the todo.txt
154
+ # format. Dates in any other format will be classed as malformed and this
155
+ # method will return nil.
156
+ #
157
+ # Deprecated
158
+ def date
159
+ logger.warn("Task#date is deprecated, use created_on instead.")
160
+
161
+ @created_on
162
+ end
163
+
164
+ # Returns whether a task's due date is in the past.
165
+ #
166
+ # Example:
167
+ #
168
+ # task = Todo::Task.new("This task is overdue! due:#{Date.today - 1}")
169
+ # task.overdue?
170
+ # #=> true
171
+ def overdue?
172
+ return true if !due_on.nil? && due_on < Date.today
173
+ false
174
+ end
175
+
176
+ # Returns if the task is done.
177
+ #
178
+ # Example:
179
+ #
180
+ # task = Todo::Task.new "x 2012-12-08 Task."
181
+ # task.done?
182
+ # #=> true
183
+ #
184
+ # task = Todo::Task.new "Task."
185
+ # task.done?
186
+ # #=> false
187
+ def done?
188
+ !@completed_on.nil?
189
+ end
190
+
191
+ # Completes the task on the current date.
192
+ #
193
+ # Example:
194
+ #
195
+ # task = Todo::Task.new "2012-12-08 Task."
196
+ # task.done?
197
+ # #=> false
198
+ #
199
+ # task.do!
200
+ # task.done?
201
+ # #=> true
202
+ # task.created_on
203
+ # #=> <Date: 2012-12-08 (4911981/2,0,2299161)>
204
+ # task.completed_on
205
+ # #=> # the current date
206
+ def do!
207
+ @completed_on = Date.today
208
+ @priority = nil
209
+ end
210
+
211
+ # Marks the task as incomplete and resets its original priority.
212
+ #
213
+ # Example:
214
+ #
215
+ # task = Todo::Task.new "x 2012-12-08 2012-03-04 Task."
216
+ # task.done?
217
+ # #=> true
218
+ #
219
+ # task.undo!
220
+ # task.done?
221
+ # #=> false
222
+ # task.created_on
223
+ # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
224
+ # task.completed_on
225
+ # #=> nil
226
+ def undo!
227
+ @completed_on = nil
228
+ @priority = orig_priority
229
+ end
230
+
231
+ # Toggles the task from complete to incomplete or vice versa.
232
+ #
233
+ # Example:
234
+ #
235
+ # task = Todo::Task.new "x 2012-12-08 Task."
236
+ # task.done?
237
+ # #=> true
238
+ #
239
+ # task.toggle!
240
+ # task.done?
241
+ # #=> false
242
+ #
243
+ # task.toggle!
244
+ # task.done?
245
+ # #=> true
246
+ def toggle!
247
+ done? ? undo! : do!
248
+ end
249
+
250
+ # Returns this task as a string.
251
+ #
252
+ # Example:
253
+ #
254
+ # task = Todo::Task.new "(A) 2012-12-08 Task"
255
+ # task.to_s
256
+ # #=> "(A) 2012-12-08 Task"
257
+ def to_s
258
+ priority_string = priority ? "(#{priority}) " : ""
259
+ done_string = done? ? "x #{completed_on} " : ""
260
+ created_on_string = created_on ? "#{created_on} " : ""
261
+ contexts_string = contexts.empty? ? "" : " #{contexts.join ' '}"
262
+ projects_string = projects.empty? ? "" : " #{projects.join ' '}"
263
+ due_on_string = due_on.nil? ? "" : " due:#{due_on}"
264
+ "#{done_string}#{priority_string}#{created_on_string}#{text}#{contexts_string}#{projects_string}#{due_on_string}"
265
+ end
266
+
267
+ # Compares the priorities of two tasks.
268
+ #
269
+ # Example:
270
+ #
271
+ # task1 = Todo::Task.new "(A) Priority A."
272
+ # task2 = Todo::Task.new "(B) Priority B."
273
+ #
274
+ # task1 > task2
275
+ # # => true
276
+ #
277
+ # task1 == task2
278
+ # # => false
279
+ #
280
+ # task2 > task1
281
+ # # => false
282
+ def <=> other_task
283
+ if self.priority.nil? and other_task.priority.nil?
284
+ 0
285
+ elsif other_task.priority.nil?
286
+ 1
287
+ elsif self.priority.nil?
288
+ -1
289
+ else
290
+ other_task.priority <=> self.priority
291
+ end
292
+ end
293
+
294
+ # Decreases the priority until Z.
295
+ def priority_dec
296
+ return if @priority.nil?
297
+ @priority = @priority.next if @priority.ord < 90
298
+ end
299
+
300
+ # Increases the priority until A. If it's nil, it sets it to A.
301
+ def priority_inc
302
+ if @priority.nil?
303
+ @priority = 'A'
304
+ else
305
+ @priority = (@priority.ord-1).chr if @priority.ord > 65
306
+ end
307
+ end
308
+
309
+ private
310
+
311
+ def orig_priority
312
+ @orig.match(self.class.priority_regex)[1] if @orig =~ self.class.priority_regex
313
+ end
314
+
315
+ def orig_created_on
316
+ begin
317
+ if @orig =~ self.class.created_on_regex
318
+ date = @orig.match self.class.created_on_regex
319
+ return Date.parse(date[1]) unless date.nil?
320
+ end
321
+ rescue; end
322
+ nil
323
+ end
324
+
325
+ def get_completed_date
326
+ begin
327
+ return Date.parse(self.class.done_regex.match(@orig)[1])
328
+ rescue; end
329
+ nil
330
+ end
331
+
332
+ def get_due_on_date
333
+ begin
334
+ return Date.parse(self.class.due_on_regex.match(@orig)[1])
335
+ rescue; end
336
+ nil
337
+ end
338
+ end
339
+ end
data/lib/todo-txt.rb ADDED
@@ -0,0 +1,7 @@
1
+ lib = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'logger'
5
+ require 'todo-txt/logger'
6
+ require 'todo-txt/list'
7
+ require 'todo-txt/task'
data/lib/todo.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'gli'
2
+ require 'todo-txt'
3
+ require 'ncursesw'
4
+ require 'todo/version.rb'
5
+ require 'todo/todo_viewer.rb'
6
+ require 'logger'
data/reset.sh ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ cp todo.txt.bak todo.txt
data/run.sh ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ bundle exec bin/todo-curses list todo.txt
@@ -0,0 +1,14 @@
1
+ require 'test_helper'
2
+
3
+ class DefaultTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ end
7
+
8
+ def teardown
9
+ end
10
+
11
+ def test_the_truth
12
+ assert true
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ require 'test/unit'
2
+
3
+ # Add test libraries you want to use here, e.g. mocha
4
+
5
+ class Test::Unit::TestCase
6
+
7
+ # Add global extensions to the test case class here
8
+
9
+ end
@@ -0,0 +1,23 @@
1
+ # Ensure we require the local version and not one we might have installed already
2
+ require File.join([File.dirname(__FILE__),'lib','todo','version.rb'])
3
+ spec = Gem::Specification.new do |s|
4
+ s.name = 'todo-curses'
5
+ s.version = Todo::VERSION
6
+ s.author = 'Loren Rogers'
7
+ s.email = 'loren@lorentrogers.com'
8
+ s.homepage = 'http://www.lorentrogers.com'
9
+ s.platform = Gem::Platform::RUBY
10
+ s.summary = 'An interactive terminal application for managing todo.txt files.'
11
+ s.files = `git ls-files`.split("
12
+ ")
13
+ s.require_paths << 'lib'
14
+ s.has_rdoc = true
15
+ s.extra_rdoc_files = ['README.rdoc','todo.rdoc']
16
+ s.rdoc_options << '--title' << 'todo-curses' << '--main' << 'README.rdoc' << '-ri'
17
+ s.bindir = 'bin'
18
+ s.executables << 'todo-curses'
19
+ s.add_development_dependency('rake')
20
+ s.add_development_dependency('rdoc')
21
+ s.add_development_dependency('aruba')
22
+ s.add_runtime_dependency('gli','2.13.4')
23
+ end
data/todo.rdoc ADDED
@@ -0,0 +1,5 @@
1
+ = todo
2
+
3
+ Generate this with
4
+ todo rdoc
5
+ After you have described your command line interface
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: todo-curses
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Loren Rogers
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rdoc
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aruba
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: gli
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 2.13.4
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 2.13.4
69
+ description:
70
+ email: loren@lorentrogers.com
71
+ executables:
72
+ - todo-curses
73
+ extensions: []
74
+ extra_rdoc_files:
75
+ - README.rdoc
76
+ - todo.rdoc
77
+ files:
78
+ - ".gitignore"
79
+ - Gemfile
80
+ - Gemfile.lock
81
+ - README.rdoc
82
+ - Rakefile
83
+ - bin/todo-curses
84
+ - features/step_definitions/todo_steps.rb
85
+ - features/support/env.rb
86
+ - features/todo.feature
87
+ - lib/todo-txt.rb
88
+ - lib/todo-txt/list.rb
89
+ - lib/todo-txt/logger.rb
90
+ - lib/todo-txt/task.rb
91
+ - lib/todo.rb
92
+ - lib/todo/todo_viewer.rb
93
+ - lib/todo/version.rb
94
+ - reset.sh
95
+ - run.sh
96
+ - test/default_test.rb
97
+ - test/test_helper.rb
98
+ - todo-curses.gemspec
99
+ - todo.rdoc
100
+ homepage: http://www.lorentrogers.com
101
+ licenses: []
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options:
105
+ - "--title"
106
+ - todo-curses
107
+ - "--main"
108
+ - README.rdoc
109
+ - "-ri"
110
+ require_paths:
111
+ - lib
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.4.8
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: An interactive terminal application for managing todo.txt files.
129
+ test_files: []
130
+ has_rdoc: true