diff --git a/README.md b/README.md index 33214ee..78913b2 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,25 @@ return { build = function(_) vim.cmd "UpdateRemotePlugins" end, } ``` + +## Setup + +### Shell Configuration + +#### Environment Variables + +CLICKUP_AUTH +: user api token for clickup + +CLICKUP_USER_ID +: user id for clickup + +CLICKUP_WORKSPACE_ID +: workspace id for clickup + + +## Commands + +### `:MrPy` + +- start task selector of user's tasks (not done/closed) diff --git a/lua/mrpy.lua b/lua/mrpy.lua new file mode 100644 index 0000000..969c263 --- /dev/null +++ b/lua/mrpy.lua @@ -0,0 +1,295 @@ +local curl = require("plenary.curl") +local M = {} + +---@param data table +---@return string | nil +M.table_to_yaml = function(data) + local json = vim.json.encode(data) + + -- Nutzt 'yq' um JSON zu YAML zu konvertieren + local handle = io.popen("echo '" .. json .. "' | yq -P e '. ' -") + local result = handle:read("*a") + handle:close() + + if not result then + print("Error: yq not installed or got invalid json data") + return nil + end + return result +end + +---@param yaml_string string +---@return table | nil +M.yaml_to_table = function(yaml_string) + -- 1. YAML String in yq einspeisen und JSON ausgeben lassen + -- Das Flag -o=json sorgt für die Konvertierung + local temp_file = os.tmpname() + local temp_file_handle = io.open(temp_file, "w+b") + if not temp_file_handle then + print("Error: could not open temp file") + print(" temp_file: " .. temp_file) + return nil + end + temp_file_handle:write(yaml_string) + temp_file_handle:flush() + temp_file_handle:close() + + local cmd = "yq '.' " .. temp_file + local handle, errres = io.popen(cmd, 'r') + local json_result = handle:read("*a") + handle:close() + + os.remove(temp_file) + + -- 2. Fehlerprüfung + if json_result == "" then + print("Error: yq not installed or got invalid yaml data") + print(" cmd: " .. cmd) + print(" err: " .. errres) + return nil + end + + -- 3. JSON in Lua-Table umwandeln + local ok, table_result = pcall(vim.json.decode, json_result) + if not ok then + print("Error: failed to decode json") + print(" cmd: " .. cmd) + print(" json: " .. json_result) + return nil + end + + return table_result +end + +---@class ClickupSession +---@field auth string +---@field user string +---@field workspace string +---@field base_url string + +---@type ClickupSession +M.session = nil + +---@class ClickupRef +---@field name string +---@field id string + +---@class ClickupTask +---@field id string +---@field name string +---@field tags? table[] +---@field locations? table[] +---@field list? ClickupRef +---@field parent? string | nil +---@field [string] string + +---@return ClickupTask[] +M.latest_tasks = function() + local resp = curl.get(M.session.base_url .. + "/team/" .. M.session.workspace .. "/task?subtasks=true&include_markdown_description=true&assignees[]=" .. + M.session.user, { + headers = { + ['Authorization'] = M.session.auth, + ["accept"] = "application/json", + ["content-type"] = "application/json", + } + }) + + if resp.status == 200 then + return vim.json.decode(resp.body).tasks + end + + print("failed http request: " .. tostring(resp.status) .. " (" .. resp.body .. ")") + return {} +end + +---@param task ClickupTask +---@return ClickupTask +M._update_task = function(task) + local update_table = {} + + for _, k in ipairs({ "name", "markdown_content", "status" }) do + if task[k] then + update_table[k] = task[k] + end + end + + local resp = curl.put(M.session.base_url .. "/task/" .. task.id, { + headers = { + ['Authorization'] = M.session.auth, + ["accept"] = "application/json", + ["content-type"] = "application/json", + }, + body = vim.json.encode(update_table) + }) + + if resp.status ~= 200 then + error("failed to update " .. task.id .. "\n(" .. resp.body .. ")") + end + + return vim.json.decode(resp.body) +end + +---@param args vim.api.keyset.create_autocmd.callback_args +M._on_tempbuf_write = function(args) + local lines = vim.api.nvim_buf_get_lines(args.buf, 0, -1, false) + ---@type string[] + local parts = {} + ---@type string[] + local current = {} + for _, line in ipairs(lines) do + if line == "---" then + if #current then + table.insert(parts, table.concat(current, "\n")) + current = {} + end + else + table.insert(current, line) + end + end + table.insert(parts, table.concat(current, "\n")) + ---@type ClickupTask + local data = vim.json.decode(parts[2]) + data['markdown_content'] = string.gsub(parts[3], "%-%-%-\n", "") + + M._update_task(data) + + vim.api.nvim_set_option_value("modified", false, { buf = args.buf }) +end + +---@class SelectionItem +---@field status string +---@field parent string | nil +---@field list string +---@field name string +---@field description string +---@field tags string[] +---@field id string + +---@param item SelectionItem +M._on_select_task = function(picker, item) + local new_name = "[ClickUp] " .. item.name + local new_buf_no = vim.api.nvim_create_buf(true, false) + + local status, _ = pcall(vim.api.nvim_buf_set_name, new_buf_no, new_name) + if status then + -- vim.api.nvim_set_option_value("buftype", "nofile", { buf = new_buf_no }) + vim.api.nvim_set_option_value("filetype", "markdown", { buf = new_buf_no }) + vim.api.nvim_create_autocmd({ "BufWriteCmd" }, { buffer = new_buf_no, callback = M._on_tempbuf_write }) + local content = { "---", "{" } + for _, k in ipairs({ "id", "name", "status", "tags", "list", "parent" }) do + if content[#content] ~= "{" then + content[#content] = content[#content] .. "," + end + table.insert(content, ' "' .. k .. '": ' .. vim.json.encode(item[k])) + end + table.insert(content, "}") + table.insert(content, "---") + for line in string.gmatch(item.description, "[^\r\n]+") do + table.insert(content, line) + end + vim.api.nvim_buf_set_lines(new_buf_no, 0, 0, false, content) + vim.api.nvim_set_option_value("modified", false, { buf = new_buf_no }) + vim.api.nvim_win_set_buf(0, new_buf_no) + else + vim.api.nvim_buf_delete(new_buf_no, {}) + picker:close() + end +end + +---@param item SelectionItem +M._item_format = function(item, _) + local ret = {} + + local hl = "SnacksPickerComment" + if item.status == "in progress" then + hl = "@method" + elseif item.status == "selected for development" then + hl = "@constant.builtin" + elseif item.status == "in review" then + hl = "@keyword" + elseif item.status == "done" then + hl = "@variable.builtin" + end + + if item.parent ~= vim.NIL then + ret[#ret + 1] = { "󰘍 ", "SnacksPickerComment" } + end + + ret[#ret + 1] = { item.name, hl } + + for _, v in ipairs(item.tags) do + ret[#ret + 1] = { " #" .. v, "SnacksPickerComment" } + end + + ret[#ret + 1] = { + " 󰻾" .. item.id, + "SnacksPickerComment" + } + return ret +end + +---@param list table[] +---@param keys string[] +M.retrieve_subkeys = function(list, keys) + local ret = {} + local interm = list + + for _, key in ipairs(keys) do + ret = {} + for _, item in ipairs(interm) do + table.insert(ret, item[key]) + end + interm = ret + end + + return ret +end + +---@param data vim.api.keyset.create_user_command.command_args +M.select_task = function(data) + local tasks = M.latest_tasks() + + ---@type SelectionItem[] + local items = {} + + for tix, t in ipairs(tasks) do + if string.sub(t.name, -7, -1) ~= "Absence" then + local preview_frontmatter = "" + + ---@type SelectionItem + local prepared = { + idx = tix, + id = t.id, + text = t.name .. t.id .. t.markdown_description, + name = t.name, + tags = M.retrieve_subkeys(t.tags, { "name" }), + status = t.status.status, + parent = t.parent, + list = t.list.name, + description = t.markdown_description, + preview = { + ft = "markdown", + }, + action = M._on_select_task + } + + for _, k in ipairs({ "id", "name", "status", "tags", "parent" }) do + preview_frontmatter = preview_frontmatter .. "\n" .. k .. ": " .. vim.json.encode(prepared[k]) + end + + prepared.preview.text = "---" .. preview_frontmatter .. "\n---\n" .. t.markdown_description + table.insert(items, #items + 1, prepared) + end + end + + require("snacks.picker").pick({ + title = "Select Task", + format = M._item_format, + preview = "preview", + confirm = M._on_select_task, + items = items + }) +end + +return M diff --git a/plugin/mrpy.lua b/plugin/mrpy.lua new file mode 100644 index 0000000..2e4334e --- /dev/null +++ b/plugin/mrpy.lua @@ -0,0 +1,10 @@ +local mrpy = require("mrpy") + +vim.api.nvim_create_user_command("MrPy", mrpy.select_task, {}) + +mrpy.session = { + auth = os.getenv("CLICKUP_AUTH") or "", + user = os.getenv("CLICKUP_USER_ID") or "", + workspace = os.getenv("CLICKUP_WORKSPACE_ID") or "", + base_url = "https://api.clickup.com/api/v2", +}