diff --git a/init.lua b/init.lua index 75f848d..81e92f0 100644 --- a/init.lua +++ b/init.lua @@ -5,3 +5,17 @@ require("config.keymap") -- load lazy.nvim to bootstrap plugins require("config.lazy") + +require('config.due_nvim').setup { + ft = '*.md', + date_hi = 'Comment', + week_hi = 'Structure', + + pattern_start = '<', + pattern_end = '>', + + use_clock_time = true, + use_clock_today = true, + use_seconds = false, +} + diff --git a/lua/config/due_nvim.lua b/lua/config/due_nvim.lua new file mode 100644 index 0000000..b4e5bdf --- /dev/null +++ b/lua/config/due_nvim.lua @@ -0,0 +1,356 @@ +local M = {} +_VT_NS = vim.api.nvim_create_namespace("due_nvim") + +local prescript +local prescript_hi +local due_hi +local ft +local today +local today_hi +local overdue +local overdue_hi +local date_hi +local week_hi +local pattern_start +local pattern_end + +local use_clock_time +local use_clock_today +local use_seconds +local default_due_time + +local user_hour +local user_min +local user_sec + +local date_pattern +local datetime_pattern +local datetime12_pattern +local fulldate_pattern +local fulldatetime_pattern +local fulldatetime12_pattern + +local regex_hi + +local update_rate + +local function patternify(str) + if not str then return "" end + return str:gsub("[%(%)%.%%%+%-%*%?%[%^%$%]]", "%%%1") +end + +local function regexify(str) + if not str then return "" end + return str:gsub("\\%^%$%.%*~%[%]&", "\\%1") +end + +local function make_pattern(pattern) + return patternify(pattern_start) .. pattern:gsub('%(', ''):gsub('%)', '') .. + patternify(pattern_end) +end + +local function make_pattern_match(pattern) + return patternify(pattern_start) .. pattern .. patternify(pattern_end) +end + +local function parseDue(due) + local year = 31556926 + local month = 2629743 + local week = 604800 + local day = 86400 + local hour = 3600 + local minute = 60 + local res = '' + local is_today = due < day + + if due >= year then + res = res .. math.floor(due / year) .. 'y ' + due = due % year + end + + if due >= month then + res = res .. math.floor(due / month) .. 'mo ' + due = due % month + end + + if due >= week then + res = res .. math.floor(due / week) .. 'w ' + due = due % week + end + + if use_clock_time or (is_today and use_clock_today) then + if due >= day then + res = res .. math.floor(due / day) .. 'd ' + due = due % day + end + + if due >= hour then + res = res .. math.floor(due / hour) .. 'h ' + due = due % hour + end + + if due >= minute then + res = res .. math.floor(due / minute) .. 'min ' + due = due % minute + end + + if use_seconds then res = res .. math.floor(due / 1) + 1 .. 's ' end + + else + res = res .. math.floor(due / day) + 1 .. 'd ' + end + + return res +end + +function M.setup(c) + c = c or {} + use_clock_time = c.use_clock_time or false + use_clock_today = c.use_clock_today or false + if type(c.use_seconds) == 'boolean' then + use_seconds = c.use_seconds + else + use_seconds = c.use_clock_time or false + end + update_rate = c.update_rate or + (use_clock_time and (use_seconds and 1000 or 60000) or 0) + default_due_time = c.default_due_time or 'midnight' + prescript = c.prescript or 'due: ' + prescript_hi = c.prescript_hi or 'Comment' + due_hi = c.due_hi or 'String' + ft = c.ft or '*.md' + today = c.today or 'TODAY' + today_hi = c.today_hi or 'Character' + overdue = c.overdue or 'OVERDUE' + overdue_hi = c.overdue_hi or 'Error' + date_hi = c.date_hi or 'Conceal' + week_hi = c.week_hi or 'Question' + pattern_start = c.pattern_start or '<' + pattern_end = c.pattern_end or '>' + date_pattern = c.date_pattern or '(%d%d)%-(%d%d)' + datetime_pattern = c.datetime_pattern or (date_pattern .. ' (%d+):(%d%d)') + datetime12_pattern = c.datetime12_pattern or (datetime_pattern .. ' (%a%a)') + fulldate_pattern = c.fulldate_pattern or ('(%d%d%d%d)%-' .. date_pattern) + fulldatetime_pattern = c.fulldatetime_pattern or + ('(%d%d%d%d)%-' .. datetime_pattern) + fulldatetime12_pattern = c.fulldatetime12_pattern or + (fulldatetime_pattern .. ' (%a%a)') + regex_hi = c.regex_hi or + "\\d*-*\\d\\+-\\d\\+\\( \\d*:\\d*\\( \\a\\a\\)\\?\\)\\?" + + if default_due_time == "midnight" then + user_hour = 23 + user_min = 59 + user_sec = 59 + elseif default_due_time == "noon" then + user_hour = 12 + user_min = 0 + user_sec = 0 + else + -- Default fallback + user_hour = 23 + user_min = 59 + user_sec = 59 + end + + local regex_start = regexify(pattern_start) + local regex_end = regexify(pattern_end) + + local regex_hi_full = '/' .. regex_start .. regex_hi .. regex_end .. '/' + + vim.api.nvim_create_autocmd({"BufEnter", "InsertLeave", "TextChanged", "TextChangedI"}, { + pattern = ft, + callback = function(args) + if vim.fn.mode() == 'i' then + -- In insert mode, just redraw + M.redraw(args.buf) + else + -- Not in insert mode, can do full draw + M.clear(args.buf) + M.draw(args.buf) + end + end, + group = vim.api.nvim_create_augroup("Due", { clear = true }) + }) + + -- Set up syntax highlighting + vim.api.nvim_create_autocmd("BufEnter", { + pattern = ft, + callback = function() + vim.cmd(string.format( + "syntax match DueDate %s display containedin=mkdNonListItemBlock,mkdListItemLine,mkdBlockquote conceal", + regex_hi_full + )) + vim.cmd(string.format("highlight default link DueDate %s", date_hi)) + end, + group = vim.api.nvim_create_augroup("DueSyntax", { clear = true }) + }) + + -- Start async updates if configured + vim.api.nvim_create_autocmd("BufEnter", { + pattern = ft, + callback = function(args) + M.async_update(args.buf) + end, + group = vim.api.nvim_create_augroup("DueAsync", { clear = true }) + }) +end + +local function draw_due(due, buf, key) + local parsed + + if due > 0 then + if not (use_clock_time or use_clock_today) and due < 86400 then + parsed = { today, today_hi } + elseif due < 86400 then + parsed = { parseDue(due), today_hi } + elseif due < 604800 then + parsed = { parseDue(due), due_hi } + else + parsed = { parseDue(due), week_hi } + end + else + parsed = { overdue, overdue_hi } + end + + vim.api.nvim_buf_set_extmark(buf, _VT_NS, key - 1, 0, { + virt_text = { { prescript, prescript_hi }, parsed }, + virt_text_pos = 'eol', + }) +end + +function M.draw(buf) + -- get current time + local now = os.time(os.date('*t')) + + -- find which date pattern is being passed in by user + for key, value in pairs(vim.api.nvim_buf_get_lines(buf, 0, -1, {})) do + local fulldatetime12 = value:match(make_pattern(fulldatetime12_pattern)) + if fulldatetime12 then + local year, month, day, hour, min, period = + fulldatetime12:match(make_pattern_match(fulldatetime12_pattern)) + hour = tonumber(hour) + local is_pm = period:lower() == 'pm' + if is_pm and hour < 12 or not is_pm and hour == 12 then + hour = hour + 12 + if hour == 24 then + hour = 0 + end + end + draw_due(os.time({ + year = tonumber(year), + month = tonumber(month), + day = tonumber(day), + hour = hour, + min = tonumber(min), + sec = user_sec + }) - now, buf, key) + goto continue + end + + local fulldatetime = value:match(make_pattern(fulldatetime_pattern)) + if fulldatetime then + local year, month, day, hour, min = + fulldatetime:match(make_pattern_match(fulldatetime_pattern)) + draw_due(os.time({ + year = tonumber(year), + month = tonumber(month), + day = tonumber(day), + hour = tonumber(hour), + min = tonumber(min), + sec = user_sec + }) - now, buf, key) + goto continue + end + + local fulldate = value:match(make_pattern(fulldate_pattern)) + if fulldate then + local year, month, day = fulldate:match(make_pattern_match( + fulldate_pattern)) + draw_due(os.time({ + year = tonumber(year), + month = tonumber(month), + day = tonumber(day), + hour = user_hour, + min = user_min, + sec = user_sec + }) - now, buf, key) + goto continue + end + + local datetime12 = value:match(make_pattern(datetime12_pattern)) + if datetime12 then + local month, day, hour, min, period = + datetime12:match(make_pattern_match(datetime12_pattern)) + local year = os.date("%Y") + hour = tonumber(hour) + local is_pm = period:lower() == 'pm' + if is_pm and hour < 12 or not is_pm and hour == 12 then + hour = hour + 12 + if hour == 24 then + hour = 0 + end + end + draw_due(os.time({ + year = tonumber(year), + month = tonumber(month), + day = tonumber(day), + hour = hour, + min = tonumber(min), + sec = user_sec + }) - now, buf, key) + goto continue + end + + local datetime = value:match(make_pattern(datetime_pattern)) + if datetime then + local month, day, hour, min = datetime:match(make_pattern_match( + datetime_pattern)) + local year = os.date("%Y") + draw_due(os.time({ + year = tonumber(year), + month = tonumber(month), + day = tonumber(day), + hour = tonumber(hour), + min = tonumber(min), + sec = user_sec + }) - now, buf, key) + goto continue + end + + local date = value:match(make_pattern(date_pattern)) + if date then + local month, day = date:match(make_pattern_match(date_pattern)) + local year = os.date("%Y") + + draw_due(os.time({ + year = tonumber(year), + month = tonumber(month), + day = tonumber(day), + hour = user_hour, + min = user_min, + sec = user_sec + }) - now, buf, key) + goto continue + end + ::continue:: + end +end + +function M.clear(buf) vim.api.nvim_buf_clear_namespace(buf, _VT_NS, 0, -1) end + +function M.redraw(buf) + M.clear(buf) + M.draw(buf) +end + +function M.async_update(buf) + if update_rate <= 0 then return end + local timer = vim.loop.new_timer() + timer:start(update_rate, 0, vim.schedule_wrap(function() + M.redraw(buf) + M.async_update(buf) + end)) +end + +return M diff --git a/lua/config/options.lua b/lua/config/options.lua index 45266d7..5d7b5a2 100644 --- a/lua/config/options.lua +++ b/lua/config/options.lua @@ -87,3 +87,9 @@ vim.filetype.add({ ['/tmp/calcurse.*;.*/calcurse/notes/.*'] = 'markdown', }, }) + +-- markdown settings +vim.opt.conceallevel = 2 +vim.g.vim_markdown_folding_disabled = 1 +vim.g.vim_markdown_math = 1 +vim.g.vim_markdown_strikethrough = 1