feat: modal bar labels, all-emoji icons, picker focus, dapui gutter cleanup

Control bar refinements:
- All-emoji icons, supplementary plane only (plus ♻️ with VS16 per user
  preference). Dropped Nerd Font Codicons — they rendered as unrelated
  glyphs in the user's font (debug-start as a gear, step-into arrow
  backwards, etc.). Replaced set:
    🚀 start / 🟢 continue (dynamic), 👣 step over, 🔽 descend,
    🔼 return, ♻️ restart, 🛑 quit/terminate, 💡 help.
- Modal label text — the tail after the (key) updates based on dap state:
    no session:  "(c) start"   / "(q)uit"
    active:      "(c)ontinue"  / "(q) terminate"
  So the label always shows what pressing the key will actually do.
- Help icon switched from ❓ to 💡 — ❓ (U+2753) was rendering zero-width
  in the user's font so the help label effectively disappeared. 💡 is
  supplementary-plane and renders reliably.

Launch picker focus fix:
- run_in_source() now calls nvim_set_current_win instead of nvim_win_call.
  nvim_win_call returns focus to the caller's window synchronously, so
  when dap.continue() triggered an async vim.ui.select picker, the picker
  opened but no window held focus — user had to click into it manually.
  Shifting focus directly means the picker inherits source-window focus.

Bar buffer robustness:
- buftype=nofile + BufEnter autocmd that bounces focus back if anything
  accidentally lands in the bar. Prevents E21 "Cannot make changes,
  'modifiable' is off" that the user hit after a mouse drag landed focus
  in the bar and their next keypress tried to edit.

dapui pane gutter cleanup:
- set_dapui_window_opts now forces number=false, relativenumber=false,
  signcolumn=no, statuscolumn="", foldcolumn=0, and sets fillchars=eob:
  to hide the `~` end-of-buffer marks. snacks statuscolumn was rendering
  a gutter with line-number-like marks in dapui panes where they have no
  semantic meaning.

Sidebar reorder from last commit stands: Call Stack → Scopes → Watches
→ Breakpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-04-12 13:18:09 -06:00
1 parent 95cfcf915b
commit 4cec465a16
1 file changed
+83 -28
+83 -28
View File
@@ -21,18 +21,19 @@ local vt_initialized = false
-- file stays ASCII through any tool pipeline that strips PUA codepoints.
local ICON = {
-- Control-bar buttons: Nerd Font Codicons. Monochrome, but render
-- *consistently* across the whole bar — the VS16-dependent emoji family
-- (▶️ ⬇️ ⬆️) falls back to text-style in many terminals while
-- supplementary-plane emoji (🔴 🐛) render as colored glyphs, producing
-- the "some icons are blocks, some are characters" inconsistency. The
-- Codicon family all ships in the same Nerd Font glyph range.
play = "\xee\xab\x98", -- U+EAD8 debug-start
step_over = "\xee\xab\x96", -- U+EAD6 debug-step-over
step_into = "\xee\xab\x95", -- U+EAD5 debug-step-into
step_out = "\xee\xab\x94", -- U+EAD4 debug-step-out
restart = "\xee\xad\x84", -- U+EB44 debug-restart
stop = "\xee\xab\x97", -- U+EAD7 debug-stop
-- Control-bar emoji — strictly supplementary-plane (U+1F000+) or
-- standalone-emoji-default (like 🛑) codepoints. No VS16-dependent
-- glyphs: those render as text-mode in half of modern terminals, which
-- is exactly the "some icons are blocks, some are chars" inconsistency
-- the last iteration had. Every icon here is guaranteed to render as a
-- colored emoji in Ghostty + any modern terminal with emoji support.
go = "\xf0\x9f\x9f\xa2", -- 🟢 green circle (U+1F7E2) — go / continue / running
start_rkt = "\xf0\x9f\x9a\x80", -- 🚀 rocket (U+1F680) — launch a new session
step_over = "\xf0\x9f\x91\xa3", -- 👣 footprints (U+1F463) — step
step_into = "\xf0\x9f\x94\xbd", -- 🔽 down-pointing red triangle (U+1F53D) — into
step_out = "\xf0\x9f\x94\xbc", -- 🔼 up-pointing red triangle (U+1F53C) — out
restart = "\xe2\x99\xbb\xef\xb8\x8f", -- ♻️ recycle (U+267B + VS16) — user's pick
stop = "\xf0\x9f\x9b\x91", -- 🛑 octagonal stop sign (U+1F6D1)
-- pane-title emoji (supplementary plane — reliably colorful)
scopes = "\xf0\x9f\x94\x8e", -- 🔎 magnifier (U+1F50E)
@@ -44,7 +45,7 @@ local ICON = {
-- brand / state emoji
bug = "\xf0\x9f\x90\x9b", -- 🐛 bug (U+1F41B)
help = "\xe2\x9d\x93", -- ❓ (U+2753) black question mark
help = "\xf0\x9f\x92\xa1", -- 💡 lightbulb (U+1F4A1) — insight/help
-- IP arrows
ip_left = "\xf0\x9f\x91\x88", -- 👈 backhand pointing left (U+1F448)
@@ -217,14 +218,31 @@ local function render_bar()
if not status_ok then status_msg = "" end
local status_text = (status_msg ~= "" and (" " .. status_msg) or "")
-- ─── CENTER ZONE ─── control labels, each clickable
-- ─── CENTER ZONE ─── control labels, each clickable. Two labels have
-- modal variants that track the dap state so the text reflects what the
-- key will ACTUALLY do right now:
-- (c) start when no session is running
-- (c)ontinue when a session is paused or running
-- (q)uit when no session is running (exits debug mode)
-- (q) terminate when a session is running (kills the process)
local has_session = state ~= "READY"
local continue_icon, continue_tail
if has_session then
continue_icon = ICON.go
continue_tail = "ontinue"
else
continue_icon = ICON.start_rkt
continue_tail = " start"
end
local quit_tail = has_session and " terminate" or "uit"
local controls = {
{ icon = ICON.play, key = keyof("continue", "c"), tail = "ontinue", fn = action("continue") },
{ icon = ICON.step_over, key = keyof("step_over", "s"), tail = "tep", fn = action("step_over") },
{ icon = ICON.step_into, key = keyof("step_into", "d"), tail = "escend", fn = action("step_into") },
{ icon = ICON.step_out, key = keyof("step_out", "r"), tail = "eturn", fn = action("step_out") },
{ icon = ICON.restart, key = keyof("restart", "R"), tail = "estart", fn = action("restart") },
{ icon = ICON.stop, key = keyof("terminate", "q"), tail = "uit", fn = action("terminate") },
{ icon = continue_icon, key = keyof("continue", "c"), tail = continue_tail, fn = action("continue") },
{ icon = ICON.step_over, key = keyof("step_over", "s"), tail = "tep", fn = action("step_over") },
{ icon = ICON.step_into, key = keyof("step_into", "d"), tail = "escend", fn = action("step_into") },
{ icon = ICON.step_out, key = keyof("step_out", "r"), tail = "eturn", fn = action("step_out") },
{ icon = ICON.restart, key = keyof("restart", "R"), tail = "estart", fn = action("restart") },
{ icon = ICON.stop, key = keyof("terminate", "q"), tail = quit_tail, fn = action("terminate") },
}
-- ─── RIGHT ZONE ─── help (no "(?)" — the ❓ emoji is self-documenting)
@@ -409,6 +427,9 @@ local function open_bar()
end
bar_buf = vim.api.nvim_create_buf(false, true)
vim.bo[bar_buf].bufhidden = "wipe"
vim.bo[bar_buf].buftype = "nofile"
vim.bo[bar_buf].swapfile = false
vim.bo[bar_buf].buflisted = false
vim.bo[bar_buf].filetype = "TurboDebugBar"
bar_win = vim.api.nvim_open_win(bar_buf, false, bar_position())
vim.wo[bar_win].winhighlight = "Normal:TurboDebugBar,EndOfBuffer:TurboDebugBar"
@@ -421,10 +442,25 @@ local function open_bar()
vim.wo[bar_win].statuscolumn = ""
vim.wo[bar_win].wrap = false
-- click dispatch (both mouse button and drag — drag counts as click on bar_buf)
-- click dispatch
vim.keymap.set("n", "<LeftMouse>", handle_bar_click, { buffer = bar_buf, silent = true, nowait = true })
vim.keymap.set("n", "<LeftRelease>", "<Nop>", { buffer = bar_buf, silent = true, nowait = true })
-- Safety net: if the user somehow lands in the bar buffer (mouse drag into
-- it, or some focus accident), bounce them back out immediately. Stops
-- them from hitting E21 "Cannot make changes, 'modifiable' is off" when
-- they try to type in a non-modifiable buffer.
vim.api.nvim_create_autocmd("BufEnter", {
buffer = bar_buf,
callback = function()
vim.schedule(function()
if vim.api.nvim_get_current_buf() == bar_buf then
pcall(vim.cmd, "wincmd p")
end
end)
end,
})
render_bar()
end
@@ -450,13 +486,24 @@ local function set_dapui_window_opts(win)
-- adjacent windows. breakindent + showbreak make continuation lines visually
-- attached to their parent. The `format_value` wrap below truncates only
-- pathologically-long values.
--
-- Also kill line numbers, the statuscolumn (which snacks overrides with a
-- rendered gutter), and the `~` end-of-buffer markers — these are pure
-- noise in debug-info panes where there's no file to navigate.
if vim.api.nvim_win_is_valid(win) then
vim.wo[win].wrap = true
vim.wo[win].breakindent = true
vim.wo[win].linebreak = true
vim.wo[win].showbreak = " " .. ICON.wrap_mark .. " "
vim.wo[win].sidescrolloff = 0
vim.wo[win].cursorline = true
vim.wo[win].wrap = true
vim.wo[win].breakindent = true
vim.wo[win].linebreak = true
vim.wo[win].showbreak = " " .. ICON.wrap_mark .. " "
vim.wo[win].sidescrolloff = 0
vim.wo[win].cursorline = true
vim.wo[win].number = false
vim.wo[win].relativenumber = false
vim.wo[win].signcolumn = "no"
vim.wo[win].statuscolumn = ""
vim.wo[win].foldcolumn = "0"
-- make end-of-buffer `~` invisible by setting EOB fillchar to space
vim.wo[win].fillchars = "eob: "
end
end
@@ -650,8 +697,16 @@ local function source_window()
end
local function run_in_source(fn)
-- We can't use nvim_win_call here: dap.continue() may open a vim.ui.select
-- picker asynchronously, and by the time the picker opens, nvim_win_call
-- has already returned focus to the original window — the picker opens
-- but nothing has focus on it, forcing the user to click in to interact.
-- Actually shift focus to the source window so the picker takes focus
-- properly.
local w = source_window()
if w then return vim.api.nvim_win_call(w, fn) end
if w and w ~= vim.api.nvim_get_current_win() then
pcall(vim.api.nvim_set_current_win, w)
end
return fn()
end