todo-curses 0.0.1

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