1 -------------------------------------------------------------------------------
2 -- Tools to deal with tasks
3 -------------------------------------------------------------------------------
5 local Configuration = require('nota.configuration')
6 local Notes = require('nota.notes')
7 local Util = require('nota.util')
9 -------------------------------------------------------------------------------
11 -------------------------------------------------------------------------------
12 local unchecked_pattern = '^(%s*)%- %[ %]'
13 local unchecked_important_pattern = '^(%s*)%* %[ %]'
14 local checked_pattern = '^(%s*)%- %[x%]'
15 local checked_important_pattern = '^(%s*)%* %[x%]'
17 local function is_open_task(line)
18 return line:match(unchecked_pattern) or line:match(unchecked_important_pattern)
21 local function is_completed_task(line)
22 return line:match(checked_pattern) or line:match(checked_important_pattern)
25 local function is_task(line)
26 return is_open_task(line) or is_completed_task(line)
29 local function open(path)
30 local parent = Util.directory_name(path)
31 Util.ensure_directory_exists(parent)
32 vim.cmd('edit ' .. path)
35 local function open_inbox()
36 local path = Configuration.path_for(Configuration.configuration.tasks.inbox)
40 local function open_someday()
41 local path = Configuration.path_for(Configuration.configuration.tasks.someday)
45 local function parse_tags(tag_string)
47 for tag in tag_string:gmatch('([^,]+)') do
48 table.insert(tags, tag)
53 local function table_contains_value(target_table, value)
54 for _, table_value in ipairs(target_table) do
55 if table_value == value then
62 local function remove_value(target_table, value)
63 for i, table_value in ipairs(target_table) do
64 if table_value == value then
65 table.remove(target_table, i)
70 -------------------------------------------------------------------------------
72 -------------------------------------------------------------------------------
74 --- Toggles a task completion status
75 function Tasks.toggle()
76 local line_number = api.nvim_win_get_cursor(0)[1]
77 local line = api.nvim_get_current_line()
79 if line:match(unchecked_pattern) then
80 line = line:gsub(unchecked_pattern, '%1- [x]', 1)
81 elseif line:match(unchecked_important_pattern) then
82 line = line:gsub(unchecked_important_pattern, '%1* [x]', 1)
83 elseif line:match(checked_pattern) then
84 line = line:gsub(checked_pattern, '%1- [ ]', 1)
85 elseif line:match(checked_important_pattern) then
86 line = line:gsub(checked_important_pattern, '%1* [ ]', 1)
88 api.nvim_err_writeln('Toggle completion only works on tasks')
91 api.nvim_buf_set_lines(0, line_number - 1, line_number, false, {line})
94 --- Toggles a task completion status
95 function Tasks.toggle_importance()
96 local line_number = api.nvim_win_get_cursor(0)[1]
97 local line = api.nvim_get_current_line()
99 if line:match(unchecked_pattern) then
100 line = line:gsub(unchecked_pattern, '%1* [ ]', 1)
101 elseif line:match(unchecked_important_pattern) then
102 line = line:gsub(unchecked_important_pattern, '%1- [ ]', 1)
103 elseif line:match(checked_pattern) then
104 line = line:gsub(checked_pattern, '%1* [x]', 1)
105 elseif line:match(checked_important_pattern) then
106 line = line:gsub(checked_important_pattern, '%1- [x]', 1)
108 api.nvim_err_writeln('Toggle importance only works on tasks')
111 api.nvim_buf_set_lines(0, line_number - 1, line_number, false, {line})
114 --- Inserts a new task at the cursor location
115 function Tasks.insert()
116 vim.cmd('normal! o- [ ] ')
117 vim.cmd('startinsert!')
120 --- Captures a new task into the inbox
121 function Tasks.capture()
122 local prefix = '- [ ] '
124 vim.cmd('normal! Go'..prefix)
125 vim.cmd('startinsert!')
129 function Tasks.tag(tag)
130 local line_number = api.nvim_win_get_cursor(0)[1]
131 local line = api.nvim_get_current_line()
132 if is_task(line) then
133 local pattern = ':(.*):$'
134 local tag_string = line:match(pattern) or ''
135 if not tag or tag == '' then
136 tag = vim.fn.input('Add new tag for task (' .. tag_string .. '): ')
137 if not tag or tag == '' then
141 local tags = parse_tags(tag_string)
142 if not table_contains_value(tags, tag) then
143 table.insert(tags, tag)
144 local new_tags = table.concat(tags, ',')
145 if line:match(pattern) then
146 line = line:gsub(pattern, ':' .. new_tags .. ':', 1)
148 line = line .. ' :' .. new_tags .. ':'
150 api.nvim_buf_set_lines(0, line_number - 1, line_number, false, {line})
153 api.nvim_err_writeln('Tagging only works on tasks')
157 --- Remove tag on a task
158 function Tasks.remove_tag(tag)
159 local line_number = api.nvim_win_get_cursor(0)[1]
160 local line = api.nvim_get_current_line()
161 if is_task(line) then
162 local pattern = ':(.*):$'
163 local tag_string = line:match(pattern)
164 if not tag_string then
165 api.nvim_err_writeln('No tags to remove')
169 if not tag or tag == '' then
170 tag = vim.fn.input('Pick tag to remove (' .. tag_string .. '): ')
173 local tags = parse_tags(tag_string)
174 remove_value(tags, tag)
178 new_tags = ':' .. table.concat(tags, ',') .. ':'
181 line = line:gsub(pattern, new_tags, 1):gsub('%s+$', '')
182 api.nvim_buf_set_lines(0, line_number - 1, line_number, false, {line})
184 api.nvim_err_writeln('Tagging only works on tasks')
188 --- Reschedule a task for today
189 function Tasks.reschedule_for_today()
190 local today = os.date('%Y-%m-%d')
191 Tasks.reschedule(today)
194 --- Reschedule a task for tomorrow
195 function Tasks.reschedule_for_tomorrow()
196 local tomorrow = os.date('%Y-%m-%d', os.time() + 24*60*60)
197 Tasks.reschedule(tomorrow)
200 --- Reschedule a task for someday
201 function Tasks.reschedule_for_someday()
202 Tasks.reschedule('someday')
205 --- Reschedule a task for an arbitrary date
206 function Tasks.reschedule(new_date)
208 if not new_date or new_date == '' then
209 new_date = vim.fn.input('Reschedule for which date (YYYY-mm-dd): ')
212 if not Util.is_valid_date(new_date) and new_date ~= 'someday' then
213 api.nvim_err_writeln(new_date .. ' is not a valid date in the format YYYY-mm-dd')
217 local line_number = api.nvim_win_get_cursor(0)[1]
218 local buffer = api.nvim_get_current_buf()
219 local line = api.nvim_get_current_line()
220 local filename = vim.fn.expand('%:t:r')
222 if is_open_task(line) then
223 if Util.is_before_today(filename) and line:match(unchecked_pattern) then
224 local rescheduled_line = line:gsub(unchecked_pattern, '%1- [>' .. new_date .. ']', 1)
225 api.nvim_buf_set_lines(0, line_number - 1, line_number, false, {rescheduled_line})
226 elseif Util.is_before_today(filename) and line:match(unchecked_important_pattern) then
227 local rescheduled_line = line:gsub(unchecked_important_pattern, '%1* [>' .. new_date .. ']', 1)
228 api.nvim_buf_set_lines(0, line_number - 1, line_number, false, {rescheduled_line})
230 api.nvim_buf_set_lines(buffer, line_number - 1, line_number, false, {})
235 if new_date == 'someday' then
238 Notes.open_daily(new_date)
241 local line_count = api.nvim_buf_line_count(0)
242 api.nvim_buf_set_lines(0, line_count, line_count, false, {line})
243 api.nvim_win_set_cursor(0, {line_count, 0})
245 api.nvim_err_writeln('Reschedule only works on open tasks')