summary refs log tree commit diff
path: root/.config/nvim/lua/mod
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2026-09-27 17:38:39 +0200
committerRobert Günzler <r@gnzler.io>2026-09-27 17:38:39 +0200
commit0860db73ae6fd9a6f6347219d820bf026a6b8b91 (patch)
tree11318c3670d1c32c5aa0bf404c22fba3bf63fc53 /.config/nvim/lua/mod
parent4e34cbf9673aeaba252e01df554a0d204e83c5e2 (diff)
downloaddotfiles-master.tar.gz
Automated-commit-by: dots
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to '.config/nvim/lua/mod')
-rw-r--r--.config/nvim/lua/mod/gh.lua86
-rw-r--r--.config/nvim/lua/mod/git.lua23
-rw-r--r--.config/nvim/lua/mod/jobs.lua68
-rw-r--r--.config/nvim/lua/mod/termopen.lua102
-rw-r--r--.config/nvim/lua/mod/utils.lua15
-rw-r--r--.config/nvim/lua/mod/win.lua110
6 files changed, 404 insertions, 0 deletions
diff --git a/.config/nvim/lua/mod/gh.lua b/.config/nvim/lua/mod/gh.lua
new file mode 100644
index 0000000..1f8a267
--- /dev/null
+++ b/.config/nvim/lua/mod/gh.lua
@@ -0,0 +1,86 @@
+local M = {}
+
+-- gh api \
+--   --method POST \
+--   -H "Accept: application/vnd.github+json" \
+--   -H "X-GitHub-Api-Version: 2022-11-28" \
+--   /repos/OWNER/REPO/pulls/PULL_NUMBER/comments \
+
+function M.pr_comment(repo, pull, commit_id, comment)
+  vim.print(vim.inspect(comment))
+  vim.print(vim.inspect({
+    'gh',
+    'api',
+    '--method',
+    'POST',
+    string.format('/repos/%s/pulls/%d/comments', repo, pull),
+    '-f',
+    'body=' .. comment.body,
+    '-f',
+    'commit_id=' .. commit_id,
+    '-f',
+    'path=' .. comment.path,
+    '-f',
+    'side=RIGHT',
+    '-f',
+    'line=' .. comment.line,
+  }))
+end
+
+function M.pr_view()
+  local p = vim
+    .system({
+      'gh',
+      'pr',
+      'view',
+      '--json',
+      'number,headRepository,headRefOid',
+      '--jq',
+      '{head:.headRefOid,repo:.headRepository.nameWithOwner,pull:.number}',
+    })
+    :wait()
+  return vim.json.decode(p.stdout)
+end
+
+function M.submit_review()
+  local gitroot = vim.fs.root(0, { '.git' })
+  if not gitroot then
+    vim.notify('Not a git repository.', vim.log.levels.WARN)
+    return
+  end
+
+  local review = { comments = {} }
+
+  local marks = require('haunt.api').get_bookmarks()
+  for _, mark in ipairs(marks) do
+    -- nil for bookmarks outside the current repo
+    local path = vim.fs.relpath(gitroot, mark.file)
+    if path then
+      review.comments[#review.comments] = {
+        path = path,
+        line = mark.line,
+        body = mark.note,
+      }
+    end
+  end
+
+  review.pr = M.pr_view()
+
+  vim.print(vim.inspect(review))
+
+  vim.ui.select({
+    'OK',
+    'Cancel',
+  }, {
+    prompt = 'Submit review?',
+  }, function(choice)
+    vim.print(choice)
+    if choice == 'OK' then
+      for _, comment in ipairs(review.comments) do
+        M.pr_comment(pr.repo, pr.pull, pr.head, comment)
+      end
+    end
+  end)
+end
+
+return M
diff --git a/.config/nvim/lua/mod/git.lua b/.config/nvim/lua/mod/git.lua
new file mode 100644
index 0000000..3e36e61
--- /dev/null
+++ b/.config/nvim/lua/mod/git.lua
@@ -0,0 +1,23 @@
+local M = {}
+
+function M.commit()
+  local commit = vim.fn.expand('<cword>') -- XXX: can we do better?
+  vim.cmd.DiffviewOpen(commit .. '^!')
+end
+
+function M.log()
+  local bufnr = vim.api.nvim_create_buf(true, true)
+  assert(bufnr ~= 0, 'Failed to create buffer')
+
+  vim.bo[bufnr].bufhidden = 'wipe'
+  vim.bo[bufnr].modifiable = true
+  vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, content)
+  vim.bo[bufnr].modifiable = false
+
+  vim.keymap.set('n', 'q', '<cmd>quit!<cr>', { silent = true, buffer = bufnr })
+  vim.keymap.set('n', 'K', M.commit, { silent = true, buffer = bufnr })
+
+  vim.api.nvim_win_set_buf(0, bufnr)
+end
+
+return M
diff --git a/.config/nvim/lua/mod/jobs.lua b/.config/nvim/lua/mod/jobs.lua
new file mode 100644
index 0000000..a4dea6f
--- /dev/null
+++ b/.config/nvim/lua/mod/jobs.lua
@@ -0,0 +1,68 @@
+-- this plugin provides an API to register and recall jobs.
+
+local M = {
+  _jobs = {},
+}
+
+local function format_jobcmd(cmd)
+  if type(cmd) == 'table' then
+    return table.concat(cmd, ' ')
+  end
+  return cmd
+end
+
+local function register(id, cmd)
+  table.insert(M._jobs, id, cmd)
+  vim.notify_once(string.format('started job %d (%s)', id, format_jobcmd(M._jobs[id])))
+end
+
+local function deregister(id)
+  local cmd = M._jobs[id]
+  table.insert(M._jobs, id, nil)
+  vim.notify_once(string.format('stopped job %d (%s)', id, format_jobcmd(cmd)))
+end
+
+function M.start(opts)
+  local previous_on_exit = opts.on_exit or function() end
+  opts.on_exit = function(id, exitcode, event)
+    deregister(id)
+    return previous_on_exit(id, exitcode, event)
+  end
+
+  local id = vim.fn.jobstart(opts.cmd, opts)
+
+  register(id, opts.cmd)
+end
+
+function M.select()
+  if #M._jobs == 0 then
+    vim.notify('No running jobs', vim.log.levels.WARN)
+    return
+  end
+
+  local function provider(cb)
+    for id, cmd in pairs(M._jobs) do
+      cb(string.format('[%d] %s', id, format_jobcmd(cmd)))
+    end
+    cb(nil)
+  end
+  local function parse_jobid(entry)
+    return tonumber(string.match(entry, '%[([0-9]*)%]'))
+  end
+
+  require('fzf-lua').fzf_exec(provider, {
+    prompt = 'Jobs❯',
+    actions = {
+      ['ctrl-x'] = {
+        fn = function(selected)
+          for _, entry in pairs(selected) do
+            vim.fn.jobstop(parse_jobid(entry))
+          end
+        end,
+        reload = true,
+      },
+    },
+  })
+end
+
+return M
diff --git a/.config/nvim/lua/mod/termopen.lua b/.config/nvim/lua/mod/termopen.lua
new file mode 100644
index 0000000..f8117f1
--- /dev/null
+++ b/.config/nvim/lua/mod/termopen.lua
@@ -0,0 +1,102 @@
+-- this plugin provides a popup terminal.
+local jobs = require('mod.jobs')
+
+local M = {}
+
+function M.termopen(opts)
+  opts = opts or {}
+
+  -- setup the buffer
+  local buf = vim.api.nvim_create_buf(false, true)
+  vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = buf })
+  vim.api.nvim_set_option_value('modifiable', false, { buf = buf })
+
+  -- setup keymappings
+  vim.keymap.set('n', 'q', '<cmd>close<cr>', { buffer = buf })
+  vim.keymap.set('n', '<esc>', '<cmd>close<cr>', { buffer = buf })
+
+  -- setup the window
+  -- local w = math.ceil(vim.o.columns * 1.0)
+  -- local h = math.ceil(vim.o.lines * 0.4)
+  local win = vim.api.nvim_open_win(buf, true, {
+    -- style = 'minimal',
+    -- relative = 'editor',
+    -- width = w,
+    -- height = h,
+    -- row = math.ceil(vim.o.lines - h),
+    -- col = vim.o.columns,
+    -- border = { ' ', '─', ' ', ' ', ' ', '─', ' ', ' ' },
+    split = 'below',
+    win = -1,
+  })
+  vim.api.nvim_set_current_win(win)
+
+  -- spawn the command
+  if #opts.cmd == 0 then
+    opts.cmd = { vim.env.SHELL }
+  end
+
+  -- vim.notify('termopen: running ' .. vim.inspect(opts.cmd), vim.log.levels.DEBUG)
+
+  jobs.start(vim.tbl_deep_extend('error', opts, {
+    term = true,
+    env = {
+      -- we need this to avoid opening in the term buffer
+      NVR_PRE_CMD = 'close',
+    },
+    on_exit = function()
+      -- if vim.api.nvim_win_is_valid(win) then
+      --   vim.api.nvim_win_close(win, true)
+      -- end
+    end,
+  }))
+
+  -- put into insert mode
+  vim.cmd.startinsert()
+end
+
+-- wrappers convenience
+
+function M.term(a)
+  local cwd = nil
+  local args = a.fargs
+
+  if #args > 0 then
+    if args[1]:sub(0, 4) == 'cwd=' then
+      cwd = args[1]:sub(5)
+      args = vim.list_slice(args, 2)
+    end
+  end
+
+  for i = 0, #args do
+    if args[i] == '%' then
+      args[i] = vim.fn.expand(args[i])
+    end
+  end
+
+  M.termopen({ cmd = args, cwd = cwd })
+end
+
+function M.make(a)
+  if vim.opt_local.makeprg == nil then
+    vim.ui.input({ prompt = 'Enter makeprg: ' }, function(input)
+      vim.opt_local.makeprg = input
+    end)
+  end
+
+  local makeprg = vim.opt_local.makeprg:get()
+  if string.match(makeprg, [[%$%*]]) == nil then
+    makeprg = makeprg .. ' ' .. a.args
+  end
+
+  local cmd = vim.split(makeprg, ' ')
+  cmd = vim.tbl_map(function(p)
+    p, _ = string.gsub(p, [[%$%*]], a.args)
+    p, _ = vim.fn.expand(p)
+    return p
+  end, cmd)
+
+  M.termopen({ cmd = cmd })
+end
+
+return M
diff --git a/.config/nvim/lua/mod/utils.lua b/.config/nvim/lua/mod/utils.lua
new file mode 100644
index 0000000..1f3cd8b
--- /dev/null
+++ b/.config/nvim/lua/mod/utils.lua
@@ -0,0 +1,15 @@
+local M = {}
+
+function M.buf_edit_lines(buf, start_row, end_row, callback)
+  local lines = {}
+  lines = vim.api.nvim_buf_get_lines(buf, start_row, end_row, true)
+  lines = callback(lines)
+  return vim.api.nvim_buf_set_lines(buf, start_row, end_row, true, lines)
+end
+
+function M.edit_current_line(callback)
+  local row, _ = unpack(vim.api.nvim_win_get_cursor(0))
+  return M.buf_edit_lines(0, row - 1, row, callback)
+end
+
+return M
diff --git a/.config/nvim/lua/mod/win.lua b/.config/nvim/lua/mod/win.lua
new file mode 100644
index 0000000..e5c5f71
--- /dev/null
+++ b/.config/nvim/lua/mod/win.lua
@@ -0,0 +1,110 @@
+local M = {}
+
+function M.open(name, content, opts)
+  if name == '' then
+    return
+  end
+  if #content == 0 then
+    return
+  end
+
+  local opts = opts or {}
+  local autoclose = opts.autoclose or false
+  local buf_opts = opts.buf or { buftype = '' }
+
+  local win_opts = opts.win or false
+  -- profiles
+  if type(win_opts) == 'table' and type(win_opts[1] == 'string') then
+    local profile = win_opts[1]
+    local overwrites = unpack(win_opts, 2)
+    if profile == 'emplace' then
+      win_opts = false
+    elseif profile == 'smallsplit' then
+      win_opts = {
+        split = 'above',
+        win = 0,
+        height = 20,
+      }
+    elseif profile == 'popup' then
+      local winh = vim.api.nvim_win_get_height(0)
+      local winw = vim.api.nvim_win_get_width(0)
+      local winsize = 0.5
+      win_opts = {
+        relative = 'win',
+        height = math.floor(winh * winsize),
+        width = math.floor(winw * winsize),
+        row = math.floor((winh * 0.5) - (0.5 * (winh * winsize))),
+        col = math.floor((winw * 0.5) - (0.5 * (winw * winsize))),
+        style = 'minimal',
+        border = 'single',
+        zindex = 99,
+        title = name,
+      }
+    end
+    if overwrites then
+      for k, v in pairs(overwrites) do
+        win_opts[k] = v
+      end
+    end
+  end
+
+  local bufnr = vim.api.nvim_create_buf(false, true)
+  assert(bufnr ~= 0, 'Failed to create buffer')
+
+  vim.bo[bufnr].modifiable = true
+  vim.api.nvim_buf_set_name(bufnr, name)
+  vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, content)
+  for k, v in pairs(buf_opts) do
+    vim.bo[bufnr][k] = v
+  end
+
+  -- without win_opts (by default) replace current buffer
+  local winid = 0
+  if win_opts then
+    winid = vim.api.nvim_open_win(0, not autoclose, win_opts)
+    assert(winid ~= 0, 'Failed to create window')
+  end
+
+  vim.keymap.set('n', 'q', '<cmd>quit!<cr>', { silent = true, buffer = bufnr })
+
+  if autoclose then
+    local group = 'modwin_' .. vim.base64.encode(vim.uv.random(16))
+    local group_id = vim.api.nvim_create_augroup(group, {})
+
+    vim.api.nvim_create_autocmd('WinClosed', {
+      pattern = tostring(winid),
+      group = group_id,
+      callback = function()
+        vim.api.nvim_create_augroup(group, {})
+        M.winid = nil
+      end,
+    })
+    vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, {
+      group = group_id,
+      callback = function()
+        vim.api.nvim_create_augroup(group, {})
+        pcall(vim.api.nvim_win_close, winid, true)
+      end,
+    })
+  end
+
+  vim.api.nvim_win_set_buf(winid, bufnr)
+  return { bufnr, winid }
+end
+
+function M.popup(name, content, opts)
+  local defaults = {
+    autoclose = true,
+    buf = {
+      bufhidden = 'wipe',
+      buftype = 'nofile',
+      modifiable = false,
+    },
+    win = { 'popup' },
+  }
+  opts = opts or {}
+  opts = vim.tbl_deep_extend('force', defaults, opts)
+  M.open(name, content, opts)
+end
+
+return M