From 58d22d16f5c5fd419729ee71098eb64e2b89867f Mon Sep 17 00:00:00 2001
From: johnseth97 <17620345+johnseth97@users.noreply.github.com>
Date: Sun, 27 Apr 2025 19:51:54 -0400
Subject: [PATCH 1/3] fixing behavior of quit
---
README.md | 1 -
lua/gh_dash/init.lua | 128 ++++++++++++++++++++++++-------------------
2 files changed, 71 insertions(+), 58 deletions(-)
diff --git a/README.md b/README.md
index 0c9b6fb..bfcdfb0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,6 @@
# gh-dash Neovim Plugin
-
## A Neovim plugin integrating the open-source gh-dash TUI for the `gh` CLI ([gh-dash](https://github.com/dlvhdr/gh-dash/))
> Latest version: 
diff --git a/lua/gh_dash/init.lua b/lua/gh_dash/init.lua
index 7f439b9..b6c240b 100644
--- a/lua/gh_dash/init.lua
+++ b/lua/gh_dash/init.lua
@@ -1,10 +1,18 @@
-local vim = vim
-
local M = {}
local config = {
keymaps = {},
border = 'single',
+ custom_border = {
+ {}, -- Top left corner
+ {}, -- Top side
+ {}, -- Top right corner
+ {}, -- Right side
+ {}, -- Bottom right corner
+ {}, -- Bottom side
+ {}, -- Bottom left corner
+ {}, -- Left side
+ },
width = 0.8,
height = 0.8,
cmd = { 'gh', 'dash' },
@@ -33,6 +41,41 @@ function M.setup(user_config)
end
end
+local styles = {
+ single = {
+ { '╭', 'FloatBorder' },
+ { '─', 'FloatBorder' },
+ { '╮', 'FloatBorder' },
+ { '│', 'FloatBorder' },
+ { '╯', 'FloatBorder' },
+ { '─', 'FloatBorder' },
+ { '╰', 'FloatBorder' },
+ { '│', 'FloatBorder' },
+ },
+ double = {
+ { '╔', 'FloatBorder' },
+ { '═', 'FloatBorder' },
+ { '╗', 'FloatBorder' },
+ { '║', 'FloatBorder' },
+ { '╝', 'FloatBorder' },
+ { '═', 'FloatBorder' },
+ { '╚', 'FloatBorder' },
+ { '║', 'FloatBorder' },
+ },
+ square = {
+ { '┌', 'FloatBorder' },
+ { '─', 'FloatBorder' },
+ { '┐', 'FloatBorder' },
+ { '│', 'FloatBorder' },
+ { '┘', 'FloatBorder' },
+ { '─', 'FloatBorder' },
+ { '└', 'FloatBorder' },
+ { '│', 'FloatBorder' },
+ },
+ custom = config.custom_border,
+ none = nil,
+}
+
-- Create a floating window displaying the gh_dash buffer
local function open_window()
-- compute dimensions and position
@@ -43,40 +86,11 @@ local function open_window()
-- resolve border style (string or table)
local border = config.border
if type(border) == 'string' then
- local styles = {
- single = {
- { '╭', 'FloatBorder' },
- { '─', 'FloatBorder' },
- { '╮', 'FloatBorder' },
- { '│', 'FloatBorder' },
- { '╯', 'FloatBorder' },
- { '─', 'FloatBorder' },
- { '╰', 'FloatBorder' },
- { '│', 'FloatBorder' },
- },
- double = {
- { '╔', 'FloatBorder' },
- { '═', 'FloatBorder' },
- { '╗', 'FloatBorder' },
- { '║', 'FloatBorder' },
- { '╝', 'FloatBorder' },
- { '═', 'FloatBorder' },
- { '╚', 'FloatBorder' },
- { '║', 'FloatBorder' },
- },
- square = {
- { '┌', 'FloatBorder' },
- { '─', 'FloatBorder' },
- { '┐', 'FloatBorder' },
- { '│', 'FloatBorder' },
- { '┘', 'FloatBorder' },
- { '─', 'FloatBorder' },
- { '└', 'FloatBorder' },
- { '│', 'FloatBorder' },
- },
- none = nil,
- }
- border = styles[border] or styles.single
+ if border == 'none' then
+ border = 'none'
+ else
+ border = styles[border] or styles.single
+ end
end
-- open floating window
state.win = vim.api.nvim_open_win(state.buf, true, {
@@ -95,16 +109,13 @@ function M.open()
vim.api.nvim_set_current_win(state.win)
return
end
- if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
+ if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) or vim.api.nvim_buf_get_option(state.buf, 'modified') then
-- create an unlisted scratch buffer for the terminal
state.buf = vim.api.nvim_create_buf(false, false)
-- buffer options
vim.api.nvim_buf_set_option(state.buf, 'bufhidden', 'hide')
vim.api.nvim_buf_set_option(state.buf, 'swapfile', false)
vim.api.nvim_buf_set_option(state.buf, 'filetype', 'gh_dash')
- -- map in terminal and normal modes to close the gh_dash window
- vim.api.nvim_buf_set_keymap(state.buf, 't', '', [[lua require('gh_dash').close()]], { noremap = true, silent = true })
- vim.api.nvim_buf_set_keymap(state.buf, 'n', '', [[lua require('gh_dash').close()]], { noremap = true, silent = true })
end
open_window()
-- determine if config.cmd is a simple executable name (no args) for checking
@@ -120,40 +131,34 @@ function M.open()
-- if simple command and not found, handle auto-install or notify
if check_cmd and vim.fn.executable(check_cmd) == 0 then
if config.autoinstall then
- if vim.fn.executable 'npm' == 1 then
+ if vim.fn.executable 'gh' == 1 then
-- install via npm in the floating terminal to show output
do
local shell_cmd = vim.o.shell or 'sh'
local cmd = {
shell_cmd,
'-c',
- "echo 'Autoinstalling OpenAI gh_dash via npm...'; npm install -g @openai/gh_dash",
+ "echo 'Autoinstalling gh_dash via gh CLI extensions...'; gh extension install dlvhdr/gh-dash",
}
state.job = vim.fn.termopen(cmd, {
cwd = vim.loop.cwd(),
- on_exit = function(_, exit_code)
+ on_exit = function(_, exit_code, _)
+ state.job = nil
if exit_code == 0 then
- vim.notify('[gh_dash.nvim] gh_dash CLI installed successfully', vim.log.levels.INFO)
- -- automatically re-launch gh_dash CLI now that it's installed
vim.schedule(function()
M.close()
- state.buf = nil
- M.open()
end)
- else
- vim.notify('[gh_dash.nvim] failed to install gh_dash CLI', vim.log.levels.ERROR)
end
- state.job = nil
end,
})
end
else
-- show installation instructions in the gh_dash popup
local msg = {
- 'npm not found; cannot auto-install gh_dash CLI.',
+ 'gh CLI not found; cannot auto-install gh_dash extension.',
'',
- 'Please install via your system package manager, or manually run:',
- ' npm install -g @openai/gh_dash',
+ 'Please install the gh CLI via your system package manager',
+ 'i.e. `brew install gh`',
}
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, msg)
end
@@ -163,7 +168,7 @@ function M.open()
'gh_dash CLI not found.',
'',
'Install with:',
- ' npm install -g @openai/gh_dash',
+ 'gh extension install dlvhdr/gh-dash',
'',
'Or enable autoinstall in your plugin setup:',
' require("gh_dash").setup{ autoinstall = true }',
@@ -175,9 +180,14 @@ function M.open()
-- spawn the gh_dash CLI in the floating terminal buffer
if not state.job then
state.job = vim.fn.termopen(config.cmd, {
- cwd = vim.loop.cwd(),
- on_exit = function()
+ wd = vim.loop.cwd(),
+ on_exit = function(_, exit_code, _)
state.job = nil
+ if exit_code == 0 then
+ vim.schedule(function()
+ M.close()
+ end)
+ end
end,
})
end
@@ -188,6 +198,10 @@ function M.close()
vim.api.nvim_win_close(state.win, true)
state.win = nil
end
+ if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
+ vim.api.nvim_buf_delete(state.buf, { force = true })
+ state.buf = nil
+ end
end
function M.toggle()
@@ -218,7 +232,7 @@ function M.status()
return M.statusline() ~= ''
end,
-- gear icon
- icon = '',
+ icon = '',
-- default color (blue)
color = { fg = '#51afef' },
}
From bc33c8fd5e8d4b9f65eda296420784854b7bb08b Mon Sep 17 00:00:00 2001
From: johnseth97 <17620345+johnseth97@users.noreply.github.com>
Date: Sun, 27 Apr 2025 20:05:42 -0400
Subject: [PATCH 2/3] updated behavior to quit cleanly w/ q, background w/
---
lua/gh_dash/init.lua | 17 ++++++++++++++++-
tests/gh_dash_spec.lua | 35 ++++++++++++++++++++++-------------
2 files changed, 38 insertions(+), 14 deletions(-)
diff --git a/lua/gh_dash/init.lua b/lua/gh_dash/init.lua
index b6c240b..09d2900 100644
--- a/lua/gh_dash/init.lua
+++ b/lua/gh_dash/init.lua
@@ -116,6 +116,15 @@ function M.open()
vim.api.nvim_buf_set_option(state.buf, 'bufhidden', 'hide')
vim.api.nvim_buf_set_option(state.buf, 'swapfile', false)
vim.api.nvim_buf_set_option(state.buf, 'filetype', 'gh_dash')
+ -- Escape backgrounds the window cleanly
+ -- Map in terminal mode to hide the popup
+ vim.api.nvim_buf_set_keymap(
+ state.buf,
+ 't',
+ '',
+ [[lua vim.defer_fn(function() require('gh_dash').toggle() end, 10)]],
+ { noremap = true, silent = true }
+ )
end
open_window()
-- determine if config.cmd is a simple executable name (no args) for checking
@@ -206,8 +215,14 @@ end
function M.toggle()
if state.win and vim.api.nvim_win_is_valid(state.win) then
- M.close()
+ -- HIDE the window (don't kill the job)
+ vim.api.nvim_win_close(state.win, true)
+ state.win = nil
+ elseif state.buf and vim.api.nvim_buf_is_valid(state.buf) then
+ -- Reopen window into existing buffer
+ open_window()
else
+ -- Full open if everything is gone
M.open()
end
end
diff --git a/tests/gh_dash_spec.lua b/tests/gh_dash_spec.lua
index 531fd5b..5f437ad 100644
--- a/tests/gh_dash_spec.lua
+++ b/tests/gh_dash_spec.lua
@@ -1,13 +1,22 @@
-- tests/gh_dash_spec.lua
-- luacheck: globals describe it assert eq
-- luacheck: ignore a -- “a” is imported but unused
+-- tests/gh_dash_spec.lua
+-- luacheck: globals describe it assert eq
local a = require 'plenary.async.tests'
local eq = assert.equals
describe('gh_dash.nvim', function()
before_each(function()
- vim.cmd 'set noswapfile' -- prevent side effects
- vim.cmd 'silent! bwipeout!' -- close any open gh_dash windows
+ vim.cmd 'set noswapfile'
+ for _, buf in ipairs(vim.api.nvim_list_bufs()) do
+ if vim.api.nvim_buf_is_valid(buf) then
+ local ft = vim.api.nvim_buf_get_option(buf, 'filetype')
+ if ft == 'gh_dash' then
+ vim.api.nvim_buf_delete(buf, { force = true })
+ end
+ end
+ end
end)
it('loads the module', function()
@@ -20,7 +29,6 @@ describe('gh_dash.nvim', function()
it('creates gh_dash commands', function()
require('gh_dash').setup { keymaps = {} }
-
local cmds = vim.api.nvim_get_commands {}
assert(cmds['GHdash'], 'GHdash command not found')
assert(cmds['GHdashToggle'], 'GHdashToggle command not found')
@@ -32,15 +40,14 @@ describe('gh_dash.nvim', function()
local win = vim.api.nvim_get_current_win()
local buf = vim.api.nvim_win_get_buf(win)
- local ft = vim.api.nvim_buf_get_option(buf, 'filetype')
- eq(ft, 'gh_dash')
+ assert(vim.api.nvim_buf_is_valid(buf), 'buffer should exist')
+ eq(vim.api.nvim_buf_get_option(buf, 'filetype'), 'gh_dash')
require('gh_dash').close()
end)
it('toggles the window', function()
require('gh_dash').setup { cmd = "echo 'test'" }
-
require('gh_dash').toggle()
local win1 = vim.api.nvim_get_current_win()
assert(vim.api.nvim_win_is_valid(win1), 'gh_dash window should be open')
@@ -50,14 +57,16 @@ describe('gh_dash.nvim', function()
assert(not still_valid, 'gh_dash window should be closed')
end)
- it('shows statusline only when job is active but window is not', function()
- require('gh_dash').setup { cmd = 'sleep 1000' }
- require('gh_dash').open()
-
- vim.defer_fn(function()
+ it(
+ 'shows statusline only when job is active but window is not',
+ a.wrap(function()
+ require('gh_dash').setup { cmd = 'sleep 1' }
+ require('gh_dash').open()
require('gh_dash').close()
+ vim.wait(100)
+
local status = require('gh_dash').statusline()
eq(status, '[gh_dash]')
- end, 100)
- end)
+ end, 3000)
+ )
end)
From 59d79982082817729ff444147118228b860d454a Mon Sep 17 00:00:00 2001
From: johnseth97 <17620345+johnseth97@users.noreply.github.com>
Date: Sun, 27 Apr 2025 20:41:55 -0400
Subject: [PATCH 3/3] updated tests structure to use makefile.
---
.github/workflows/ci.yml | 33 +++++++--------------------------
.gitignore | 2 ++
.luacheckrc | 9 ++++-----
makefile | 25 +++++++++++++++++++++++++
tests/gh_dash_spec.lua | 39 +++++++++++++++++++++++----------------
tests/minimal_init.lua | 1 +
6 files changed, 62 insertions(+), 47 deletions(-)
create mode 100644 .gitignore
create mode 100644 makefile
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a29dfb7..066d8fe 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,25 +21,18 @@ jobs:
- name: Install Neovim ${{ matrix.neovim }}
run: |
set -euo pipefail
-
VERSION=${{ matrix.neovim }}
-
- # All historic 0.x tags use nvim-linux64.tar.gz, everything newer uses -x86_64
if [[ "$VERSION" =~ ^v0\.[0-9]+\. ]]; then
FILENAME=nvim-linux64.tar.gz
else
FILENAME=nvim-linux-x86_64.tar.gz
fi
-
URL="https://github.com/neovim/neovim/releases/download/${VERSION}/${FILENAME}"
echo "Downloading $URL"
-
- curl -fL -o nvim.tar.gz "$URL" # -f = fail on 4xx/5xx, -L = follow redirects
-
+ curl -fL -o nvim.tar.gz "$URL"
mkdir nvim-extract
tar -xzf nvim.tar.gz -C nvim-extract
-
- DIR="$(ls nvim-extract | head -n1)" # should be nvim-linux64 or nvim-linux-x86_64
+ DIR="$(ls nvim-extract | head -n1)"
sudo mv "nvim-extract/$DIR" /opt/nvim
echo "/opt/nvim/bin" >> "$GITHUB_PATH"
@@ -55,32 +48,20 @@ jobs:
run: |
set -euo pipefail
sudo apt-get update
- sudo apt-get install -y lua5.1 liblua5.1-0-dev luarocks
+ sudo apt-get install -y lua5.1 liblua5.1-0-dev luarocks make
luarocks --lua-version=5.1 --local install luacheck
luarocks --lua-version=5.1 --local install luacov
luarocks --lua-version=5.1 --local install luacov-reporter-lcov
- echo "$HOME/.luarocks/bin" >> $GITHUB_PATH
+ echo "$HOME/.luarocks/bin" >> "$GITHUB_PATH"
- - name: Run tests with coverage
+ - name: Run tests and generate coverage
run: |
eval "$(luarocks --lua-version=5.1 path)"
- luarocks --lua-version=5.1 list # debug
- nvim --headless -u tests/minimal_init.lua -c "luafile tests/run_cov.lua"
-
- luacov -r lcov > lcov.info
- sed -i 's|SF:.*/codex.nvim/codex.nvim/|SF:|g' lcov.info
- head -n 10 lcov.info # debug
-
- # Debug output
- echo "=== first 20 lines of lcov.info ==="
- head -n 20 lcov.info
+ make coverage
- echo "=== all source-file entries ==="
- grep '^SF:' lcov.info | sed -e 's/^SF://g' | sort | uniq | head -n 10
-
- name: Upload code coverage
uses: codecov/codecov-action@v4
with:
- files: lcov.info # <-- new file
+ files: lcov.info
disable_search: true
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..109b360
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+luacov.report.out
+luacov.stats.out
diff --git a/.luacheckrc b/.luacheckrc
index f43584d..e6be2e2 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -1,9 +1,8 @@
-- .luacheckrc
-std = "luajit"
-globals = { "vim" }
+std = 'luajit'
+globals = { 'vim', 'describe', 'it', 'before_each', 'after_each', 'pending', 'assert', 'eq' }
ignore = {
- "plugin/*", -- plugin loader shim
+ 'plugin/*', -- plugin loader shim
}
-max_line_length = false -- or turn it off completely
-
+max_line_length = false -- or turn it off completely
diff --git a/makefile b/makefile
new file mode 100644
index 0000000..dd186a5
--- /dev/null
+++ b/makefile
@@ -0,0 +1,25 @@
+# Makefile for gh_dash.nvim testing and coverage
+# Usage:
+# make test - run unit tests
+# make coverage - run tests + generate coverage (luacov + lcov.info)
+
+# Force correct Lua version for Neovim (Lua 5.1)
+LUAROCKS_ENV = eval "$(luarocks --lua-version=5.1 path)"
+
+# Headless Neovim test runner
+NVIM_TEST_CMD = nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests/"
+
+.PHONY: test coverage clean
+
+test:
+ $(LUAROCKS_ENV) && $(NVIM_TEST_CMD)
+
+coverage:
+ $(LUAROCKS_ENV) && nvim --headless -u tests/minimal_init.lua -c "luafile tests/run_cov.lua"
+ ls -lh luacov.stats.out
+ $(LUAROCKS_ENV) && luacov -t LcovReporter
+ @echo "Generated coverage report: lcov.info"
+
+clean:
+ rm -f luacov.stats.out lcov.info
+ @echo "Cleaned coverage artifacts"
diff --git a/tests/gh_dash_spec.lua b/tests/gh_dash_spec.lua
index 5f437ad..7445939 100644
--- a/tests/gh_dash_spec.lua
+++ b/tests/gh_dash_spec.lua
@@ -1,9 +1,4 @@
-- tests/gh_dash_spec.lua
--- luacheck: globals describe it assert eq
--- luacheck: ignore a -- “a” is imported but unused
--- tests/gh_dash_spec.lua
--- luacheck: globals describe it assert eq
-local a = require 'plenary.async.tests'
local eq = assert.equals
describe('gh_dash.nvim', function()
@@ -57,16 +52,28 @@ describe('gh_dash.nvim', function()
assert(not still_valid, 'gh_dash window should be closed')
end)
- it(
- 'shows statusline only when job is active but window is not',
- a.wrap(function()
- require('gh_dash').setup { cmd = 'sleep 1' }
- require('gh_dash').open()
- require('gh_dash').close()
- vim.wait(100)
+ pending('hides the window on but keeps job alive', function()
+ -- this test is not reliable in headless mode, skipping for now
+ --
+ -- require('gh_dash').setup { cmd = 'echo "test"' }
+ -- require('gh_dash').open()
+ --
+ -- local win = vim.api.nvim_get_current_win()
+ -- vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('', true, false, true), 't', true)
+ --
+ -- local win_still_valid = pcall(vim.api.nvim_win_get_buf, win)
+ -- assert(not win_still_valid, ' should hide the window')
+ --
+ -- local status = require('gh_dash').statusline()
+ -- eq(status, '[gh_dash]', 'job should still be running after hiding window')
+ end)
+
+ it('shows statusline only when job is active but window is not', function()
+ require('gh_dash').setup { cmd = 'echo "test"' }
+ require('gh_dash').open()
+ require('gh_dash').close()
- local status = require('gh_dash').statusline()
- eq(status, '[gh_dash]')
- end, 3000)
- )
+ local status = require('gh_dash').statusline()
+ eq(status, '[gh_dash]')
+ end)
end)
diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua
index a050c77..ee16f20 100644
--- a/tests/minimal_init.lua
+++ b/tests/minimal_init.lua
@@ -1,3 +1,4 @@
vim.cmd 'set rtp+=.'
vim.cmd 'set rtp+=./plenary.nvim' -- if using as a submodule or symlinked
require 'plugin.gh_dash' -- triggers plugin/gh_dash.lua
+vim.opt.runtimepath:append '~/.local/share/nvim/lazy/plenary.nvim/'