Skip to content

[idea] Floating persistent terminal toggle — <leader>T opens/hides a reusable scratch terminal #303

Description

@stanfish06

What

Add a <leader>T keymap that opens a floating terminal window. Unlike <leader>, (which opens a new split terminal each time), this terminal is created once and toggled — hidden when not in focus, restored to the same shell session when recalled.

Where

lua/config/options.lua — alongside the existing terminal keymaps (<leader>, on line 21, <Esc><Esc> exit on line 20).

Why it matters

The current setup has two terminal entry points: :te / tabe | te for full-tab terminals, and <leader>, for a 15-line split. Both create a new shell each time, losing working directory and shell history. The tabline timer already tracks foreground processes per-terminal PID, so a persistent float would integrate naturally. A single scratch terminal (think VS Code's integrated terminal) is the common "quick command" pattern that the current split-terminal approach partially covers but doesn't persist.

Recommended implementation

local scratch_term = { buf = nil, win = nil }

local function toggle_scratch_terminal()
    -- if window is open, hide it
    if scratch_term.win and vim.api.nvim_win_is_valid(scratch_term.win) then
        vim.api.nvim_win_close(scratch_term.win, false)
        scratch_term.win = nil
        return
    end

    -- create buffer once
    if not scratch_term.buf or not vim.api.nvim_buf_is_valid(scratch_term.buf) then
        scratch_term.buf = vim.api.nvim_create_buf(false, false)
        vim.api.nvim_buf_call(scratch_term.buf, function()
            vim.fn.termopen(vim.o.shell, {
                on_exit = function()
                    scratch_term.buf = nil
                    scratch_term.win = nil
                end,
            })
        end)
    end

    local ui = vim.api.nvim_list_uis()[1] or { width = 120, height = 40 }
    local width  = math.floor(ui.width * 0.8)
    local height = math.floor(ui.height * 0.7)
    scratch_term.win = vim.api.nvim_open_win(scratch_term.buf, true, {
        relative = "editor",
        width  = width,
        height = height,
        row    = math.floor((ui.height - height) / 2),
        col    = math.floor((ui.width  - width)  / 2),
        border = "single",
        style  = "minimal",
        title  = " scratch terminal ",
    })
    vim.cmd("startinsert")
end

vim.keymap.set({ "n", "t" }, "<leader>T", toggle_scratch_terminal, { desc = "Toggle scratch terminal" })

The on_exit callback clears the stale refs so the next toggle creates a fresh terminal rather than trying to reopen a dead buffer.

Trade-offs

  • The terminal persists for the Neovim session lifetime; exit it explicitly (exit or <C-d>) to free the shell process.
  • <leader>T in terminal mode conflicts with nothing in the current config (checked against all existing t-mode mappings: only <Esc><Esc> is set).
  • The FileTree module (lua/config/filetree.lua) already demonstrates the pattern (reusable float with toggle), so the implementation is well-understood.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions