Ceramicist

Plugin for Neovim

Ceramicist is Neovim plugin to execute shell commands (like :!) and display their output in a dedicated window. It is similar to the terminal available in Neovim, but with some features intended to make it more convenient to use when you don’t need a full shell.

The main features are:

Installation

Ceramicist can be installed with any package manager for Neovim, like vim.pack:

vim.pack.add { "https://github.com/ayosec/nvim-ceramicist" }

require("ceramicist").setup({})

Usage

The setup() function creates an instance of Ceramicist.

require("ceramicist").setup({})

This instance is accessible through the user-command defined in the setup() options. By default, the user-command is :Ceramicist:

:Ceramicist ./scripts/something

The arguments of the user-command specify the shell command to run, like :!.

If there is another job running when a new one is executed, the previous job is interrupted before starting the new one.

To define a different command name (for example, :C):

require("ceramicist").setup {
    user_command = { name = "C" }
}

Then:

:C make test

Repeating Commands

If the user-command is executed with no arguments it repeats the last shell command, like :!!.

:Ceramicist make test

:Ceramicist             " Repeat 'make test'

You can also use the rerun() function, available in the buffer variable b:ceramicist_session, to repeat it.

The next example uses an autocommand to trigger a rerun when C-R is pressed on a window for a Ceramicist session:

vim.api.nvim_create_autocmd("User", {
    pattern = "Ceramicist/SessionCreated",
    callback = function(args)
        local map_opts = { buf = args.data.buffer }

        vim.keymap.set(
            "n", "<C-R>",
            function() vim.b.ceramicist_session().rerun() end,
            map_opts
        )
    end
})

Watch Mode

The :CeramicistToggleWatch user-command can enable or disable the Watch mode in a session. The user-command changes only the session in the current buffer. To enable Watch mode in multiple sessions, execute it in each one.

When Watch mode is enabled, the last shell command in the session is repeated after saving changes on any buffer. See :h BufWritePost.

Content Reset

If the user-command is executed with the ! modifier (see :h :_!) the output from previous jobs is removed before running a new one:

:Ceramicist scripts/tests       " The output is added to the session.

:Ceramicist! scripts/tests      " Reset session before running the job.

Multiple Sessions

Multiple sessions of the same instance can be created by adding a number to the user-command, either before or after the name:

:Ceramicist scripts/something       " Default session.

:2Ceramicist scripts/other          " Session 2

:Ceramicist2 scripts/other          " Session 2

The number is the session id. Its default value depends on the context where the user-command is executed:

Multiple Instances

Multiple instances of Ceramicist can be created with different calls to setup(), each one with its own user-command name.

The next example defines an instance on :C with the default configuration, and another instance on :M that executes make(1) instead of a shell expression. It also provides a custom completion list for :M:

require("ceramicist").setup {
    user_command = { name = "C" }
}

require("ceramicist").setup {
    user_command = {
        name = "M",
        complete = function()
            return { "all", "fmt", "test" }
        end,
    },

    job_command = function(cmdline)
        return { "make", cmdline }
    end,
}

For more details on the user_command.complete field, see :h :command-complete and :h :command-completion-customlist.

Window Placement

By default, the window for the session is open in a horizontal split at the bottom of the current tab. The modifiers :vertical, :tab, :topleft, etc, can be used to change the placement of the new window:

Deleting Sessions

A session can be deleted with :bdelete! (or just :bd!). When it is deleted, its state (the output from previous jobs, the last shell command, etc) is lost.

If there is a running job when the buffer is deleted, the job is interrupted.

The session will be recreated the next time the user-command is executed.

Listing Sessions

The function list_sessions returns an iterator to get all active sessions. These objects can be used to add extra functionality to manage the sessions.

For example, to add a :DeleteCeramicistSessions user-command to delete all idle (i.e. without a running job) sessions:

vim.api.nvim_create_user_command(
    "DeleteCeramicistSessions",
    function()
        for session in require("ceramicist").list_sessions() do
            if not session.is_running() then
                vim.cmd.bdelete { session.buffer, bang = true }
            end
        end
    end,
    { desc = "Delete idle Ceramicist sessions" }
)

Configuration

The default configuration provided by Ceramicist is very basic, since it is expected that users adjust it to their own preferences.

See config.lua for a reference of all available options.

The configuration is defined as the argument of the setup() function. Each call to setup() creates a new Ceramicist instance. There is no global state, so each instance has its own configuration.

The fields output.header and output.footer can define functions that are used to render the header before running a job, and the footer after it is finished.

Both functions must return a list of { text, highlight } pairs, like the virt_text field of :h nvim_buf_set_extmark.

header() receives a single argument, a string with the command line used to run the job.

footer() receives the exit code and the duration in nanoseconds. The exit code can be either an integer, or a string with the name of the signal that stopped the process.

Status Line

The field statusline defines a string to be used as the statusline of the sessions of the instance.

The buffer variable b:ceramicist_statusline is a function to provide a short summary of the status of the session (the PID of the running job, if Watch mode is enabled, the full command line, etc).

If statusline is false, the statusline is not modified.

Autocommands

Extra configuration options can be set by using autocommands:

In the data field, SessionCreated receives the buffer number, the window id, and the instance of the session.

For JobStarted and JobFinished, they also receive the command line to run the job.

The next example uses SessionCreated to add 2 key mappings (C-R to repeat the last command, and C-A to toggle Watch mode).

vim.api.nvim_create_autocmd("User", {
    pattern = "Ceramicist/SessionCreated",
    callback = function(args)
        local map_opts = { buf = args.data.buffer }

        -- <C-A> toggle watch
        vim.keymap.set("n", "<C-A>", "<Cmd>CeramicistToggleWatch<CR>", map_opts)

        -- <C-R> rerun
        vim.keymap.set(
            "n", "<C-R>",
            function() vim.b.ceramicist_session().rerun() end,
            map_opts
        )
    end
})

The next example uses matchadd() to highlight some patterns when the shell command starts with scripts/.

vim.api.nvim_create_autocmd("User", {
    pattern = "Ceramicist/JobStarted",
    callback = function(args)
        local buffer = args.data.buffer
        local window = args.data.window
        local cmdline = args.data.cmdline

        -- Use a buffer variable to track if the matchadd() is done.
        if vim.b[buffer].has_matchadd then return end

        if vim.startswith(cmdline, "scripts/") then
            vim.b[buffer].has_matchadd = true
            vim.fn.matchadd("DiagnosticInfo", "^INFO", 0, -1, { window = window })
            vim.fn.matchadd("DiagnosticError", "^ERROR", 0, -1, { window = window })
        end
    end
})

Example

The configuration example below:

vim.api.nvim_set_hl(0, "CeramicistNormal", { bg = "#001429" })

vim.api.nvim_set_hl(0, "CeramicistHeader", { bg = "#002E5C" })
vim.api.nvim_set_hl(0, "Ceramicist.Time", { fg = "#7777FF" })

vim.api.nvim_set_hl(0, "CeramicistFooterFail", { bg = "#652131" })
vim.api.nvim_set_hl(0, "CeramicistFooterSuccess", { bg = "#21653F" })

vim.api.nvim_set_hl(0, "Ceramicist.Success.Duration", { bg = "#329A5F"  })
vim.api.nvim_set_hl(0, "Ceramicist.Fail.Duration", { bg = "#9A324A"  })
vim.api.nvim_set_hl(0, "Ceramicist.Fail.Code", { bg = "#732638"  })
vim.api.nvim_set_hl(0, "Ceramicist.Fail.Signal", { bg = "#73264D"  })

vim.api.nvim_set_hl(0, "CeramicistStatusLineWatch", { bg = "#004D4D" })
vim.api.nvim_set_hl(0, "CeramicistStatusLineRunning", { bg = "#702000" })


local utils = require("ceramicist.utils")

local context = require("ceramicist").setup({
    user_command = { name = "C" },

    output = {
        gap = 3,

        header = function(cmdline)
            return {
                { vim.fn.strftime(" %H:%M:%S  "), "Ceramicist.Time" },
                { utils.escape_control_chars(cmdline), "" },
            }
        end,

        footer = function(exit, duration)
            local fd = " " .. utils.format_duration(duration) .. " "
            if exit == 0 then
                return { { "   ", "" }, { fd, "Ceramicist.Success.Duration" } }
            end

            -- Failed
            local status
            if type(exit) == "number" then
                status = { " $! = " .. exit .. " ", "Ceramicist.Fail.Code" }
            else
                status = { " " .. exit .. " ", "Ceramicist.Fail.Signal" }
            end

            local sep = { "   ", "" }
            return {  sep, status, sep, { fd, "Ceramicist.Fail.Duration" } }
        end,
    },
})


vim.api.nvim_create_autocmd("User", {
    pattern = "Ceramicist/SessionCreated",
    callback = function(args)
        local map_opts = { buf = args.data.buffer }

        -- <C-A> toggle watch
        vim.keymap.set(
            "n", "<C-A>",
            "<Cmd>CeramicistToggleWatch<CR>",
            map_opts
        )

        -- <C-R> rerun
        vim.keymap.set(
            "n", "<C-R>",
            function() vim.b.ceramicist_session().rerun() end,
            map_opts
        )
    end
})

Alternatives

There are many plugins providing a similar functionality to Ceramicist. This is a non-exhaustive list of alternatives: