local curl = require("plenary.curl") local notify = require("snacks.notifier").notify local gitlab = require("eta.gitlab") local clickup = require("eta.clickup") 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 -y") 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 = "cat " .. temp_file .. " | yq" local handle, errres = io.popen(cmd, 'r') local json_result = handle:read("*a") handle:close() -- 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 os.remove(temp_file) return table_result end ---@type eta.clickup.Session M.clickup_session = nil ---@type eta.gitlab.Session M.gitlab_session = nil ---@param task eta.clickup.Task ---@return eta.clickup.Task 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.clickup_session.base_url .. "/task/" .. task.id, { headers = { ['Authorization'] = M.clickup_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 eta.clickup.Task local data = M.yaml_to_table(parts[2]) or {} data['markdown_content'] = string.gsub(parts[3], "%-%-%-\n", "") M._update_task(data) vim.api.nvim_set_option_value("modified", false, { buf = args.buf }) local notif_id = notify("updated task #" .. data.id, "info", { title = "ETA", style = 'fancy' }) 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 picker snacks.Picker | nil ---@param item SelectionItem M._on_select_task = function(picker, item) local notif_id = notify("opening task #" .. item.id, "info", { title = "ETA", style = 'fancy' }) 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 = { "---"} local to_dump = {} for _, k in ipairs({ "id", "name", "status", "tags", "list", "parent", "dependencies" }) do to_dump[k] = item[k] end local yaml_string = M.table_to_yaml(to_dump) or "" for line in yaml_string:gmatch("([^\n]*)\n?") do table.insert(content, line) 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, {}) if picker then picker:close() end local to_open = vim.fn.bufnr(new_name) vim.api.nvim_win_set_buf(0, to_open) 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 id string M.open_task = function(id) if id:match("[a-z0-9]+") then local notif_id = notify("opening #".. id, "info", { timeout = 1000, title = "ETA", style = 'fancy' }) local t = clickup.task(M.clickup_session, id) ---@type SelectionItem local item = { idx = nil, 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 = nil } pcall(M._on_select_task,nil, item) else error("invalid id format") end end ---@param data vim.api.keyset.create_user_command.command_args M.select_task = function(data) local notif_id = notify("fetching tasks", "warn", { timeout = 10000, title = "ETA", style = 'fancy' }) if not pcall(M.open_task,vim.fn.expand("")) then local tasks = clickup.latest_tasks(M.clickup_session) ---@type SelectionItem[] local items = {} for tix, t in ipairs(tasks) do if string.sub(t.name, -7, -1) ~= "Absence" then local preview_frontmatter = "" local deps = {} for _, dependency in ipairs(t.dependencies) do table.insert(deps, dependency.depends_on) end ---@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, dependencies = deps, 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.notifier").hide(notif_id) require("snacks.picker").pick({ title = "Select Task", format = M._item_format, preview = "preview", confirm = M._on_select_task, items = items }) end end ---@param data vim.api.keyset.create_user_command.command_args M.mr_from_task = function(data) if data.args then notify(data.args, "info", { title = "ETA" }) end end return M