diff --git a/README.md b/README.md index 712bef2..5b5b4df 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,17 @@ this plugin was originally a fork of [nocksock/do.nvim](https://github.com/nocks ### Adding Tasks -- `:Do add {task}` +- `:Do` will ask user input for `{task}` - `:Do {task}` - `:Do "{task}"` +- `:Do add {task}` will all add `{task}` to the end of the tasklist -- `:Do! add {task}` +- `:Do!` will ask user input for `{task}` - `:Do! {task}` - `:Do! "{task}"` +- `:Do! add {task}` will all add `{task}` to the start of the tasklist @@ -48,9 +50,9 @@ lazy.nvim: ## Configuration -### Default Configs +### Default Options -[see the source code for default configs](https://github.com/Hashino/doing.nvim/blob/e4639e848b1503c14a591e3bfc6862560eeccefb/lua/doing/state.lua#L18-L45) +[see the source code for default options](https://github.com/Hashino/doing.nvim/blob/main/lua/doing/config.lua) ### Example Config diff --git a/lua/doing.lua b/lua/doing.lua index 2dcc5be..a6d40a4 100644 --- a/lua/doing.lua +++ b/lua/doing.lua @@ -1,25 +1,26 @@ --- A tinier task manager that helps you stay on track. -local state = require("doing.state") -local utils = require("doing.utils") -local edit = require("doing.edit") +local config = require("doing.config") +local state = require("doing.state") +local utils = require("doing.utils") +local edit = require("doing.edit") -local Doing = {} +local Doing = {} ----setup doing.nvim +---@brief setup doing.nvim ---@param opts? DoingOptions function Doing.setup(opts) - state.options = vim.tbl_deep_extend("force", state.default_opts, opts or {}) - state.tasks = state.init(state.options.store) + config.options = vim.tbl_deep_extend("force", config.default_opts, opts or {}) + + state.tasks = state.init(config.options.store.file_name) -- doesn't touch the winbar if disabled so other plugins can manage -- it without interference - if state.options.winbar.enabled then + if config.options.winbar.enabled then state.auGroupID = vim.api.nvim_create_augroup("doing_nvim", { clear = true, }) vim.api.nvim_create_autocmd({ "BufEnter", }, { group = state.auGroupID, callback = function() - -- gives time to process filetype + -- HACK: gives time to process filetype vim.defer_fn(function() utils.update_winbar() end, 100) @@ -28,7 +29,7 @@ function Doing.setup(opts) end end ----add a task to the list +---@brief add a task to the list ---@param task? string task to add ---@param to_front? boolean whether to add task to front of list function Doing.add(task, to_front) @@ -36,8 +37,9 @@ function Doing.add(task, to_front) Doing.setup() end - if task then - if task:sub(1,1) == '"' and task:sub(-1,-1) == '"' then + if task ~= nil and task ~= "" then + -- remove quotes if present + if task:sub(1, 1) == '"' and task:sub(-1, -1) == '"' then task = task:sub(2, -2) end @@ -70,11 +72,11 @@ function Doing.done() end if state.tasks:count() > 0 then - state.tasks:pop() + state.tasks:done() if state.tasks:count() == 0 then utils.show_message("All tasks done ") - elseif not state.options.show_remaining then + elseif not config.options.show_remaining then utils.show_message(state.tasks:count() .. " tasks left.") else utils.task_modified() @@ -84,8 +86,8 @@ function Doing.done() end end --- returns current plugin task/message --- @param force boolean displays the message even if the plugin display is turned off +---@param force? boolean return status even if the plugin is toggled off +---@return string current current plugin task or message function Doing.status(force) if not state.tasks then Doing.setup() @@ -99,11 +101,11 @@ function Doing.status(force) local count = state.tasks:count() -- append task count number if there is more than 1 task - if state.options.show_remaining and count > 1 then + if config.options.show_remaining and count > 1 then tasks_left = " +" .. (state.tasks:count() - 1) .. " more" end - return state.options.doing_prefix .. state.tasks:current() .. tasks_left + return config.options.doing_prefix .. state.tasks:current() .. tasks_left elseif force then return "Not doing any tasks" end diff --git a/lua/doing/api.lua b/lua/doing/api.lua deleted file mode 100644 index 0e5f470..0000000 --- a/lua/doing/api.lua +++ /dev/null @@ -1,3 +0,0 @@ -vim.deprecate( 'require("doing.api")', 'require("doing")', "0.2.0", "doing.nvim") - -return require("doing") diff --git a/lua/doing/config.lua b/lua/doing/config.lua new file mode 100644 index 0000000..d08d502 --- /dev/null +++ b/lua/doing/config.lua @@ -0,0 +1,44 @@ +local Config = {} + +---@class DoingOptions +---@field ignored_buffers string[]|fun():string[] elements are checked against buffer filetype/filename/filepath +---@field message_timeout integer how many millisecons messages will stay on status +---@field doing_prefix string prefix to show before the task +---@field winbar.enabled boolean if plugin should manage the winbar +---@field store.file_name string name of the task file +---@field store.auto_delete_file boolean auto delete tasks file +---@field show_remaining boolean show "+n more" when there are more than 1 tasks +---@field edit_win_config table window configs of the floating editor + +Config.default_opts = { + message_timeout = 2000, + doing_prefix = "Doing: ", + + -- doesn"t display on buffers that match filetype/filename/filepath to + -- entries. can be either a string array or a function that returns a + -- string array. filepath can be relative to cwd or absolute + ignored_buffers = { "NvimTree", }, + + -- if should append "+n more" to the status when there's tasks remaining + show_remaining = true, + + -- window configs of the floating tasks editor + -- see :h nvim_open_win() for available options + edit_win_config = { + width = 50, + height = 15, + border = "rounded", + }, + + -- if plugin should manage the winbar + winbar = { enabled = true, }, + + store = { + -- name of tasks file + file_name = ".tasks", + }, +} + +Config.options = Config.default_opts + +return Config diff --git a/lua/doing/edit.lua b/lua/doing/edit.lua index 46b8706..4ba230d 100644 --- a/lua/doing/edit.lua +++ b/lua/doing/edit.lua @@ -1,32 +1,36 @@ +local config = require("doing.config") local state = require("doing.state") -local global_win = nil -local global_buf = nil - local Edit = {} ---- Get all the tasks currently in the pop up window +Edit.win = nil +Edit.buf = nil + +---get a tasks table from the buffer lines local function get_buf_tasks() - local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true) - local indices = {} + local tasks = {} - for _, line in pairs(lines) do - if line:gsub("%s", "") ~= "" then - table.insert(indices, line) + if Edit.buf then + local lines = vim.api.nvim_buf_get_lines(Edit.buf, 0, -1, true) + + for _, line in pairs(lines) do + -- checks if line is just spaces + if line:gsub("%s", "") ~= "" then + table.insert(tasks, line) + end end end - return indices + return tasks end --- creates window -local function get_floating_window() - local bufnr = vim.api.nvim_create_buf(false, false) +---creates window +local function setup_floating_window() + Edit.buf = vim.api.nvim_create_buf(false, false) - local width = state.options.edit_win_config.width - local height = state.options.edit_win_config.height + local width = config.options.edit_win_config.width + local height = config.options.edit_win_config.height - -- Get the current screen size local screen_width = vim.o.columns local screen_height = vim.o.lines @@ -44,72 +48,57 @@ local function get_floating_window() noautocmd = true, } - local win = vim.api.nvim_open_win(bufnr, true, - vim.tbl_extend("force", default_win_config, state.options.edit_win_config)) - - vim.api.nvim_set_option_value("winhl", "Normal:NormalFloat", {}) - - return { - buf = bufnr, - win = win, - } + Edit.win = vim.api.nvim_open_win(Edit.buf, true, + vim.tbl_extend("force", default_win_config, config.options.edit_win_config)) end --- closes the window +---closes the window local function close_edit(callback) if callback then callback(get_buf_tasks()) end - vim.api.nvim_win_close(0, true) - global_win = nil - global_buf = nil + if Edit.win then + vim.api.nvim_win_close(Edit.win, true) + Edit.win = nil + end end --- opens a float window to manage tasks +---@brief open floating window to edit tasks +---@param tasks table list of tasks +---@param callback function function to call when window is closed function Edit.open_edit(tasks, callback) - if global_win ~= nil and vim.api.nvim_win_is_valid(global_win) then - close_edit() - return + if Edit.win then + return close_edit() end - local win_info = get_floating_window() - global_win = win_info.win - global_buf = win_info.buf + setup_floating_window() vim.api.nvim_set_option_value("number", true, {}) vim.api.nvim_set_option_value("swapfile", false, {}) vim.api.nvim_set_option_value("filetype", "doing_tasks", {}) - vim.api.nvim_set_option_value("buftype", "acwrite", {}) vim.api.nvim_set_option_value("bufhidden", "delete", {}) - vim.api.nvim_buf_set_name(global_buf, "do-edit") - vim.api.nvim_buf_set_lines(global_buf, 0, #tasks, false, tasks) + vim.api.nvim_buf_set_name(Edit.buf, "do-edit") + + vim.api.nvim_buf_set_lines(Edit.buf, 0, #tasks, false, tasks) vim.keymap.set("n", "q", function() close_edit(callback) - end, { buffer = global_buf, }) + end, { buffer = Edit.buf, }) vim.keymap.set("n", "", function() close_edit(callback) - end, { buffer = global_buf, }) + end, { buffer = Edit.buf, }) - -- event after tasks from pop up has been written to + -- save tasks when buffer is written vim.api.nvim_create_autocmd("BufWriteCmd", { group = state.auGroupID, - buffer = global_buf, + buffer = Edit.buf, callback = function() local new_todos = get_buf_tasks() state.tasks:set(new_todos) end, }) - - vim.api.nvim_create_autocmd("BufModifiedSet", { - group = state.auGroupID, - buffer = global_buf, - callback = function() - vim.api.nvim_set_option_value("modified", false, {}) - end, - }) end return Edit diff --git a/lua/doing/state.lua b/lua/doing/state.lua index 3d7751b..27f4274 100644 --- a/lua/doing/state.lua +++ b/lua/doing/state.lua @@ -1,79 +1,41 @@ -local dir_separator = "/" -if vim.loop.os_uname().sysname:find("Windows") then - dir_separator = "\\" -end +local utils = require("doing.utils") +local dir_separator = utils.get_path_separator() local State = {} ----@class DoingOptions ----@field ignored_buffers string[]|fun():string[] elements of the array are checked against buffer filename/filetype ----@field message_timeout integer how many millisecons messages will stay on screen ----@field doing_prefix string prefix to show before the task ----@field winbar.enabled boolean if plugin should manage the winbar ----@field store.file_name string name of the task file ----@field store.auto_delete_file boolean auto delete tasks file ----@field show_remaining boolean show "+n more" when there are more than 1 tasks ----@field edit_win_config table window configs of the floating editor - -State.default_opts = { - message_timeout = 2000, - doing_prefix = "Doing: ", - - -- doesn"t display on buffers that match filetype/filename/filepath to - -- entries. can be either a string array or a function that returns a - -- string array. filepath can be relative to cwd or absolute - ignored_buffers = { "NvimTree", }, - - -- if should append "+n more" to the status when there's tasks remaining - show_remaining = true, - - -- window configs of the floating tasks editor - -- see :h nvim_open_win() for available options - edit_win_config = { - width = 50, - height = 15, - border = "rounded", - }, - - -- if plugin should manage the winbar - winbar = { enabled = true, }, - - store = { - -- name of tasks file - file_name = ".tasks", - }, -} - -State.view_enabled = true +State.file_name = nil State.tasks = nil State.message = nil +State.view_enabled = true State.auGroupID = nil -State.options = State.default_opts ----initialize task store -State.init = function(options) +---@brief initialzes the tasklist state +---@param file_name string name of the file to store tasks +---@return table instance instantiated state +function State.init(file_name) + State.file_name = file_name + local default_state = { - options = options, file = nil, tasks = {}, } local instance = setmetatable(default_state, { __index = State, }) - local state = require("doing.state") vim.api.nvim_create_autocmd("DirChanged", { - group = state.auGroupID, + group = State.auGroupID, callback = function() - state.tasks = State.init(state.options.store) + State.tasks = State.init(file_name) end, }) + instance.tasks = instance:import_file() or {} return instance end --- creates a file based on configs +---creates a file based on configs function State:create_file() - local name = State.options.store.file_name + local name = State.file_name local cwd = vim.fn.getcwd() local file = io.open(cwd .. dir_separator .. name, "w") assert(file, "couldn't create " .. name .. " in current cwd: " .. cwd) @@ -84,9 +46,9 @@ function State:create_file() return cwd .. dir_separator .. name end --- finds tasks file in cwd +---finds tasks file in cwd function State:import_file() - local file = vim.fn.findfile(vim.fn.getcwd() .. dir_separator .. State.options.store.file_name, + local file = vim.fn.findfile(vim.fn.getcwd() .. dir_separator .. State.file_name, ".;") if file == "" then @@ -106,13 +68,12 @@ local function delete_file(file_path) local success, err, err_name = (vim.uv or vim.loop).fs_unlink(file_path) if not success then - vim.notify(tostring(err_name) .. ":" .. tostring(err), - vim.log.levels.ERROR, { title = "doing.nvim: error deleting tasks file", }) + utils.notify(tostring(err_name) .. ":" .. tostring(err), vim.log.levels.ERROR) end end)() end --- syncs file tasks with loaded tasks. creates file if force == true +---syncs file tasks with loaded tasks. creates file if force == true function State:sync() if (not self.file) and #self.tasks > 0 then self.file = self:create_file() @@ -123,8 +84,7 @@ function State:sync() if self.file and vim.fn.filewritable(self.file) and self.tasks ~= {} then local res = vim.fn.writefile(self.tasks, self.file) if res ~= 0 then - vim.notify("error writing to tasks file", - vim.log.levels.ERROR, { title = "doing.nvim", }) + utils.notify("error writing to tasks file", vim.log.levels.ERROR) end end @@ -158,7 +118,7 @@ function State:add(str, to_front) return self:sync() end -function State:pop() +function State:done() return table.remove(self.tasks, 1), self:sync() end diff --git a/lua/doing/utils.lua b/lua/doing/utils.lua index 1d62e6b..a3c11bc 100644 --- a/lua/doing/utils.lua +++ b/lua/doing/utils.lua @@ -1,19 +1,10 @@ -local state = require("doing.state") +local config = require("doing.config") local Utils = {} ---- gets called when a task is added, edited, or removed -function Utils.task_modified() - Utils.update_winbar() - vim.api.nvim_exec_autocmds("User", { - pattern = "TaskModified", - group = state.auGroupID, - }) -end - ---redraw winbar depending on if there are tasks function Utils.update_winbar() - if state.options.winbar.enabled then + if config.options.winbar.enabled then vim.api.nvim_set_option_value("winbar", require("doing").status(), { scope = "local", }) end @@ -37,7 +28,7 @@ function Utils.should_display() return false end - local ignore = state.options.ignored_buffers + local ignore = config.options.ignored_buffers ignore = type(ignore) == "function" and ignore() or ignore local home_path_abs = tostring(os.getenv("HOME")) @@ -47,7 +38,6 @@ function Utils.should_display() -- checks if exclude is a relative filepath and expands it if exclude:sub(1, 2) == "./" or exclude:sub(1, 2) == ".\\" then exclude = vim.fn.getcwd() .. exclude:sub(2, -1) - vim.notify(exclude) end if @@ -66,17 +56,43 @@ function Utils.should_display() return true end ---- show a message for the duration of `options.message_timeout` or timeout +---gets called when a task is added, edited, or removed +function Utils.task_modified() + Utils.update_winbar() + vim.api.nvim_exec_autocmds("User", { + pattern = "TaskModified", + group = require("doing.state").auGroupID, + }) +end + +---@brief show a message for the duration of `options.message_timeout` or timeout ---@param str string message to show ---@param timeout? number time in ms to show message function Utils.show_message(str, timeout) - state.message = str + require("doing.state").message = str Utils.task_modified() vim.defer_fn(function() - state.message = nil + require("doing.state").message = nil Utils.task_modified() - end, timeout or state.options.message_timeout) + end, timeout or config.options.message_timeout) +end + +function Utils.get_path_separator() + local dir_separator = "/" + if (vim.loop or vim.uv).os_uname().sysname:find("Windows") then + dir_separator = "\\" + end + + return dir_separator +end + +---@brief calls vim.notify with a title and icon +---@param msg string the message to show +---@param log_level? integer the log level to show +function Utils.notify(msg, log_level) + vim.notify(msg, log_level or vim.log.levels.OFF, + { title = "doing.nvim", icon = "", }) end return Utils diff --git a/plugin/doing.lua b/plugin/doing.lua index 355bd87..5858f25 100644 --- a/plugin/doing.lua +++ b/plugin/doing.lua @@ -1,3 +1,4 @@ +local utils = require("doing.utils") local doing = require("doing") local do_cmds = { @@ -7,8 +8,7 @@ local do_cmds = { ["toggle"] = doing.toggle, ["status"] = function() - vim.notify(doing.status(true), vim.log.levels.INFO, - { title = "doing.nvim", icon = "", }) + utils.notify(doing.status(true)) end, } @@ -18,14 +18,16 @@ vim.api.nvim_create_user_command("Do", function(args) local cmd = args.args:sub(1, (args.args:find(" ") or (#args.args + 1)) - 1) local cmd_args = args.args:sub(#cmd + 2) or "" + -- checks if first argument is a Do command if vim.tbl_contains(vim.tbl_keys(do_cmds), cmd) then do_cmds[cmd](cmd_args, args.bang) - else + else -- otherwise, treat the the arguments as the task do_cmds["add"](args.args, args.bang) end end, { nargs = "?", bang = true, + -- sets up completion for the `:Do` command complete = function(_, cmd_line) local params = vim.split(cmd_line, "%s+", { trimempty = true, })