page icon indicating copy to clipboard operation
page copied to clipboard

Use neovim as pager

Page

Build Status Lines Of Code

Allows you to redirect text into neovim. You can set it as $PAGER to view logs, diffs, various command outputs.

ANSI escape sequences will be interpreted by :term buffer, which makes page noticeably faster than vimpager and nvimpager. Also, text will be displayed instantly as it arrives - no need to wait until EOF.

Also, text from neovim :term buffer will be redirected directly into a new buffer in the same neovim instance - no nested neovim will be spawned. That's by utilizing $NVIM_LISTEN_ADDRESS like neovim-remote does.

Ultimately, page reuses all of neovim's text editing+navigating+searching facilities and will either facilitate all of plugins+mappings+options set in your neovim config.

Usage

  • under regular terminal

usage under regular terminal

  • under neovim's terminal

usage under neovim's terminal


CLI

expand page --help
USAGE:
page [OPTIONS] [FILE]...

ARGS:
<FILE>...   Open provided file in separate buffer [without other flags revokes implied by default
                 -o or -p option]

OPTIONS:
-o                    Create and use output buffer (to redirect text from page's stdin) [implied by
                      default unless -x and/or <FILE> provided without other flags]
-O [<NOOPEN_LINES>]   Prefetch <NOOPEN_LINES> from page's stdin: if all input fits then print it to
                      stdout and exit without neovim usage (to emulate `less --quit-if-one-screen`)
                      [empty: term height - 3 (space for prompt); negative: term height -
                      <NOOPEN_LINES>; 0: disabled and default; ignored with -o, -p, -x and when
                      page isn't piped]
-p                    Print path of pty device associated with output buffer (to redirect text from
                      commands respecting output buffer size and preserving colors) [implied if
                      page isn't piped unless -x and/or <FILE> provided without other flags]
-P                    Set $PWD as working directory at output buffer (to navigate paths with `gf`)
-q [<QUERY_LINES>]    Read no more than <QUERY_LINES> from page's stdin: next lines should be
                      fetched by invoking :Page <QUERY> command or 'r'/'R' keypress on neovim side
                      [empty: term height - 2 (space for tab and buffer lines); negative: term
                      height - <QUERY_LINES>; 0: disabled and default; <QUERY> is optional and
                      defaults to <QUERY_LINES>; doesn't take effect on <FILE> buffers]
-f                    Cursor follows content of output buffer as it appears instead of keeping top
                      position (like `tail -f`)
-F                    Cursor follows content of output and <FILE> buffers as it appears instead of
                      keeping top position
-t <FILETYPE>         Set filetype on output buffer (to enable syntax highlighting) [pager:
                      default; not works with text echoed by -O]
-b                    Return back to current buffer
-B                    Return back to current buffer and enter into INSERT/TERMINAL mode
-n <NAME>             Set title for output buffer (to display it in statusline) [env:
                      PAGE_BUFFER_NAME=page -h]
-w                    Do not remap i, I, a, A, u, d, x, q (and r, R with -q) keys [wouldn't unmap
                      on connected instance output buffer]
                       ~ ~ ~
-a <ADDRESS>          TCP/IP socked address or path to named pipe listened by running host neovim
                      process [env: NVIM_LISTEN_ADDRESS=/tmp/nvimECnHF6/0]
-A <ARGUMENTS>        Arguments that will be passed to child neovim process spawned when <ADDRESS>
                      is missing [env: NVIM_PAGE_ARGS=]
-c <CONFIG>           Config that will be used by child neovim process spawned when <ADDRESS> is
                      missing [file:$XDG_CONFIG_HOME/page/init.vim]
-C                    Enable PageConnect PageDisconnect autocommands
-e <COMMAND>          Run command  on output buffer after it was created
    --e <LUA>         Run lua expr on output buffer after it was created
-E <COMMAND_POST>     Run command  on output buffer after it was created or connected as instance
    --E <LUA_POST>    Run lua expr on output buffer after it was created or connected as instance
                       ~ ~ ~
-i <INSTANCE>         Create output buffer with <INSTANCE> tag or use existed with replacing its
                      content by text from page's stdin
-I <INSTANCE_APPEND>  Create output buffer with <INSTANCE_APPEND> tag or use existed with appending
                      to its content text from page's stdin
-x <INSTANCE_CLOSE>   Close  output buffer with <INSTANCE_CLOSE> tag if it exists
                      [without other flags revokes implied by defalt -o or -p option]
                       ~ ~ ~
-W                    Flush redirection protection that prevents from producing junk and possible
                      overwriting of existed files by invoking commands like `ls >
                      $(NVIM_LISTEN_ADDRESS= page -E q)` where the RHS of > operator evaluates not
                      into /path/to/pty as expected but into a bunch of whitespace-separated
                      strings/escape sequences from neovim UI; bad things happens when some shells
                      interpret this as many valid targets for text redirection. The protection is
                      only printing of a path to the existed dummy directory always first before
                      printing of a neovim UI might occur; this makes the first target for text
                      redirection from page's output invalid and disrupts the whole redirection
                      early before other harmful writes might occur. [env:PAGE_REDIRECTION_PROTECT;
                      (0 to disable)]
                       ~ ~ ~
-l                    Split left  with ratio: window_width  * 3 / (<l-PROVIDED> + 1)
-r                    Split right with ratio: window_width  * 3 / (<r-PROVIDED> + 1)
-u                    Split above with ratio: window_height * 3 / (<u-PROVIDED> + 1)
-d                    Split below with ratio: window_height * 3 / (<d-PROVIDED> + 1)
-L <SPLIT_LEFT_COLS>  Split left  and resize to <SPLIT_LEFT_COLS>  columns
-R <SPLIT_RIGHT_COLS> Split right and resize to <SPLIT_RIGHT_COLS> columns
-U <SPLIT_ABOVE_ROWS> Split above and resize to <SPLIT_ABOVE_ROWS> rows
-D <SPLIT_BELOW_ROWS> Split below and resize to <SPLIT_BELOW_ROWS> rows
                       ^
-+                    With any of -r -l -u -d -R -L -U -D open floating window instead of split
                      [to not overwrite data in the current terminal]
                       ~ ~ ~
-h, --help            Print help information

nvim/init.lua customizations

Statusline appearance:

-- String that will append to buffer name
vim.g.page_icon_pipe = '|' -- When piped
vim.g.page_icon_redirect = '>' -- When exposes pty device
vim.g.page_icon_instance = '$' -- When `-i, -I` flags provided

Autocommand hooks:

-- Will run once when output buffer is created
vim.api.create_autocmd('User', {
    pattern = 'PageOpen',
    callback = lua_function,
})

-- Will run once when file buffer is created
vim.api.create_autocmd('User', {
    pattern = 'PageOpenFile',
    callback = lua_function,
})

-- Only with -C option provided: --

-- will run always when output buffer is created
-- and also when `page` connects to instance `-i, -I` buffers:
vim.api.create_autocmd('User', {
    pattern = 'PageConnect',
    callback = lua_function,
})

-- Will run when page process exits
vim.api.create_autocmd('User', {
    pattern = 'PageDisconnect',
    callback = lua_function,
})

Example: close page buffer on <C-c> hotkey:

_G.page_close = function(page_alternate_buf)
    local current_buf = vim.api.nvim_get_current_buf()
    vim.api.nvim_buf_delete(current_buf, { force = true })
    -- reenter into terminal mode
    if current_buf == page_alternate_buf and
        vim.api.nvim_get_mode() == 'n'
    then
        vim.cmd 'norm a'
    end
end

vim.api.nvim_create_autocmd('User', {
    pattern = 'PageOpen',
    callback = function()
        vim.api.nvim_set_keymap('n', '<C-c>', function()
            page_close(vim.b.page_alternate_bufnr)
        end, { buffer = 0 })
        vim.api.nvim_set_keymap('t', '<C-c>', function()
            page_close(vim.b.page_alternate_bufnr)
        end, { buffer = 0 })
    end
})

Shell hacks

To use as $PAGER without scrollback overflow:

export PAGER="page -q 90000"

To use as $MANPAGER:

export MANPAGER="page -t man"

To pick a bit better neovim's native man highlighting:

man () {
    PROGRAM="${@[-1]}"
    SECTION="${@[-2]}"
    page "man://$PROGRAM${SECTION:+($SECTION)}"
}

To circumvent neovim config picking:

page -c NONE

To override neovim config (create this file or use -c option):

$XDG_CONFIG_HOME/page/init.lua # init.vim is also supported

To set output buffer name as first two words from invoked command (zsh only):


preexec () {
    [ -z "$NVIM_LISTEN_ADDRESS" ] && return
    WORDS=(${1// *|*})
    export PAGE_BUFFER_NAME="${WORDS[@]:0:2}"
}

Buffer defaults

These commands are run on each page buffer creation:

vim.b.page_alternate_bufnr = {$initial_buf_nr}
if vim.wo.scrolloff > 999 or vim.wo.scrolloff < 0 then
    vim.g.page_scrolloff_backup = 0
else
    vim.g.page_scrolloff_backup = vim.wo.scrolloff
end
vim.bo.scrollback, vim.wo.scrolloff, vim.wo.signcolumn, vim.wo.number =
    100000, 999, 'no', false
{$filetype}
{$edit}
vim.api.nvim_create_autocmd('BufEnter', {
    buffer = 0,
    callback = function() vim.wo.scrolloff = 999 end
})
vim.api.nvim_create_autocmd('BufLeave', {
    buffer = 0,
    callback = function() vim.wo.scrolloff = vim.g.page_scrolloff_backup end
})
{$notify_closed}
{$pre}
vim.cmd 'silent doautocmd User PageOpen | redraw'
{$lua_provided_by_user}
{$cmd_provided_by_user}
{$after}

Where:

--{$initial_buf_nr}
-- Is always set on all buffers created by page

'number of parent :term buffer or -1 when page isn't spawned from :term'
--{$filetype}
-- Is set only on output buffers.
-- On files buffers filetypes are detected automatically.

vim.bo.filetype='value of -t argument or "pager"'
--{$edit}
-- Is appended when no -w option provided

vim.bo.modifiable = false
_G.page_echo_notification = function(message)
    vim.defer_fn(function()
        local msg = "-- [PAGE] " .. message .. " --"
        vim.api.nvim_echo({{ msg, 'Comment' }, }, false, {})
        vim.cmd 'au CursorMoved <buffer> ++once echo'
    end, 64)
end
_G.page_bound = function(top, message, move)
    local row, col, search
    if top then
        row, col, search = 1, 1, { '\\S', 'c' }
    else
        row, col, search = 9999999999, 9999999999, { '\\S', 'bc' }
    end
    vim.api.nvim_call_function('cursor', { row, col })
    vim.api.nvim_call_function('search', search)
    if move ~= nil then move() end
    _G.page_echo_notification(message)
end
_G.page_scroll = function(top, message)
    vim.wo.scrolloff = 0
    local move
    if top then
        local key = vim.api.nvim_replace_termcodes('z<CR>M', true, false, true)
        move = function() vim.api.nvim_feedkeys(key, 'nx', true) end
    else
        move = function() vim.api.nvim_feedkeys('z-M', 'nx', false) end
    end
    _G.page_bound(top, message, move)
    vim.wo.scrolloff = 999
end
_G.page_close = function()
    local buf = vim.api.nvim_get_current_buf()
    if buf ~= vim.b.page_alternate_bufnr and
        vim.api.nvim_buf_is_loaded(vim.b.page_alternate_bufnr)
    then
        vim.api.nvim_set_current_buf(vim.b.page_alternate_bufnr)
    end
    vim.api.nvim_buf_delete(buf, { force = true })
    local exit = true
    for _, b in ipairs(vim.api.nvim_list_bufs()) do
        local bt = vim.api.nvim_buf_get_option(b, 'buftype')
        if bt == "" or bt == "acwrite" or bt == "terminal" or bt == "prompt" then
            local bm = vim.api.nvim_buf_get_option(b, 'modified')
            if bm then
                exit = false
                break
            end
            local bl = vim.api.nvim_buf_get_lines(b, 0, -1, false)
            if #bl ~= 0 and bl[1] ~= "" and #bl > 1 then
                exit = false
                break
            end
        end
    end
    if exit then
        vim.cmd "qa!"
    end
end
local function page_map(key, expr)
    vim.api.nvim_buf_set_keymap(0, '', key, expr, { nowait = true })
end
page_map('I', '<CMD>lua _G.page_scroll(true, "in the beginning of scroll")<CR>')
page_map('A', '<CMD>lua _G.page_scroll(false, "at the end of scroll")<CR>')
page_map('i', '<CMD>lua _G.page_bound(true, "in the beginning")<CR>')
page_map('a', '<CMD>lua _G.page_bound(false, "at the end")<CR>')
page_map('q', '<CMD>lua _G.page_close()<CR>')
page_map('u', '<C-u>')
page_map('d', '<C-d>')
page_map('x', 'G')
--{$notify_closed}
-- Is set only on output buffers

local closed = 'rpcnotify({channel}, "page_buffer_closed", "{page_id}")'
vim.api.nvim_create_autocmd('BufDelete', {
    buffer = 0,
    command = 'silent! call ' .. closed
})
--{$pre}
-- Is appended when -q provided

vim.b.page_query_size = {$query_lines_count}
local def_args = '{channel}, "page_fetch_lines", "{page_id}", '
local def = 'command! -nargs=? Page call rpcnotify(' .. def_args .. '<args>)'
vim.cmd(def)
vim.api.create_autocmd('BufEnter', {
    buffer = 0,
    command = def,
})

-- Also if -q provided and no -w provided

page_map('r', '<CMD>call rpcnotify(' .. def_args .. 'b:page_query_size * v:count1)<CR>')
page_map('R', '<CMD>call rpcnotify(' .. def_args .. '99999)<CR>')

-- If -P provided ({pwd} is $PWD value)

vim.b.page_lcd_backup = getcwd()
vim.cmd 'lcd {pwd}'
vim.api.nvim_create_autocmd('BufEnter', {
    buffer = 0,
    command = 'lcd {pwd}'
})
vim.api.nvim_create_autocmd('BufLeave', {
    buffer = 0,
    command = 'exe "lcd" . b:page_lcd_backup'
})
--{$lua_provided_by_user}
-- Is appended when --e provided

'value of --e flag'
--{$cmd_provided_by_user}
-- Is appended when -e provided

vim.cmd [====[{$command}]====]
--{$after}
-- Is appended only on file buffers

vim.api.nvim_exec_autocmds('User', {
    pattern = 'PageOpenFile',
})

Limitations

Installation

  • From binaries

    • Grab binary for your platform from releases (currently Linux and OSX are supported)
  • Arch Linux:

    • Package page-git is available on AUR
    • Or: git clone [email protected]:I60R/page.git && cd page && makepkg -ef && sudo pacman -U page-git*.pkg.tar.xz
  • Manually:

    • Install rustup from your distribution package manager
    • Configure toolchain: rustup install stable && rustup default stable
    • git clone [email protected]:I60R/page.git && cd page && cargo install --path .