pax_global_header00006660000000000000000000000064151634010630014511gustar00rootroot0000000000000052 comment=74cdd9ebc9caf79e47a3f01b81815b9901c61b88 nvimpager-0.14.0/000077500000000000000000000000001516340106300135635ustar00rootroot00000000000000nvimpager-0.14.0/.github/000077500000000000000000000000001516340106300151235ustar00rootroot00000000000000nvimpager-0.14.0/.github/workflows/000077500000000000000000000000001516340106300171605ustar00rootroot00000000000000nvimpager-0.14.0/.github/workflows/test.yml000066400000000000000000000073371516340106300206740ustar00rootroot00000000000000name: Run tests on: push: pull_request: schedule: - cron: "0 0 1 * *" # every month jobs: ubuntu-ppa: runs-on: ubuntu-latest strategy: matrix: ppa: - false - true steps: - uses: actions/checkout@v6 - name: activate the PPA for latest neovim run: sudo add-apt-repository -y ppa:neovim-ppa/unstable if: matrix.ppa - name: install dependencies run: | sudo apt-get update sudo apt-get install -yqq --no-install-recommends neovim scdoc lua-busted - name: Run the test suite run: make test BUSTED='busted --exclude-tags=ppa,v10' env: TERM: dumb appimage: runs-on: ubuntu-latest strategy: matrix: x: - version: v0.9.0 file: nvim hash: 0e1e6d53c6c8055de23bdb33f60bb64af0baf11390669c1b40ecbbf2c7a34547 tags: [appimage] - version: v0.10.0 file: nvim hash: 6a021e9465fe3d3375e28c3e94c1c2c4f7d1a5a67e4a78cf52d18d77b1471390 tags: [appimage, v10] - version: v0.11.0 file: nvim-linux-x86_64 hash: ca44cd43fe8d55418414496e8ec7bac83f611705ece167f4ccb93cbf46fec6c0 tags: [appimage, v10] - version: v0.12.0 file: nvim-linux-x86_64 hash: 7876b67462af08abdc884818b398b3e82907d6a4c89edfe7c6b1ff168eb7c4d6 tags: [appimage, v10] - version: nightly file: nvim-linux-x86_64 tags: [appimage, v10] steps: - uses: actions/checkout@v6 - name: install dependencies run: | sudo apt-get update sudo apt-get install -yqq --no-install-recommends scdoc lua-busted - name: Download official neovim appimage run: | wget --no-verbose https://github.com/neovim/neovim/releases/download/${{ matrix.x.version }}/${{ matrix.x.file }}.appimage if [ -n '${{ matrix.x.hash }}' ]; then echo ${{ matrix.x.hash }} ${{ matrix.x.file }}.appimage | sha256sum -c - fi chmod +x ${{ matrix.x.file }}.appimage ./${{ matrix.x.file }}.appimage --appimage-extract - name: Run the test suite run: make test "BUSTED=busted --exclude-tags=${{ join(matrix.x.tags, ',') }}" env: TERM: dumb NVIMPAGER_NVIM: squashfs-root/usr/bin/nvim macos: runs-on: macos-latest steps: - uses: actions/checkout@v6 - name: install dependencies run: | brew update brew install neovim scdoc luarocks luarocks --local install busted - name: Run the test suite run: make test BUSTED="$HOME/.luarocks/bin/busted --exclude-tags=mac,v10" env: TERM: dumb macos-nix: runs-on: macos-latest steps: - name: Install Nix uses: cachix/install-nix-action@v31 - uses: cachix/cachix-action@v17 with: name: nix-community - uses: actions/checkout@v6 - name: Run the test suite via nix run: nix build --print-build-logs nix: runs-on: ubuntu-latest strategy: matrix: package: - default - nightly update: - false - true exclude: - package: nightly update: false steps: - name: Install Nix uses: cachix/install-nix-action@v31 - uses: cachix/cachix-action@v17 with: name: nix-community - uses: actions/checkout@v6 - name: Update the fake inputs run: nix flake update if: matrix.update - name: Run the test suite with ${{ matrix.package }} neovim run: nix build --print-build-logs '.#${{ matrix.package }}' nvimpager-0.14.0/.gitignore000066400000000000000000000000661516340106300155550ustar00rootroot00000000000000luacov.*.out nvimpager.1 nvimpager.configured result* nvimpager-0.14.0/LICENSE000066400000000000000000000023771516340106300146010ustar00rootroot00000000000000Copyright (c) 2017 Lucas Hoffmann All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. nvimpager-0.14.0/README.md000066400000000000000000000076741516340106300150600ustar00rootroot00000000000000# Nvimpager Using [neovim] as a pager to view man pages, git diffs, whatnot with neovim's syntax highlighting and mouse support. ## About The `nvimpager` script calls neovim in a fashion that turns it into something like a pager. The idea is not new, this is actually rewrite of [vimpager] but with less (but stricter) dependencies and specifically for neovim. Some typical use cases: ```sh # view a file in nvimpager nvimpager file # pipe text to nvimpager echo some text | nvimpager # use it as your default $PAGER export PAGER=nvimpager man bash git diff ``` The script also has a "cat mode" which will not start up the neovim interface but instead print a highlighted version of the file to the terminal. Like cat with neovim syntax highlighting! If the input has less lines than the terminal cat mode is activated automatically so nvimpager behaves similar to `less -F`. Pager mode and cat mode can be enforced with the options `-p` and `-c` respectively. Nvimpager comes with a small set of command line options but you can also use all of neovim's command line options. Use `nvimpager -h` to see the [help text][options]. The configuration is separated from the users config for neovim. The main config file is `~/.config/nvimpager/init.vim` (or `.lua`). See [the manpage][configuration] for further explanation. ## Installation Packaging status Nvimpager is already packaged for some distributions. If not for yours, you can install it manually, read on. ### Dependencies * [neovim] ≥ v0.9.0 * [bash] * [busted] (for running the tests) * [scdoc] (to build the man page) ### Installation instructions Use the makefile to configure and install the script. It supports the usual `PREFIX` (defaults to `/usr/local`) and `DESTDIR` (defaults to empty) variables: ```sh make PREFIX=$HOME/.local install ``` The target `install-no-man` can be used to install nvimpager without the man page. Additionally the variable `BUSTED` can be used to specify the executable for the test suite: ```sh make test BUSTED="/path/to/busted --some-args" ``` ## Development Nvimpager is developed on [GitHub][nvimpager] where you are very much invited to [post][issues] bug reports, feature or pull requests! The test can be run with `make test`. They are also run on GitHub: [![Build Status]][ghactions] ### Limitations * if reading from stdin, nvimpager (like nvim) waits for EOF until it starts up * large files are slowing down neovim on startup (less does a better, i.e. faster and more memory efficient job at paging large files) ### Ideas * see how [neovim#5035], [neovim#7438] and [neovim#23093] are resolved and maybe move more code (logic) from bash to lua (bash's `[[ -t ... ]]` can be replaced by `has('ttyin')`, `has('ttyout')`) * proper lazy pipe reading while paging (like less) to improve startup time and also memory usage for large input on pipes (maybe `stdioopen()` can be used?) ## License The project is licensed under a BSD-2-clause license. See the [LICENSE](./LICENSE) file. [nvimpager]: https://github.com/lucc/nvimpager [issues]: https://github.com/lucc/nvimpager/issues [options]: ./nvimpager.md#command-line-options [configuration]: ./nvimpager.md#configuration [neovim]: https://github.com/neovim/neovim [vimpager]: https://github.com/rkitover/vimpager [bash]: https://www.gnu.org/software/bash/bash.html [busted]: https://lunarmodules.github.io/busted/ [scdoc]: https://git.sr.ht/~sircmpwn/scdoc [Build Status]: https://github.com/lucc/nvimpager/actions/workflows/test.yml/badge.svg [ghactions]: https://github.com/lucc/nvimpager/actions [neovim#5035]: https://github.com/neovim/neovim/issues/5035 (detach / reattach) [neovim#7438]: https://github.com/neovim/neovim/issues/7438 (dynamic --headless) [neovim#23093]: https://github.com/neovim/neovim/issues/23093 (detach current tui) nvimpager-0.14.0/_nvimpager000066400000000000000000000006001516340106300156310ustar00rootroot00000000000000#compdef nvimpager typeset -A opt_args local ret=1 local context curcontext="$curcontext" state line local arguments arguments=( '(* -)-h[show the help text and exit]' '(* -)-v[show version into and exit]' '-p[pager mode (overrides -a, -c)]' '-a[auto mode (overrides -c, -p)]' '-c[cat mode (overrides -a, -p)]' '(-)*:file:_files' ) _arguments -C -S $arguments && ret=0 nvimpager-0.14.0/config.ld000066400000000000000000000004661516340106300153570ustar00rootroot00000000000000-- Config file for ldoc, see -- https://stevedonovan.github.io/ldoc/manual/doc.md.html -- vim: filetype=lua project = "nvimpager" file = { "lua", "test", exclude = { "test/diff_test.lua", "test/meta_test.lua", "test/no_map_test.lua", }} all = true readme = "README.md" format = "markdown" nvimpager-0.14.0/flake.lock000066400000000000000000000066621516340106300155310ustar00rootroot00000000000000{ "nodes": { "flake-parts": { "inputs": { "nixpkgs-lib": [ "neovim", "nixpkgs" ] }, "locked": { "lastModified": 1772408722, "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=", "owner": "hercules-ci", "repo": "flake-parts", "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3", "type": "github" }, "original": { "owner": "hercules-ci", "repo": "flake-parts", "type": "github" } }, "flake-utils": { "inputs": { "systems": "systems" }, "locked": { "lastModified": 1731533236, "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { "owner": "numtide", "repo": "flake-utils", "type": "github" } }, "neovim": { "inputs": { "flake-parts": "flake-parts", "neovim-src": "neovim-src", "nixpkgs": "nixpkgs" }, "locked": { "lastModified": 1775076062, "narHash": "sha256-ruqxqJtdmNm/fmjuAdwtSBNcbBeMgE1hwELlUnAFgyU=", "owner": "nix-community", "repo": "neovim-nightly-overlay", "rev": "215965fbe5b5dbd61bf33c8bda4a20c2b32c3df2", "type": "github" }, "original": { "owner": "nix-community", "repo": "neovim-nightly-overlay", "type": "github" } }, "neovim-src": { "flake": false, "locked": { "lastModified": 1774915197, "narHash": "sha256-yor+eo8CVi7wBp7CjAMQnVoK+m197gsl7MvUzaqicns=", "owner": "neovim", "repo": "neovim", "rev": "dbc4800dda2b0dc3290dc79955f857256e0694e2", "type": "github" }, "original": { "owner": "neovim", "repo": "neovim", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1774701658, "narHash": "sha256-CIS/4AMUSwUyC8X5g+5JsMRvIUL3YUfewe8K4VrbsSQ=", "owner": "NixOS", "repo": "nixpkgs", "rev": "b63fe7f000adcfa269967eeff72c64cafecbbebe", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_2": { "locked": { "lastModified": 1775036866, "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", "owner": "nixos", "repo": "nixpkgs", "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", "type": "github" }, "original": { "owner": "nixos", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "flake-utils": "flake-utils", "neovim": "neovim", "nixpkgs": "nixpkgs_2" } }, "systems": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", "repo": "default", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default", "type": "github" } } }, "root": "root", "version": 7 } nvimpager-0.14.0/flake.nix000066400000000000000000000046621516340106300153750ustar00rootroot00000000000000{ description = "Development flake for nvimpager"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; neovim.url = "github:nix-community/neovim-nightly-overlay"; }; outputs = { self, nixpkgs, flake-utils, neovim, ... }: let inherit (builtins) head match readFile; inherit (flake-utils.lib) eachDefaultSystem; version = head (match ".*version=([0-9.]*)\n.*" (readFile ./nvimpager)) + "-dev-${toString self.sourceInfo.lastModifiedDate}"; inherit (nixpkgs.lib.strings) optionalString; nvimpager = { stdenv, neovim, ncurses, procps, scdoc, lua51Packages, util-linux }: stdenv.mkDerivation { pname = "nvimpager"; inherit version; src = self; buildInputs = [ ncurses # for tput procps # for nvim_get_proc() which uses ps(1) ]; nativeBuildInputs = [ scdoc ]; makeFlags = [ "PREFIX=$(out)" "VERSION=${version}" ]; buildFlags = [ "nvimpager.configured" "nvimpager.1" ]; preBuild = '' patchShebangs nvimpager substituteInPlace nvimpager --replace ':-nvim' ':-${neovim}/bin/nvim' ''; doCheck = true; nativeCheckInputs = [ lua51Packages.busted util-linux neovim ]; # filter out one test that fails in the sandbox of nix preCheck = let neovim-version = neovim.version or neovim.passthru.unwrapped.version; neovim-new = null == match "^0\\.9\\..*" neovim-version; exclude-tags = "nix" + optionalString stdenv.isDarwin ",mac" + optionalString neovim-new ",v10"; in '' checkFlagsArray+=('BUSTED=busted --output TAP --exclude-tags=${exclude-tags}') ''; }; in ({ overlays.default = final: prev: { nvimpager = final.callPackage nvimpager {}; }; } // (eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; callPackage = pkgs.callPackage nvimpager; neovim-nightly = neovim.packages.${system}.default; default = callPackage {}; nightly = callPackage { neovim = neovim-nightly; }; ldoc = pkgs.runCommandLocal "nvimpager-api-docs" {} "cd ${self} && ${pkgs.luaPackages.ldoc}/bin/ldoc . --dir $out"; in { apps.default = flake-utils.lib.mkApp { drv = default; }; packages = { inherit default nightly ldoc neovim-nightly; inherit (pkgs) neovim; }; checks = { inherit default nightly ldoc; }; }))); } nvimpager-0.14.0/lua/000077500000000000000000000000001516340106300143445ustar00rootroot00000000000000nvimpager-0.14.0/lua/nvimpager/000077500000000000000000000000001516340106300163345ustar00rootroot00000000000000nvimpager-0.14.0/lua/nvimpager/ansi2highlight.lua000066400000000000000000000270441516340106300217520ustar00rootroot00000000000000--- Functions to convert terminal escape sequences to nvim highlight groups. -- Neovim defines this object but luacheck doesn't know it. So we define a -- shortcut and tell luacheck to ignore it. local nvim = vim.api -- luacheck: ignore -- A neovim highlight namespace to group together all highlights added to -- buffers by this module. local namespace --- A cache to remember which syntax groups have already been defined. local cache = {} --- A mapping of ansi color numbers to neovim color names local colors = { [0] = "black", [8] = "darkgray", [1] = "red", [9] = "lightred", [2] = "green", [10] = "lightgreen", [3] = "yellow", [11] = "lightyellow", [4] = "blue", [12] = "lightblue", [5] = "magenta", [13] = "lightmagenta", [6] = "cyan", [14] = "lightcyan", [7] = "lightgray", [15] = "white", } --- the names of neovim's highlighting attributes that are handled by this --- module --- Most attributes are referred to by their highlighting attribute name in --- neovim's :highlight command. local attributes = { [1] = "bold", --[2] = "faint", -- not handled by neovim [3] = "italic", [4] = "underline", --[5] = "slow blink", -- not handled by neovim --[6] = "underline", -- not handled by neovim [7] = "reverse", [8] = "conceal", [9] = "strikethrough", -- TODO when to use the gui attribute "standout"? } --- Format 24 bit RGB colors in hex notation --- @param r integer --- @param g integer --- @param b integer local function hexformat_rgb_numbers(r, g, b) return string.format("#%06x", r * 2^16 + g * 2^8 + b) end --- Split one of the predefined colors for 256color terms into their RGB --- values --- --- @param color_number integer local function split_predefined_terminal_color(color_number) local r = math.floor(color_number / 36) local g = math.floor(math.floor(color_number / 6) % 6) local b = math.floor(color_number % 6) local lookup = {[0]=0, [1]=95, [2]=135, [3]=175, [4]=215, [5]=255} return lookup[r], lookup[g], lookup[b] end --- Create an iterator that tokenizes the given input string into ansi escape --- sequences. --- --- Lua patterns for string.gmatch local function tokenize(input_string) -- The empty input string is a special case where we return one single -- token. if input_string == "" then return string.gmatch("", "") end -- we keep track of the position in the input with a local variable so that -- our "next" function does not need to rely on the second argument. -- Especially if a token appears twice in the input that might be of -- importance. local position = 1 local function next(input) -- If the position we are currently tokenizing is beyond the input string -- return nil => stop tokenizing. if input:len() < position then return nil end -- If we are on the last character and it is a semicolon, return an empty -- token and move position beyond the input to stop on the next call. -- This is hard to handle properly in the tokenizer below. if input:len() == position and input:sub(-1) == ";" then position = position + 1 return "" end -- first check for the special sequences "38;" "48;" local init = input:sub(position, position+2) if init == "38;" or init == "48;" then -- Try to match an 8 bit or a 24 bit color sequence local patterns = {"([34])8;5;(%d+);?", "([34])8;2;(%d+);(%d+);(%d+);?"} for _, pattern in ipairs(patterns) do local start, stop, token, c1, c2, c3 = input:find(pattern, position) if start == position then position = stop + 1 return token == "3" and "foreground" or "background", c1, c2, c3 end end -- If no valid special sequence was found we fall through to the normal -- tokenization. end -- handle all other tokens, we expect a simple number followed by either a -- semicolon or the end of the string, or the end of the input string -- directly. local oldpos = position local next_pos = input:find(";", position) if next_pos == nil then -- no further semicolon was found, we reached the end of the input -- string, the next call to this function will return nil position = input:len() + 1 return input:sub(oldpos, -1) else position = next_pos -- We only skip the semicolon if it was not at the end of the input -- string. if next_pos < input:len() then position = next_pos + 1 end return input:sub(oldpos, next_pos - 1) end end return next, input_string, nil end --- The internal state of the parser local state = { line = 1, -- the line where the currently described state starts column = 1, -- the column where the currently described state starts } --- Reset the parser function state:clear() self.foreground = "" self.background = "" self.ctermfg = "" self.ctermbg = "" for _, k in pairs(attributes) do self[k] = false end end --- Compute the name of the current active nvim highlight group function state:state2highlight_group_name() if self.conceal then return "NvimPagerConceal" end local name = "NvimPagerFG_" .. self.foreground:gsub("#", "") .. "_BG_" .. self.background:gsub("#", "") for _, field in pairs(attributes) do if self[field] then name = name .. "_" .. field end end return name end --- Parse the terminal escape sequences in the given string --- --- @param string string function state:parse(string) for _token, c1, c2, c3 in tokenize(string) do local token = _token -- First we check for 256 colors and 24 bit color sequences. if c3 ~= nil then self[token] = hexformat_rgb_numbers(tonumber(c1), tonumber(c2), tonumber(c3)) elseif c1 ~= nil then self:parse8bit(token, c1) self["cterm"..token:sub(1, 1).."g"] = tonumber(c1) else if token == "" then token = 0 else token = tonumber(token) end if token == 0 then self:clear() elseif token == 1 or token == 3 or token == 4 or token == 7 or token == 8 or token == 9 then -- 2, 5 and 6 could be handled here if they were supported. self[attributes[token]] = true elseif token == 21 then -- 22 means "doubley underline" or "bold off", we could implement -- doubley underline by undercurl. --self.undercurl = true elseif token == 22 then self.bold = false --self.faint = false elseif token == 23 or token == 24 or token == 27 or token == 28 or token == 29 then -- 25 means blink off so it could also be handled here if it was -- supported. self[attributes[token - 20]] = false elseif token >= 30 and token <= 37 then -- foreground color self.foreground = colors[token - 30] self.ctermfg = token - 30 elseif token == 39 then -- reset foreground self.foreground = "" self.ctermfg = "" elseif token >= 40 and token <= 47 then -- background color self.background = colors[token - 40] self.ctermbg = token - 40 elseif token == 49 then -- reset background self.background = "" self.ctermbg = "" elseif token >= 90 and token <= 97 then -- bright foreground color self.foreground = colors[token - 82] elseif token >= 100 and token <= 107 then -- bright background color self.background = colors[token - 92] end end end end --- Parse an 8 bit color number into a highlight definition --- --- @param fgbg "foreground"|"background" --- @param color string function state:parse8bit(fgbg, color) local colornr = tonumber(color) if colornr >= 0 and colornr <= 7 then color = colors[colornr] elseif colornr >= 8 and colornr <= 15 then -- high pallet colors color = colors[colornr] -- + 82 + 10 * (fgbg == "background" and 1 or 0) elseif colornr >= 16 and colornr <= 231 then -- color cube color = hexformat_rgb_numbers(split_predefined_terminal_color(colornr-16)) else -- grayscale ramp colornr = 8 + 10 * (colornr - 232) color = hexformat_rgb_numbers(colornr, colornr, colornr) end self[fgbg] = ""..color end --- Compute an nvim highlight command for the current internal state with the --- given highlight group name. --- --- @param groupname string function state:compute_highlight_command(groupname) local args = "" if self.foreground ~= "" then args = args.." guifg="..self.foreground if self.ctermfg ~= "" then args = args .. " ctermfg=" .. self.ctermfg end end if self.background ~= "" then args = args.." guibg="..self.background if self.ctermbg ~= "" then args = args .. " ctermbg=" .. self.ctermbg end end local attrs = "" for _, key in pairs(attributes) do if self[key] then attrs = attrs .. "," .. key end end attrs = attrs:sub(2) if attrs ~= "" then args = args .. " gui=" .. attrs .. " cterm=" .. attrs end if args == "" then return "highlight default link " .. groupname .. " Normal" else return "highlight default " .. groupname .. args end end --- Wrapper around nvim_buf_add_highlight to fix index offsets --- --- The function nvim_buf_add_highlight expects 0 based line numbers and column --- numbers. Set the start column to 0, the end column to -1 if not given. --- --- @param groupname string nvim highlight group name --- @param line integer the line number for the line to highlight --- @param from integer|nil starting position in the line --- @param to integer|nil end position in the line local function add_highlight(groupname, line, from, to) local line_0 = line - 1 local from_0 = (from or 1) - 1 local to_0 = (to or 0) - 1 nvim.nvim_buf_add_highlight(0, namespace, groupname, line_0, from_0, to_0) end --- Apply a highlight to a range in the current buffer --- --- The highlight attributes are generated from the current state (self). --- --- @local --- @param from_line integer --- @param from_column integer --- @param to_line integer --- @param to_column integer function state:render(from_line, from_column, to_line, to_column) if from_line == to_line and from_column == to_column then return end local groupname = self:state2highlight_group_name() -- check if the hl group already exists if cache[groupname] == nil then nvim.nvim_command(self:compute_highlight_command(groupname)) cache[groupname] = true end if from_line == to_line then add_highlight(groupname, from_line, from_column, to_column) else add_highlight(groupname, from_line, from_column) for line = from_line+1, to_line-1 do add_highlight(groupname, line) end add_highlight(groupname, to_line, 1, to_column) end end --- Parse the current buffer for ansi escape sequences and add buffer --- highlights to the buffer instead. local function ansi2highlight() nvim.nvim_command( "syntax match NvimPagerEscapeSequence conceal '\\e\\[[0-9;]*m'") nvim.nvim_command( "syntax match NvimPagerEscapeSequence conceal '\\e\\[[0-2]\\?K'") nvim.nvim_command("highlight NvimPagerConceal gui=NONE guisp=NONE " .. "guifg=background guibg=background") nvim.nvim_win_set_option(0, "conceallevel", 3) nvim.nvim_win_set_option(0, "concealcursor", "nv") local pattern = "\27%[([0-9;]*)m" state:clear() namespace = nvim.nvim_create_namespace("") for lnum, line in ipairs(nvim.nvim_buf_get_lines(0, 0, -1, false)) do local start, end_, spec local col = 1 repeat start, end_, spec = line:find(pattern, col) if start ~= nil then state:render(state.line, state.column, lnum, start) state.line = lnum state.column = end_ state:parse(spec) -- update the position to find the next match in the line col = end_ end until start == nil end end return { hexformat_rgb_numbers = hexformat_rgb_numbers, run = ansi2highlight, split_predefined_terminal_color = split_predefined_terminal_color, state = state, tokenize = tokenize, } nvimpager-0.14.0/lua/nvimpager/cat.lua000066400000000000000000000201471516340106300176120ustar00rootroot00000000000000--- Functions for nvimpager's cat mode -- Neovim defines this object but luacheck doesn't know it. So we define a -- shortcut and tell luacheck to ignore it. local vim = vim -- luacheck: ignore local nvim = vim.api -- luacheck: ignore local util = require("nvimpager/util") -- These variables will be initialized during the first call to cat_mode() -- -- A local copy of the termguicolors option, used for color output in cat -- mode. local colors_24_bit local color2escape --- A cache to map syntax groups to ansi escape sequences in cat mode or --- remember defined syntax groups in the ansi rendering functions. local cache = {} --- Split a 24 bit color number into the three red, green and blue values --- --- @param color_number number local function split_rgb_number(color_number) -- The lua implementation of these bit shift operations is taken from -- http://nova-fusion.com/2011/03/21/simulate-bitwise-shift-operators-in-lua local r = math.floor(color_number / 2 ^ 16) local g = math.floor(math.floor(color_number / 2 ^ 8) % 2 ^ 8) local b = math.floor(color_number % 2 ^ 8) return r, g, b end --- Compute the escape sequences for a 24 bit color number. --- --- @param color_number number a 24 bit color number --- @param foreground boolean whether to return the escape sequences for fg or --- bg colors local function color2escape_24bit(color_number, foreground) local red, green, blue = split_rgb_number(color_number) local escape if foreground then escape = '38;2;' else escape = '48;2;' end return escape .. red .. ';' .. green .. ';' .. blue end --- Compute the escape sequences for a 8 bit color number. --- --- @param color_number number an 8 bit color number --- @param foreground boolean whether to return the escape sequences for fg or --- bg colors local function color2escape_8bit(color_number, foreground) local prefix if color_number < 8 then if foreground then prefix = '3' else prefix = '4' end elseif color_number < 16 then color_number = color_number - 8 if foreground then prefix = '9' else prefix = '10' end elseif foreground then prefix = '38;5;' else prefix = '48;5;' end return prefix .. color_number end --- Compute a ansi escape sequences to render a syntax group on the terminal. --- --- @param groupid integer local function group2ansi(groupid) if cache[groupid] then return cache[groupid] end local info = nvim.nvim_get_hl(0, {id = groupid, link = false}) if colors_24_bit then info.foreground = info.fg info.background = info.bg else info.foreground = info.ctermfg info.background = info.ctermbg if info.cterm ~= nil then info.bold = info.cterm.bold info.italic = info.cterm.italic info.underline = info.cterm.underline else info.bold = nil info.italic = nil info.underline = nil end end if info.reverse then info.foreground, info.background = info.background, info.foreground end -- Reset all attributes before setting new ones. The vimscript version did -- use sevel explicit reset codes: 22, 24, 25, 27 and 28. If no foreground -- or background color was defined for a syntax item they were reset with -- 39 or 49. local escape = '\27[0' if info.bold then escape = escape .. ';1' end if info.italic then escape = escape .. ';3' end if info.underline then escape = escape .. ';4' end if info.foreground then escape = escape .. ';' .. color2escape(info.foreground, true) end if info.background then escape = escape .. ';' .. color2escape(info.background, false) end escape = escape .. 'm' cache[groupid] = escape return escape end --- Check if the current buffer is empty local function check_empty() if nvim.nvim_buf_line_count(0) <= 1 and nvim.nvim_buf_get_offset(0, 0) <= 0 then local handle = io.open(nvim.nvim_buf_get_name(0)) if handle == nil then return true end local eof = handle:read(0) handle:close() if eof == nil then return true end end return false end --- Iterate through the current buffer and print it to stdout with terminal --- color codes for highlighting. local function highlight() -- Detect an empty buffer. if check_empty() then return elseif util.check_escape_sequences() then for _, line in ipairs(nvim.nvim_buf_get_lines(0, 0, -1, false)) do io.write(line, '\n') end return end local conceallevel = nvim.nvim_win_get_option(0, 'conceallevel') local syntax_id_conceal = nvim.nvim_call_function('hlID', {'Conceal'}) local syntax_id_whitespace = nvim.nvim_call_function('hlID', {'Whitespace'}) local syntax_id_non_text = nvim.nvim_call_function('hlID', {'NonText'}) local list = nvim.nvim_win_get_option(0, "list") local listchars = list and vim.opt.listchars:get() or {} local last_syntax_id = -1 local last_conceal_id = -1 local linecount = nvim.nvim_buf_line_count(0) for lnum, line in ipairs(nvim.nvim_buf_get_lines(0, 0, -1, false)) do local outline = '' local skip_next_char = false local syntax_id for cnum = 1, line:len() do local conceal_info = nvim.nvim_call_function('synconcealed', {lnum, cnum}) local conceal = conceal_info[1] == 1 local replace = conceal_info[2] local conceal_id = conceal_info[3] if skip_next_char then skip_next_char = false elseif conceal and last_conceal_id == conceal_id then -- luacheck: ignore -- skip this char else local append if conceal then syntax_id = syntax_id_conceal if replace == '' and conceallevel == 1 then replace = ' ' end append = replace last_conceal_id = conceal_id else append = line:sub(cnum, cnum) if list and string.find(" \194", append, 1, true) ~= nil then syntax_id = syntax_id_whitespace if append == " " then if line:find("^ +$", cnum) ~= nil then append = listchars.trail or listchars.space or append else append = listchars.space or append end elseif append == "\194" and line:sub(cnum + 1, cnum + 1) == "\160" then -- Utf8 non breaking space is "\194\160", neovim represents all -- files as utf8 internally, regardless of the actual encoding. -- See :help 'encoding'. append = listchars.nbsp or "\194\160" skip_next_char = true end else syntax_id = nvim.nvim_call_function('synID', {lnum, cnum, true}) end end if syntax_id ~= last_syntax_id then outline = outline .. group2ansi(syntax_id) last_syntax_id = syntax_id end outline = outline .. append end end -- append a eol listchar if &list is set if list and listchars.eol ~= nil then syntax_id = syntax_id_non_text if syntax_id ~= last_syntax_id then outline = outline .. group2ansi(syntax_id) last_syntax_id = syntax_id end outline = outline .. listchars.eol end -- Write the whole line and a newline char. If this was the last line -- also reset the terminal attributes. io.write(outline, lnum == linecount and cache[0] or '', '\n') end end --- Initialize some module level variables for cat mode. local function init() -- Get the value of &termguicolors from neovim. colors_24_bit = nvim.nvim_get_option('termguicolors') -- Select the correct coloe escaping function. if colors_24_bit then color2escape = color2escape_24bit else color2escape = color2escape_8bit end -- Initialize the ansi group to color cache with the "Normal" hl group. cache[0] = group2ansi(nvim.nvim_call_function('hlID', {'Normal'})) end --- Call the highlight function to write the highlighted version of all buffers --- to stdout and quit nvim. local function cat_mode() init() highlight() -- We can not use nvim_list_bufs() as a file might appear on the command -- line twice. In this case we want to behave like cat(1) and display the -- file twice. for _ = 2, nvim.nvim_call_function('argc', {}) do nvim.nvim_command('next') highlight() end nvim.nvim_command('quitall!') end --- @export return { cat_mode = cat_mode, init = init, color2escape_24bit = color2escape_24bit, color2escape_8bit = color2escape_8bit, split_rgb_number = split_rgb_number, group2ansi = group2ansi, } nvimpager-0.14.0/lua/nvimpager/init.lua000066400000000000000000000171061516340106300200070ustar00rootroot00000000000000--- Functions to use neovim as a pager. --- --- This code is a rewrite of two sources: vimcat and vimpager (which also --- contains a version of vimcat): --- --- - Vimcat goes back to Matthew J. Wozniski and can be found at --- --- - Vimpager was written by Rafael Kitover and can be found at --- -- Information about terminal escape codes: -- https://en.wikipedia.org/wiki/ANSI_escape_code -- Neovim defines this object but luacheck doesn't know it. So we define a -- shortcut and tell luacheck to ignore it. local nvim = vim.api -- luacheck: ignore local cat = require("nvimpager/cat") local pager = require("nvimpager/pager") -- names that will be exported from this module local nvimpager = require("nvimpager/options") -- These variables will be initialized during the first call to cat_mode() or -- pager_mode(). -- -- This variable holds the name of the detected parent process for pager mode. local doc --- Replace a string prefix in all items in a list local function replace_prefix(table, old_prefix, new_prefix) -- Escape all punctuation chars to protect from lua pattern chars. old_prefix = old_prefix:gsub('[^%w]', '%%%0') for index, value in ipairs(table) do table[index] = value:gsub('^' .. old_prefix, new_prefix, 1) end return table end --- Parse the command of the given pid to detect some common --- documentation programs (man, pydoc, perldoc, git, ...). --- --- @param pid integer|nil local function detect_process(pid) if not pid then return nil end -- FIXME saving and resetting gcr after nvim_get_proc is a workaround for -- https://github.com/neovim/neovim/issues/23122, reported in #84 local old_gcr = vim.o.gcr vim.o.gcr = '' local proc = nvim.nvim_get_proc(pid) vim.o.gcr = old_gcr if proc == nil then return 'none' end local command = proc.name if command == 'man' then return 'man' elseif command:find('^[Pp]ython[0-9.]*') ~= nil or command:find('^[Pp]ydoc[0-9.]*') ~= nil then return 'pydoc' elseif command == 'ruby' or command == 'irb' or command == 'ri' then return 'ri' elseif command == 'perl' or command == 'perldoc' then return 'perldoc' elseif command == 'git' then return 'git' end return nil end --- Parse the command of the calling process --- $PARENT was exported by the calling bash script and points to the calling --- program. local function detect_parent_process() return detect_process(tonumber(os.getenv('PARENT'))) end --- Check if a string uses poor man's bold or underline tricks --- --- Return true if all characters are followed by backspace and themself again --- or if all characters are preceded by underscore and backspace. Spaces --- are ignored. --- --- @param line string local function detect_man_page_helper(line) line = line:gsub('%s+', '') if line == "" then return false end local index = 1 while index <= #line do local cur = line:sub(index, index) local next = line:sub(index+1, index+1) local third = line:sub(index+2, index+2) if (cur == third and next == '\b') or (cur == '_' and next == '\b' and third ~= nil) then index = index + 3 -- continue after the overwriting character else return false end end return true end --- Search the beginning of the current buffer to detect if it contains a man --- page. local function detect_man_page_in_current_buffer() -- Only check the first twelve lines (for speed). for _, line in ipairs(nvim.nvim_buf_get_lines(0, 0, 12, false)) do if detect_man_page_helper(line) then return true end end return false end --- Remove ansi escape sequences from the current buffer. local function strip_ansi_escape_sequences_from_current_buffer() local modifiable = nvim.nvim_buf_get_option(0, "modifiable") nvim.nvim_buf_set_option(0, "modifiable", true) nvim.nvim_command( [=[keepjumps silent %substitute/\v\e\[[;?]*[0-9.;]*[a-z]//egi]=]) nvim.nvim_win_set_cursor(0, {1, 0}) nvim.nvim_buf_set_option(0, "modifiable", modifiable) end --- Detect possible filetypes for the current buffer by looking at the pstree --- or ansi escape sequences or manpage sequences in the current buffer. local function detect_filetype() if not doc and detect_man_page_in_current_buffer() then doc = 'man' end if doc == 'git' then if nvimpager.git_colors then -- Use the highlighting from the git commands. doc = nil else -- Use nvim's syntax highlighting for git buffers instead of git's -- internal highlighting. strip_ansi_escape_sequences_from_current_buffer() end end -- python uses the same "highlighting" technique with backspace as roff. -- This means we have to load the full :Man plugin for python as well and -- not just set the filetype to man. if doc == 'man' or doc == 'pydoc' then nvim.nvim_buf_set_option(0, 'readonly', false) nvim.nvim_command("Man!") nvim.nvim_buf_set_option(0, 'readonly', true) -- do not set the file type again later on doc = nil elseif doc == 'perldoc' or doc == 'ri' then doc = 'man' -- only set the syntax, not the full :Man plugin end if doc ~= nil then nvim.nvim_buf_set_option(0, 'filetype', doc) end end --- Setup function to be called from --cmd. function nvimpager.stage1() -- Don't remember file names and positions nvim.nvim_set_option('shada', '') -- prevent messages when opening files (especially for the cat version) nvim.nvim_set_option('shortmess', nvim.nvim_get_option('shortmess')..'F') -- Define autocmd group for nvimpager. local group = nvim.nvim_create_augroup('NvimPager', {}) local tmp = os.getenv('TMPFILE') if tmp and tmp ~= "" then nvim.nvim_create_autocmd("BufReadPost", {pattern = tmp, once = true, group = group, callback = function() nvim.nvim_buf_set_option(0, "buftype", "nofile") end}) nvim.nvim_create_autocmd("VimLeavePre", {pattern = "*", once = true, group = group, callback = function() os.remove(tmp) end}) end doc = detect_parent_process() if doc == 'git' then -- We disable modelines for this buffer as they could disturb the git -- highlighting in diffs. nvim.nvim_buf_set_option(0, 'modeline', false) nvim.nvim_set_option('modelines', 0) end -- Theoretically these options only affect the pager mode so they could also -- be set in stage2() but that would overwrite user settings from the init -- file. nvim.nvim_set_option('mouse', 'a') nvim.nvim_set_option('laststatus', 0) end --- Set up autocomands to start the correct mode after startup or for each --- file. This function assumes that in "cat mode" we are called with --- --headless and hence do not have a user interface. This also means that --- this function can only be called with -c or later as the user interface --- would not be available in --cmd. function nvimpager.stage2() detect_filetype() local callback, events if #nvim.nvim_list_uis() == 0 then callback, events = cat.cat_mode, 'VimEnter' else callback, events = pager.pager_mode, {'VimEnter', 'BufWinEnter'} end local group = nvim.nvim_create_augroup('NvimPager', {clear = false}) -- The "nested" in these autocomands enables nested executions of -- autocomands inside the *_mode() functions. See :h autocmd-nested. nvim.nvim_create_autocmd(events, {pattern = '*', callback = callback, nested = true, group = group}) end -- functions only exported for tests nvimpager._testable = { detect_man_page_helper = detect_man_page_helper, detect_process = detect_process, detect_parent_process = detect_parent_process, replace_prefix = replace_prefix, } return nvimpager nvimpager-0.14.0/lua/nvimpager/options.lua000066400000000000000000000006101516340106300205270ustar00rootroot00000000000000-- names that will be exported from this module return { -- user facing options maps = true, -- if the default mappings should be defined git_colors = false, -- if the highlighting from the git should be used -- follow the end of the file when it changes (like tail -f or less +F) follow = false, follow_interval = 500, -- interval to check the underlying file in ms } nvimpager-0.14.0/lua/nvimpager/pager.lua000066400000000000000000000050761516340106300201450ustar00rootroot00000000000000--- Functions for the pager mode of nvimpager. -- Neovim defines this object but luacheck doesn't know it. So we define a -- shortcut and tell luacheck to ignore it. local vim = vim -- luacheck: ignore local nvim = vim.api -- luacheck: ignore local nvimpager = require("nvimpager/options") local util = require("nvimpager/util") local ansi2highlight = require("nvimpager/ansi2highlight") local follow_timer = nil function nvimpager.toggle_follow() if follow_timer ~= nil then vim.fn.timer_pause(follow_timer, nvimpager.follow) nvimpager.follow = not nvimpager.follow else follow_timer = vim.fn.timer_start( nvimpager.follow_interval, function() nvim.nvim_command("silent checktime") nvim.nvim_command("silent $") end, { ["repeat"] = -1 }) nvimpager.follow = true end end --- Set up mappings to make nvim behave a little more like a pager. local function set_maps() local function map(lhs, rhs, mode) -- we are using buffer local maps because we want to overwrite the buffer -- local maps from the man plugin (and maybe others) vim.keymap.set(mode or 'n', lhs, rhs, { buffer = true }) end map('q', 'quitall!') map('q', 'quitall!', 'v') map('', '') map('', '') map('g', 'gg') map('', '') map('', '') map('k', '') map('j', '') map('F', nvimpager.toggle_follow) end --- Setup function for the VimEnter autocmd. --- This function will be called for each buffer once local function pager_mode() if util.check_escape_sequences() then -- Try to highlight ansi escape sequences. ansi2highlight.run() -- Lines with concealed ansi esc sequences seem shorter than they are (by -- character count) so it looks like they wrap to early and the concealing -- of escape sequences only works for the first &synmaxcol chars. nvim.nvim_buf_set_option(0, "synmaxcol", 0) -- unlimited nvim.nvim_win_set_option(0, "wrap", false) end nvim.nvim_buf_set_option(0, 'modifiable', false) nvim.nvim_buf_set_option(0, 'modified', false) if nvimpager.maps then -- if this is done in VimEnter it will override any settings in the user -- config, if we do it globally we are not overwriting the mappings from -- the man plugin set_maps() end -- Check if the user requested follow mode on startup if nvimpager.follow then -- turn follow mode of so that we can use the init logic in toggle_follow nvimpager.follow = false nvimpager.toggle_follow() end end --- @export return { pager_mode = pager_mode, } nvimpager-0.14.0/lua/nvimpager/util.lua000066400000000000000000000011611516340106300200130ustar00rootroot00000000000000--- Utility functions for nvimpager local nvim = vim.api -- luacheck: ignore --- Check if the beginning of the current buffer contains ansi escape sequences. --- --- For performance only the first 100 lines are checked. local function check_escape_sequences() local filetype = nvim.nvim_buf_get_option(0, 'filetype') if filetype == '' or filetype == 'text' then for _, line in ipairs(nvim.nvim_buf_get_lines(0, 0, 100, false)) do if line:find('\27%[[;?]*[0-9.;]*[A-Za-z]') ~= nil then return true end end end return false end --- @export return { check_escape_sequences = check_escape_sequences, } nvimpager-0.14.0/makefile000066400000000000000000000052631516340106300152710ustar00rootroot00000000000000DESTDIR ?= PREFIX ?= /usr/local RUNTIME = $(PREFIX)/share/nvimpager/runtime VERSION = $(lastword $(shell ./nvimpager -v)) BUSTED = busted %.configured: % sed 's#^RUNTIME=.*$$#RUNTIME='"'$(RUNTIME)'"'#;s#version=.*$$#version=$(VERSION)#' < $< > $@ chmod +x $@ install-no-man: nvimpager.configured mkdir -p $(DESTDIR)$(PREFIX)/bin $(DESTDIR)$(RUNTIME)/lua/nvimpager \ $(DESTDIR)$(PREFIX)/share/zsh/site-functions install nvimpager.configured $(DESTDIR)$(PREFIX)/bin/nvimpager install -m 644 lua/nvimpager/*.lua $(DESTDIR)$(RUNTIME)/lua/nvimpager install -m 644 _nvimpager $(DESTDIR)$(PREFIX)/share/zsh/site-functions install: install-no-man nvimpager.1 mkdir -p $(DESTDIR)$(PREFIX)/share/man/man1 install -m 644 nvimpager.1 $(DESTDIR)$(PREFIX)/share/man/man1 uninstall: $(RM) -r $(PREFIX)/bin/nvimpager $(RUNTIME)/lua/nvimpager \ $(PREFIX)/share/man/man1/nvimpager.1 \ $(PREFIX)/share/zsh/site-functions/_nvimpager nvimpager.1: SOURCE_DATE_EPOCH = $(shell git log -1 --no-show-signature --pretty="%ct" 2>/dev/null || echo 1775108659) nvimpager.1: nvimpager.md sed '1s/$$/ "nvimpager $(VERSION)"/' $< | scdoc > $@ # The patterns prefixed with "lua" are used to require our nvimpager code from # the tests with the same module names that neovim would find them. # The patterns without prefix are to find the helper modules in the test # directory. LPATH = --lpath "lua/?.lua" --lpath "lua/?/init.lua" \ --lpath "?.lua" --lpath "?/init.lua" test: @$(BUSTED) $(LPATH) test luacov.stats.out: nvimpager lua/nvimpager/*.lua test/unit_spec.lua @$(BUSTED) $(LPATH) --coverage test/unit_spec.lua luacov.report.out: luacov.stats.out luacov lua/nvimpager TYPE = minor version: OLD_VERSION = $(patsubst v%,%,$(lastword $(shell git tag --list --sort=version:refname 'v*'))) version: NEW_VERSION = $(shell echo $(OLD_VERSION) | awk -F . -v type=$(TYPE) \ -e 'type == "major" { print $$1+1 ".0.0" }' \ -e 'type == "minor" { print $$1 "." $$2+1 ".0" }' \ -e 'type == "patch" { print $$1 "." $$2 "." $$3+1 }') version: [ $(TYPE) = major ] || [ $(TYPE) = minor ] || [ $(TYPE) = patch ] git switch main git diff --quiet HEAD sed -i 's/version=[0-9.]*$$/version=$(NEW_VERSION)/' nvimpager sed -i '/SOURCE_DATE_EPOCH/s/[0-9]\{10,\}/$(shell date +%s)/' $(MAKEFILE_LIST) (printf '%s\n' 'Version $(NEW_VERSION)' '' 'Major changes:' 'Breaking changes:' 'Changes:'; \ git log v$(OLD_VERSION)..HEAD) \ | sed -E '/^(commit|Merge:|Author:)/d; /^Date/{N;N; s/.*\n.*\n /-/;}' \ | git commit --edit --file - nvimpager makefile git tag --message="$$(git show --no-patch --format=format:%s%n%n%b)" \ v$(NEW_VERSION) clean: $(RM) nvimpager.configured nvimpager.1 luacov.* .PHONY: clean install test uninstall version nvimpager-0.14.0/nvimpager000077500000000000000000000054511516340106300155060ustar00rootroot00000000000000#!/usr/bin/env bash # Copyright (c) 2017 Lucas Hoffmann # Licenced under a BSD-2-clause licence. See the LICENSE file. RUNTIME=${BASH_SOURCE%/*} PARENT=$PPID TMPFILE= export RUNTIME export PARENT export TMPFILE export NVIM_APPNAME=nvimpager mode=auto nvim=${NVIMPAGER_NVIM:-nvim} usage () { echo "Usage: ${0##*/} [-acp] [--] [nvim options and files]" echo " ${0##*/} -h" echo " ${0##*/} -v" } description () { cat <<-EOF $NVIM_APPNAME provides a simple pager based on neovim. Options: -h this help -v version output -a enforce auto mode (default) -c enforce cat mode -p enforce pager mode All further arguments are passed to neovim. But one has to add "--" if the first argument is an option in order to stop this script from interpreting it. If "-" or no files are given stdin is read. In auto mode, if the cumulative length of all file arguments is smaller than the terminal size, cat mode is used, otherwise pager mode is used. If any none file argument (neovim option) is given pager mode is implied. EOF } while getopts achpv flag; do case $flag in a) mode=auto;; c) mode=cat;; h) usage; description; exit;; p) mode=pager;; v) version=$(git -C "$RUNTIME" describe 2>/dev/null) || version=0.14.0 echo "$NVIM_APPNAME ${version#v}" exit ;; *) usage >&2; exit 2;; esac done shift $((OPTIND - 1)) # Display the usage text if no arguments where given and stdin is a tty. if [[ $# -eq 0 && -t 0 ]]; then usage exit 2 fi # If we are not on a tty just "be" cat. if [[ ! -t 1 && $mode = auto ]]; then exec cat "$@" fi # Collect all file arguments until the first non file into $files. If one non # file is found pager mode is enforced. The special "file"-name "-" is # accepted as stdin. files=() while [[ $# -gt 0 ]]; do if [[ -f $1 ]]; then files+=("$1") shift elif [[ $1 = - ]]; then TMPFILE=$(mktemp) files+=("$TMPFILE") shift else if [[ $mode = auto ]]; then mode=pager fi break fi done # If we did not get any file arguments and stdin is not a terminal, read stdin # into a temp file. if [[ -z $TMPFILE && ${#files[@]} -eq 0 && ! -t 0 ]]; then TMPFILE=$(mktemp) files=("$TMPFILE") fi if [[ $TMPFILE ]]; then # Bash runs the EXIT trap also when exiting due to signals. trap 'rm -f "$TMPFILE"' EXIT cat > "$TMPFILE" fi # these come before all user supplied -c and --cmd arguments args=( -R --cmd 'set rtp+=$RUNTIME | lua nvimpager = require("nvimpager"); nvimpager.stage1()' -c 'lua nvimpager.stage2()' ) # Switch to cat mode if all files combined are shorter than the terminal # height. if [[ $mode = cat || \ $mode = auto && $(cat "${files[@]}" | wc -l) -le $(tput lines) ]] then args+=(--headless) fi exec -a nvimpager $nvim "${args[@]}" "${files[@]}" "$@" nvimpager-0.14.0/nvimpager.md000066400000000000000000000131101516340106300160710ustar00rootroot00000000000000nvimpager(1) # NAME nvimpager - using neovim as a pager # SYNOPSIS *nvimpager* [*-acp*] [\--] [nvim options and files] ++ *nvimpager* *-h* ++ *nvimpager* *-v* # DESCRIPTION Nvimpager is a small program that can be used like most other pagers. Internally it uses neovim with the default TUI to display the text. This means it has all the fancy syntax highlighting, mouse support and other features of neovim available in the pager, possibly including plugins! # COMMAND LINE OPTIONS Nvimpager itself interprets only very few options but all neovim options can also be specified. If options to neovim are specified before the first file name they must be preceded by "\--" to prevent nvimpager from trying to interpret them. The following options are interpreted by nvimpager itself: *-a* Run in "auto mode" (default). Auto mode will detect the terminal size and switch to pager mode if the content to display would not fit on one screen. If the content will fit on one screen it will switch to cat mode. This overrides any previous *-c* and *-p* options. *-c* Run in "cat mode". Do not start the neovim TUI, only use neovim for syntax highlighting and print the result to stdout. This overrides any previous *-a* and *-p* options. *-h* Show the help screen and exit *-p* Run in "pager mode". Start the neovim TUI to display the given content. This overrides any previous *-a* and *-c* options. *-v* Show version information and exit # CONFIGURATION Like neovim itself nvimpager will honour *$XDG_CONFIG_HOME* and *$XDG_DATA_HOME*, which default to *~/.config* and *~/.local* respectively. The main config directory is *$XDG_CONFIG_HOME/nvimpager* and the main user config file is *$XDG_CONFIG_HOME/nvimpager/init.lua* or *$XDG_CONFIG_HOME/nvimpager/init.vim*. It is an error if both files are present. The site directory is *$XDG_DATA_HOME/.local/share/nvimpager/site*. The manifest for remote plugins is read from (and written to) *$XDG_DATA_HOME/nvimpager/rplugin.vim*. The rest of the *&runtimepath* is configured like for neovim. The *-u* option of *nvim*(1) itself can be used to change the main config file from the command line. The default config files for neovim are not used by design as these potentially load many plugins and do a lot of configuration that is only relevant for editing. If one really wants to use the same config files for both nvimpager and nvim it is possible to do so by symlinking the config and site directories and the rplugin file. ## Environment variables The environment variable *$NVIMPAGER_NVIM* can be used to specify an nvim executable to use. If unset it defaults to *nvim*. ## Configuration variables The script exposes a lua table called *nvimpager* to *--cmd*/*-c* options and the init file. It can be modified to change some options that are specific to nvimpager. The following fields (options) exist: [[ *option* :- *type* :- *default* :< *explanation* | follow : bool : false : start in follow mode, i.e. continuously load changes to the opened file and scroll to the bottom (like *less +F* or *tail -f*) | follow_interval : number : 500 : how often in ms the underlying file should be checked in follow mode | git_colors : bool : false : use git command highlighting instead of nvim syntax highlighting, set this to true if you use an external diff | maps : bool : true : if some default less like maps should be defined inside pager mode So to start nvimpager and follow changes to the opened file the user can put ``` lua nvimpager.follow = true ``` in the init file (or on the command line). ## Default key mappings Nvimpager defines some mappings to make it feel more like a pager than an editor. These mappings are inspired by *less*(1) which are very close to the defaults in neovim. These mappings can be deactivated altogether by putting ``` lua nvimpager.maps = false ``` in the init file (or on the command line). The following mappings are defined by default: - *q* is mapped to quit nvimpager in normal and visual mode - ** and ** move down or up a page respectively - *g* goes to the top of the file - ** and *j* scroll the window down one line - ** and *k* scroll the window up one line - *F* toggles "follow mode" where nvimpager continuously loads changes to the underlying file and scrolls to the bottom. This is useful for watching log files. It is modeled after the *F* command in *less*(1) or the *-f* option of *tail*(1) You can remap the lua function `nvimpager.toggle_follow` if you disabled the default key mappings. # EXAMPLES To use nvimpager to view a file (with neovim's syntax highlighting if the filetype is detected): ``` nvimpager file ``` Pipe text into nvimpager to view it: ``` echo text | nvimpager ``` Use nvimpager as your default *$PAGER* to view man pages or git diffs: ``` export PAGER=nvimpager man nvimpager git diff ``` Options for *nvim*(1) can be specified if they are separated from the options for nvimpager itself. Either by separating them with *--* or by putting the nvim options after at least one non option argument: ``` nvimpager -p -- -c 'echo "option for nvim"' file nvimpager -p file -u custom_init.vim ``` Start nvimpager in "follow mode" to watch a growing log file: ``` nvimpager log_file -c 'lua nvimpager.follow = true' ``` # LIMITATIONS If reading from stdin, nvimpager (like *nvim*(1)) waits for EOF until it starts up. This means that it can not be used to continuously watch output from a long running command even in follow mode. # SEE ALSO *nvim(1)* https://github.com/neovim/neovim *vimpager(1)* https://github.com/rkitover/vimpager # AUTHORS Lucas Hoffmann nvimpager-0.14.0/test/000077500000000000000000000000001516340106300145425ustar00rootroot00000000000000nvimpager-0.14.0/test/abort_test.lua000066400000000000000000000002711516340106300174130ustar00rootroot00000000000000-- we will report an error vim.fn.assert_report("this is an error") -- and then immediately quit so that the test framework can not write the error -- to the output file vim.cmd.quit() nvimpager-0.14.0/test/diff_test.lua000066400000000000000000000001211516340106300172060ustar00rootroot00000000000000vim.cmd.edit("test/fixtures/diff2") vim.fn.assert_equal("diff", vim.o.filetype) nvimpager-0.14.0/test/first_test.lua000066400000000000000000000013331516340106300174330ustar00rootroot00000000000000-- assert some things about the nvimpager module that we export vim.fn.assert_equal("table", type(nvimpager), "nvimpager module not loaded") vim.fn.assert_equal("boolean", type(nvimpager.maps)) vim.fn.assert_equal("boolean", type(nvimpager.follow)) vim.fn.assert_equal("number", type(nvimpager.follow_interval)) -- assert that the stage2 function has run, i.e. by checking the autocommands -- it should set up local cmds = vim.api.nvim_get_autocmds({group = "NvimPager"}) vim.fn.assert_equal(2, #cmds, "missing nvimpager autocommands") -- check that the buffer local mappings are defined local maps = vim.api.nvim_buf_get_keymap(0, "v") vim.fn.assert_equal("q", maps[1].lhs) vim.fn.assert_equal("quitall!", maps[1].rhs) nvimpager-0.14.0/test/fixtures/000077500000000000000000000000001516340106300164135ustar00rootroot00000000000000nvimpager-0.14.0/test/fixtures/ansi-erase-line.txt000066400000000000000000000000531516340106300221260ustar00rootroot00000000000000foobar foobar foobar foobar nvimpager-0.14.0/test/fixtures/bin/000077500000000000000000000000001516340106300171635ustar00rootroot00000000000000nvimpager-0.14.0/test/fixtures/bin/git000077700000000000000000000000001516340106300224662test-parent.shustar00rootroot00000000000000nvimpager-0.14.0/test/fixtures/bin/man000077700000000000000000000000001516340106300224562test-parent.shustar00rootroot00000000000000nvimpager-0.14.0/test/fixtures/bin/test-parent.sh000077500000000000000000000005511516340106300217710ustar00rootroot00000000000000#!/bin/sh # A small wrapper script for the tests. It can be linked to any name in order # to simulate different parent processes. # We set PARENT to the process id of the current script in order to allow # nvimpager's lua code to be called directly here. This variable is otherwise # set up by the bash script and passed to the lua code. PARENT=$$ "$@" 2>&1 nvimpager-0.14.0/test/fixtures/conceal.tex000066400000000000000000000000211516340106300205320ustar00rootroot00000000000000\ss should be ß nvimpager-0.14.0/test/fixtures/conceal.tex.ansi000066400000000000000000000000501516340106300214650ustar00rootroot00000000000000ß should be ß nvimpager-0.14.0/test/fixtures/conceal.tex.cole0.ansi000066400000000000000000000000461516340106300224730ustar00rootroot00000000000000\ss should be ß nvimpager-0.14.0/test/fixtures/conceal.tex.cole1.ansi000077700000000000000000000000001516340106300255462conceal.tex.ansiustar00rootroot00000000000000nvimpager-0.14.0/test/fixtures/conceal.tex.cole2.ansi000077700000000000000000000000001516340106300255472conceal.tex.ansiustar00rootroot00000000000000nvimpager-0.14.0/test/fixtures/conceal.tex.red000066400000000000000000000000461516340106300213120ustar00rootroot00000000000000\ss should be ß nvimpager-0.14.0/test/fixtures/diff000066400000000000000000000013121516340106300172430ustar00rootroot00000000000000diff --git a/test/nvimpager_spec.lua b/test/nvimpager_spec.lua index 68e2097..4593495 100644 --- a/test/nvimpager_spec.lua +++ b/test/nvimpager_spec.lua @@ -405,3 +405,17 @@ describe("lua functions", function() end) end) end) + +describe("parent detection", function() + it("handles git", function() + local output = run("test/fixtures/bin/git ./nvimpager -c test/fixtures/diff") + local expected = read("test/fixtures/diff.ansi") + assert.equal(expected, output) + end) + + it("handles man", function() + local output = run("test/fixtures/bin/man ./nvimpager -c test/fixtures/man.cat") + local expected = read("test/fixtures/man.ansi") + assert.equal(expected, output) + end) +end) nvimpager-0.14.0/test/fixtures/diff-modeline000066400000000000000000000004211516340106300210350ustar00rootroot00000000000000diff --git a/nvimpager b/nvimpager index d7d4e2f..99fb715 100755 --- a/nvimpager +++ b/nvimpager @@ -104,4 +104,4 @@ then default_args+=(--headless) fi nvim "${default_args[@]}" "${files[@]}" "$@" Date: Mon Aug 20 23:37:04 2018 +0200 This is based on the log entry of 3d032f9e546fc8a1e987bbbef54e8dab1ceaf4bd but was modiefied for the tests. The last line is turned into a valid modeline even with the diff prefix. If it is interpreted it should result in an error message. diff --git a/test/fixtures/help.txt b/test/fixtures/help.txt index ad4baf6..ab64439 100644 --- a/test/fixtures/help.txt +++ b/test/fixtures/help.txt @@ -1,3 +1,3 @@ -vim: filetype=help *conceal-test* normal conceal-test + vim: foobar=error nvimpager-0.14.0/test/fixtures/git-log.ansi000066400000000000000000000014571516340106300206400ustar00rootroot00000000000000commit 3d032f9e546fc8a1e987bbbef54e8dab1ceaf4bd Author: Lucas Hoffmann <lucc@posteo.de> Date: Mon Aug 20 23:37:04 2018 +0200  This is based on the log entry of 3d032f9e546fc8a1e987bbbef54e8dab1ceaf4bd but was modiefied for the tests. The last line is turned into a valid modeline even with the diff prefix. If it is interpreted it should result in an error message. diff --git a/test/fixtures/help.txt b/test/fixtures/help.txt index ad4baf6..ab64439 100644 --- a/test/fixtures/help.txt +++ b/test/fixtures/help.txt @@ -1,3 +1,3 @@ -vim: filetype=help  *conceal-test* normal conceal-test + vim: foobar=error nvimpager-0.14.0/test/fixtures/help.txt000066400000000000000000000000661516340106300201060ustar00rootroot00000000000000*conceal-test* normal conceal-test vim: filetype=help nvimpager-0.14.0/test/fixtures/help.txt.ansi000066400000000000000000000001431516340106300210330ustar00rootroot00000000000000conceal-test normal conceal-test vim: filetype=help nvimpager-0.14.0/test/fixtures/help.txt.cole0.ansi000066400000000000000000000001231516340106300220320ustar00rootroot00000000000000*conceal-test* normal conceal-test vim: filetype=help nvimpager-0.14.0/test/fixtures/help.txt.cole1.ansi000066400000000000000000000001451516340106300220370ustar00rootroot00000000000000 conceal-test  normal conceal-test vim: filetype=help nvimpager-0.14.0/test/fixtures/help.txt.cole2.ansi000077700000000000000000000000001516340106300244552help.txt.ansiustar00rootroot00000000000000nvimpager-0.14.0/test/fixtures/listchars1.txt000066400000000000000000000000531516340106300212270ustar00rootroot00000000000000Here are spaces. And two trailing space: nvimpager-0.14.0/test/fixtures/listchars1.txt.24bit000066400000000000000000000003451516340106300221560ustar00rootroot00000000000000Here_are_spaces.$ And_two_trailing_space:--$ nvimpager-0.14.0/test/fixtures/listchars1.txt.8bit000066400000000000000000000002051516340106300220730ustar00rootroot00000000000000Here_are_spaces.$ And_two_trailing_space:--$ nvimpager-0.14.0/test/fixtures/makefile000066400000000000000000000001201516340106300201040ustar00rootroot00000000000000# a test makefile PREFIX = nothing all: nothing run command --option=$(PREFIX) nvimpager-0.14.0/test/fixtures/makefile.ansi000066400000000000000000000002051516340106300210410ustar00rootroot00000000000000# a test makefile PREFIX = nothing all: nothing  run command --option=$(PREFIX) nvimpager-0.14.0/test/fixtures/man.1000066400000000000000000000001721516340106300172500ustar00rootroot00000000000000.\" Automatically generated by Pandoc 2.2.1 .\" .TH "TEST" "0" "date" "" "" .hy .SH SYNOPSIS .PP exit .SH AUTHORS Author. nvimpager-0.14.0/test/fixtures/man.ansi000066400000000000000000000003701516340106300200420ustar00rootroot00000000000000TEST(0) TEST(0) SYNOPSIS  exit AUTHORS  Author.  date TEST(0) nvimpager-0.14.0/test/fixtures/man.cat000066400000000000000000000003151516340106300176560ustar00rootroot00000000000000TEST(0) TEST(0) SYNOPSIS exit AUTHORS Author. date TEST(0) nvimpager-0.14.0/test/fixtures/man.md000066400000000000000000000000521516340106300175050ustar00rootroot00000000000000% TEST(0) % Author % date # SYNOPSIS exit nvimpager-0.14.0/test/fixtures/nbsp.ansi000066400000000000000000000000351516340106300202270ustar00rootroot00000000000000nbsp+nbsp nvimpager-0.14.0/test/fixtures/nbsp.latin1.txt000066400000000000000000000000121516340106300212760ustar00rootroot00000000000000nbsp nbsp nvimpager-0.14.0/test/fixtures/nbsp.utf8.txt000066400000000000000000000000131516340106300207750ustar00rootroot00000000000000nbsp nbsp nvimpager-0.14.0/test/fixtures/plain.red000066400000000000000000000000301516340106300202030ustar00rootroot00000000000000some text nvimpager-0.14.0/test/fixtures/plain.red24000066400000000000000000000000541516340106300203570ustar00rootroot00000000000000some text nvimpager-0.14.0/test/fixtures/plain.txt000066400000000000000000000000121516340106300202500ustar00rootroot00000000000000some text nvimpager-0.14.0/test/helpers.lua000066400000000000000000000105341516340106300167120ustar00rootroot00000000000000--- Helper functions for the test suite --- --- @module test.helpers -- assert library used by busted local assert = require("luassert") --- the temporary directory for the tests local tmp = os.getenv("TMPDIR") or "/tmp" --- the $XDG_CONFIG_HOME for the tests local confdir = tmp .. "/nvimpager-testsuite/no-config" --- the $XDG_DATA_HOME for the tests local datadir = tmp .. "/nvimpager-testsuite/no-data" --- Run a shell command, assert it terminates with return code 0 and return its --- output. --- --- The assertion of the return status works even with Lua 5.1. The last byte --- of output of the command *must not* be a decimal digit. --- --- @param command string -- the shell command to execute --- @return string -- the output of the command local function run(command) -- From Lua 5.2 on we could use io.close to retrieve the return status of -- the process. It would return true, "exit", x where x is the status. -- For Lua 5.1 (currently used by neovim) we have to echo the return status -- in the shell command and extract it from the output. -- References: -- https://www.lua.org/manual/5.1/manual.html#pdf-io.close -- https://www.lua.org/manual/5.1/manual.html#pdf-file:close -- https://www.lua.org/manual/5.2/manual.html#pdf-io.close -- https://www.lua.org/manual/5.2/manual.html#pdf-file:close -- https://www.lua.org/manual/5.2/manual.html#pdf-os.execute -- https://stackoverflow.com/questions/7607384 command = string.format( "export XDG_CONFIG_HOME=%s XDG_DATA_HOME=%s; (%s) 2>&1; echo $?", confdir, datadir, command) local proc = io.popen(command) if proc == nil then error("Could not open pipe to child process") end local output = proc:read('*a') local status = {proc:close()} -- This is *not* the return value of the command. assert.equal(true, status[1]) -- In Lua 5.2 we could also assert this and it would be meaningful: -- assert.equal("exit", status[2]) -- assert.equal(0, status[3]) -- For Lua 5.1 we have echoed the return status with the output. First we -- assert the last two bytes, which is easy: assert.equal("0\n", output:sub(-2), "command failed") -- When the original command did not produce any output this is it. if #output ~= 2 then -- Otherwise we can only hope that the command did not produce a digit as -- it's last character of output. assert.is_nil(tonumber(output:sub(-3, -3)), "command failed") end -- If the assert succeeded we can remove two bytes from the end. return output:sub(1, -3) end --- Read contents of a file and return them. --- --- @param filename string -- the name of the file to read --- @return string -- the contents of the file local function read(filename) local file = io.open(filename) if file == nil then error("Could not open file: " .. filename) end local contents = file:read('*a') return contents end --- Write contents to a file. --- --- @param filename string -- the name of the file to write --- @param contents string -- the contents to write to the file --- @return nil local function write(filename, contents) local handle = io.open(filename, "w") if handle == nil then error("could not open file " .. filename) end handle:write(contents) handle:flush() handle:close() end --- Freshly require a nvimpager module, optionally with mocks --- --- @param module string -- the module name under lua/nvimpager to require --- @param api table|nil -- a mock for the neovim api table (:help lua-api) --- @return table -- the nvimpager module local function load_nvimpager(module, api) -- Create a local mock of the vim module that is provided by neovim. local default_api = { nvim_get_hl = function() return {} end, -- These can return different types so we just default to nil. nvim_call_function = function() end, nvim_get_option = function() end, } if api == nil then api = default_api else for key, value in pairs(default_api) do if api[key] == nil then api[key] = value end end end local vim = { api = api, o = setmetatable({}, { __index = function() return "" end }) } -- Register the api mock in the globals. _G.vim = vim -- Reload the nvimpager script package.loaded["lua/nvimpager/"..module] = nil return require("lua/nvimpager/"..module) end --- @export return { confdir = confdir, datadir = datadir, load_nvimpager = load_nvimpager, read = read, run = run, write = write, } nvimpager-0.14.0/test/meta_test.lua000066400000000000000000000000511516340106300172260ustar00rootroot00000000000000vim.fn.assert_report("this is an error") nvimpager-0.14.0/test/native_spec.lua000066400000000000000000000061251516340106300175510ustar00rootroot00000000000000--- Wrapper for test cases written with neovim's native assert functions. --- @module test.native_spec -- Busted defines these objects but luacheck doesn't know them. So we -- redefine them and tell luacheck to ignore it. local describe, it, assert = describe, it, assert -- luacheck: ignore local helpers = require("test/helpers") --- Run a test file in nvimpager's pager mode --- --- The test case will start nvimpager in pager mode and source the given file. --- The messages from v:errors will be collected from within nvimpager and --- returned by this function (as a single string) local function run_test_file(filename, extra_args) extra_args = extra_args or "" -- create an output file to transport the errors from within neovim to -- busted (it is complicated to write to stdout or stderr from within neovim -- and catch that from the outside) local outfile = os.tmpname() local output -- We write a marker to the output file in order to detect if the -- nvimpager command below really writes to the file. If it does write to -- the file our marker should be overwritten. local handle, err1 = io.open(outfile, "w") if handle == nil then error(err1) end handle:write("This should be overwritten") handle:close() -- run the actual test in protected mode in order to clean up the temp file -- if anything fails local status, err2 = pcall(function() -- Run the given test file in nvimpager and write all errors from the -- assert_* functions into outfile. helpers.run( -- define an environment variable to hold the output file name "OUTFILE='" .. outfile:gsub("'", [['\'']]) .. "' " .. -- run nvimpager in pager mode "./nvimpager -p -- " .. -- source the given lua test file "-S " .. filename .. -- write all errors reported by assert_* functions to the output file " -c 'call writefile(v:errors, $OUTFILE)' " .. -- force quit nvimpager "-c 'quitall!' " .. extra_args ) end) if status then output = helpers.read(outfile) end -- clean up the temp file os.remove(outfile) -- return the contents of the output file or re-through the error if status then return output else error(err2) end end local function test(title, filename, extra_args) it(title, function() assert.equal("", run_test_file(filename, extra_args)) end) end describe("native", function() -- tests for the custom test framework of this file describe("test framework", function() it("reports assert_* failures from neovim", function() local output = run_test_file("test/meta_test.lua") assert.not_nil(output:find("this is an error")) end) it("detects aborted test scripts", function() local output = run_test_file("test/abort_test.lua") assert.equal("This should be overwritten", output) end) end) describe("pager mode", function() test("loads the nvimpager table", "test/first_test.lua") test("detects diffs correctly", "test/diff_test.lua") test("maps can be deactivated", "test/no_map_test.lua", "--cmd 'lua nvimpager.maps = false'") end) end) nvimpager-0.14.0/test/no_map_test.lua000066400000000000000000000001721516340106300175550ustar00rootroot00000000000000vim.fn.assert_equal({}, vim.api.nvim_buf_get_keymap(0, "n")) vim.fn.assert_equal({}, vim.api.nvim_buf_get_keymap(0, "v")) nvimpager-0.14.0/test/nvimpager_spec.lua000066400000000000000000000327611516340106300202600ustar00rootroot00000000000000--- Busted tests for nvimpager --- @module test.nvimpager_spec -- Busted defines these objects but luacheck doesn't know them. So we -- redefine them and tell luacheck to ignore it. local describe, it, assert = describe, it, assert -- luacheck: ignore local helpers = require("test/helpers") local run, read, write = helpers.run, helpers.read, helpers.write describe("auto mode", function() -- Auto mode only exists during the run of the bash script. At the end of -- the bash script it has to decide if pager or cat mode is used. This -- makes these tests a little more difficult. We have to inspect the state -- of the bash script in some way. -- Source the given command line in a bash script with some mocks and print -- all set variables at the end. -- -- command: string -- the shell command to execute -- returns: string -- the output of the sourced command and all set -- variables local function bash(command) -- Make nvim an alias with a semicolon so potential redirections in the -- original nvim execution don't take effect. Also mock exec and trap. local script = [[ set -e set -u shopt -s expand_aliases NVIMPAGER_NVIM=: alias exec=: alias trap=: source ]] .. command .. "\nset" local filename = os.tmpname() write(filename, script) local output = run("bash " .. filename) os.remove(filename) return output end it("selects cat mode for small files", function() local output = bash('./nvimpager test/fixtures/makefile') -- $mode might still be auto so we check the generated command line. local args = output:match("\nargs[^\n]*\n") assert.truthy(args:match('--headless')) end) it("auto mode selects pager mode for big inputs", function() local output = bash('./nvimpager ./README.md ./nvimpager') -- $mode might still be auto so we check the generated command line. local args = output:match("\nargs[^\n]*\n") assert.is_nil(args:match('--headless')) end) end) describe("cat mode", function() describe("without files", function() -- this test behaves differently in nix's build sandbox (could be fixed -- with script(1)) and in the ci environment on github it("displays a help text #nix #mac #ppa #appimage", function() local output = run("./nvimpager -c; [ $? -eq 2 ]") local expected = [[Usage: nvimpager [-acp] [--] [nvim options and files] nvimpager -h nvimpager -v]] .. "\n" assert.equal(expected, output) end) it("does not break if forced to run", function() local output = run("./nvimpager -c -- -c 'let foo = 1'") assert.equal("", output) end) it("reads stdin", function() local output = run("echo text on stdin | ./nvimpager -c") assert.equal("\27[0mtext on stdin\27[0m\n", output) end) end) it("displays a small file with syntax highlighting to stdout #v10", function() local output = run("./nvimpager -c test/fixtures/makefile") local expected = read("test/fixtures/makefile.ansi") assert.equal(expected, output) end) it("reads stdin with syntax highlighting #v10", function() local output = run("./nvimpager -c -- " .. "-c 'set filetype=make' " .. "< test/fixtures/makefile") local expected = read("test/fixtures/makefile.ansi") assert.equal(expected, output) end) it("returns ansi escape sequences unchanged", function() local output = run("./nvimpager -c < test/fixtures/makefile.ansi") local expected = read("test/fixtures/makefile.ansi") assert.equal(expected, output) end) it("handles color schemes with a non trivial Normal group #v10", function() local output = run("./nvimpager -c test/fixtures/conceal.tex " .. "--cmd 'hi Normal ctermfg=Red'") local expected = read("test/fixtures/conceal.tex.red") assert.equal(expected, output) end) it("highlights all files #v10", function() local output = run("./nvimpager -c test/fixtures/makefile " .. "test/fixtures/help.txt ") local expected = read("test/fixtures/makefile.ansi") .. read("test/fixtures/help.txt.ansi") assert.equal(expected, output) end) it("concatenates the same file twice #v10", function() local output = run("./nvimpager -c test/fixtures/makefile " .. "test/fixtures/makefile ") local expected = read("test/fixtures/makefile.ansi") expected = expected .. expected assert.equal(expected, output) end) it("produces no output for empty files", function() local tmp = os.tmpname() finally(function() os.remove(tmp) end) -- This hangs if /dev/null is used instead. local output = run("./nvimpager -c "..tmp) assert.equal('', output) end) it("produces no output for empty stdin", function() local output = run("./nvimpager -c &1") local expected = read("test/fixtures/diff-modeline.ansi") assert.equal(expected, output) end) it("ignores mode lines in git diffs #v10", function() local output = run("test/fixtures/bin/git ./nvimpager -c " .. "test/fixtures/diff-modeline 2>&1") local expected = read("test/fixtures/diff-modeline.ansi") assert.equal(expected, output) end) it("ignores mode lines in git log diffs #mac #v10", function() local output = run("test/fixtures/bin/git ./nvimpager -c " .. "test/fixtures/git-log 2>&1") local expected = read("test/fixtures/git-log.ansi") assert.equal(expected, output) end) end) describe("conceals #v10", function() local function test_level(level) local output = run("./nvimpager -c test/fixtures/help.txt " .. "-c 'set cole="..level.."'") local expected = read("test/fixtures/help.txt.cole"..level..".ansi") assert.equal(expected, output) end it("are removed at conceallevel=2", function() test_level(2) end) it("are hidden at conceallevel=1", function() test_level(1) end) it("are highlighted at conceallevel=0", function() test_level(0) end) end) describe("conceal replacements #v10", function() local function test_replace(level) local output = run("./nvimpager -c test/fixtures/conceal.tex ".. "--cmd \"let g:tex_flavor='latex'\" ".. "-c 'set cole="..level.."'") local expected = read("test/fixtures/conceal.tex.cole"..level..".ansi") assert.equal(expected, output) end it("are replaced at conceallevel=2", function() test_replace(2) end) it("are replaced at conceallevel=1", function() test_replace(1) end) it("are highlighted at conceallevel=0", function() test_replace(0) end) end) describe("listchars", function() it("handle spaces, trailing spaces and eol with termguicolors #v10", function() local output = run("./nvimpager -c test/fixtures/listchars1.txt " .. "--cmd 'se tgc list lcs+=space:_,eol:$'") local expected = read("test/fixtures/listchars1.txt.24bit") assert.equal(expected, output) end) it("handle spaces, trailing spaces and eol with 256 colors #v10", function() local output = run("./nvimpager -c test/fixtures/listchars1.txt " .. "--cmd 'se list lcs+=space:_,eol:$'") local expected = read("test/fixtures/listchars1.txt.8bit") assert.equal(expected, output) end) describe("handles non breaking spaces", function() local expected = read("test/fixtures/nbsp.ansi") it("in utf8 files #v10", function() local output = run("./nvimpager -c test/fixtures/nbsp.utf8.txt " .. "--cmd 'se list'") assert.equal(expected, output) end) it("in latin1 files #v10", function() local output = run("./nvimpager -c test/fixtures/nbsp.latin1.txt " .. "--cmd 'se list'") assert.equal(expected, output) end) end) end) end) describe("pager mode", function() it("starts up and quits correctly", function() run("./nvimpager -p makefile -c quit") end) end) describe("cat-exec mode", function() it("is selected when stdin is not a tty", function() local output = run("./nvimpager < README.md") local expected = read("README.md") assert.equal(expected, output) end) it("does not highlight files", function() local output = run("./nvimpager < test/fixtures/makefile") local expected = read("test/fixtures/makefile") -- NOTE: no highlight assert.equal(expected, output) end) end) describe("parent detection", function() -- Wrapper to execute some lua code in a --cmd argument. local function lua_with_parent(name, code) -- First we have to shellescape the lua code. code = code:gsub("'", "'\\''") local command = [[nvim --headless --clean --cmd 'set rtp+=.' --cmd 'lua ]] ..code..[[' --cmd quit]] return run("test/fixtures/bin/"..name.." "..command) end it("detects git correctly #mac #appimage", function() local output = lua_with_parent( "git", "print(require('nvimpager')._testable.detect_parent_process())") assert.equal("git", output) end) it("detects man correctly #mac #appimage", function() local output = lua_with_parent( "man", "print(require('nvimpager')._testable.detect_parent_process())") assert.equal("man", output) end) it("handles git #v10", function() local output = run("test/fixtures/bin/git ./nvimpager -c " .. "test/fixtures/diff") local expected = read("test/fixtures/diff.ansi") assert.equal(expected, output) end) it("can pass though git colors", function() local output = run("test/fixtures/bin/git ./nvimpager -c " .. "test/fixtures/difftastic --cmd 'lua nvimpager.git_colors=true'") local expected = read("test/fixtures/difftastic") assert.equal(expected, output) end) it("handles man #mac #nix #v10", function() local output = run("test/fixtures/bin/man ./nvimpager -c " .. "test/fixtures/man.cat") local expected = read("test/fixtures/man.ansi") assert.equal(expected, output) end) end) describe("init files", function() it("can be specified with -u", function() local init = os.tmpname() finally(function() os.remove(init) end) helpers.write(init, "let g:myvar = 42") local output = run("./nvimpager -c -- -u " .. init .. [[ -c 'lua io.write(vim.g.myvar, "\n")' -c qa]]) assert.equal("42\n", output) end) local function tempdir() local dir = run("mktemp -d"):sub(1, -2) -- remove the final newline finally(function() run("rm -r " .. dir) end) return dir end it("can be init.lua", function() local dir = tempdir() run("mkdir -p " .. dir .. "/nvimpager") helpers.write(dir .. "/nvimpager/init.lua", "vim.g.myvar = 42") local output = run("XDG_CONFIG_HOME=" .. dir .. [[ ./nvimpager -c -- -c 'lua io.write(vim.g.myvar, "\n")' -c qa]]) assert.equal("42\n", output) end) it("can be init.vim", function() local dir = tempdir() run("mkdir -p " .. dir .. "/nvimpager") helpers.write(dir .. "/nvimpager/init.vim", "let myvar = 42") local output = run("XDG_CONFIG_HOME=" .. dir .. [[ ./nvimpager -c -- -c 'lua io.write(vim.g.myvar, "\n")' -c qa]]) assert.equal("42\n", output) end) end) nvimpager-0.14.0/test/unit_spec.lua000066400000000000000000000366421516340106300172510ustar00rootroot00000000000000--- Unit tests for nvimpager --- @module test.unit_spec -- Busted defines these objects but luacheck doesn't know them. So we -- redefine them and tell luacheck to ignore it. local describe, it, assert, mock, setup, before_each = describe, it, assert, mock, setup, before_each -- luacheck: ignore local load_nvimpager = require("test/helpers").load_nvimpager describe("lua functions", function() local nvimpager setup(function() nvimpager = load_nvimpager("init") end) describe("split_rgb_number", function() local split_rgb_number = require("nvimpager/cat").split_rgb_number it("handles numbers from 0 to 16777215", function() local r, g, b = split_rgb_number(0x000000) assert.equal(0, r) assert.equal(0, g) assert.equal(0, b) r, g, b = split_rgb_number(0xFFFFFF) assert.equal(255, r) assert.equal(255, g) assert.equal(255, b) end) it("correctly splits rgb values", function() local r, g, b = split_rgb_number(0x55AACC) assert.equal(0x55, r) assert.equal(0xAA, g) assert.equal(0xCC, b) end) end) describe("group2ansi", function() it("calls nvim_get_hl with and without termguicolors", function() for _, termguicolors in pairs({true, false}) do local api = { nvim_get_hl = function() return {} end, nvim_get_option = function() return termguicolors end, nvim_call_function = function() return 0 end, } local m = mock(api) local cat = load_nvimpager("cat", api) cat.init() local escape = cat.group2ansi(100) assert.stub(m.nvim_get_hl).was.called_with(0, {id = 100, link = false}) assert.equal('\27[0m', escape) end end) end) describe("color2escape_24bit", function() local color2escape_24bit = require("nvimpager/cat").color2escape_24bit it("creates foreground escape sequences", function() local e = color2escape_24bit(0xaabbcc, true) assert.equal('38;2;170;187;204', e) end) it("creates background escape sequences", function() local e = color2escape_24bit(0xccbbaa, false) assert.equal('48;2;204;187;170', e) end) end) describe("color2escape_8bit", function() local color2escape_8bit = require("nvimpager/cat").color2escape_8bit it("creates 8 colors foreground escaape sequences", function() local e = color2escape_8bit(5, true) assert.equal('35', e) end) it("creates 8 colors background escaape sequences", function() local e = color2escape_8bit(7, false) assert.equal('47', e) end) it("creates 16 colors foreground escaape sequences", function() local e = color2escape_8bit(5 + 8, true) assert.equal('95', e) end) it("creates 16 colors background escaape sequences", function() local e = color2escape_8bit(7 + 8, false) assert.equal('107', e) end) it("creates foreground escape sequences", function() local e = color2escape_8bit(0xaa, true) assert.equal('38;5;170', e) end) it("creates background escape sequences", function() local e = color2escape_8bit(0xbb, false) assert.equal('48;5;187', e) end) end) describe("hexformat_rgb_numbers", function() local ansi2highlight = require("nvimpager/ansi2highlight") local function test(r, g, b, expected) local actual = ansi2highlight.hexformat_rgb_numbers(r, g, b) assert.equal(expected, actual) end it("small numbers", function() test(1, 2, 3, '#010203') end) it("big numbers", function() test(100, 200, 150, '#64c896') end) it("0,0,0 is black", function() test(0, 0, 0, '#000000') end) it("255,255,255 is white", function() test(255, 255, 255, '#ffffff') end) end) describe("split_predefined_terminal_color", function() local ansi2highlight = require("nvimpager/ansi2highlight") local function test(col, exp_r, exp_g, exp_b) local r, g, b = ansi2highlight.split_predefined_terminal_color(col) assert.equal(exp_r, r) assert.equal(exp_g, g) assert.equal(exp_b, b) end it("handles 0 as black", function() test(0, 0, 0, 0) end) it("handles 215 as white", function() test(215, 255, 255, 255) end) it("handles 137 as something", function() test(137, 175, 215, 255) end) end) describe("replace_prefix", function() it("can replace a simple prefix in a table of strings", function() local t = nvimpager._testable.replace_prefix({"foo", "bar", "baz"}, "b", "XXX") assert.same({"foo", "XXXar", "XXXaz"}, t) end) it("can replace strings with slashes", function() local t = nvimpager._testable.replace_prefix( {"/a/b/c", "/a/b/d", "/g/e/f"}, "/a/b", "/x/y") assert.same({"/x/y/c", "/x/y/d", "/g/e/f"}, t) end) it("only replaces at the start of the items", function() local t = nvimpager._testable.replace_prefix( {"abc", "cab"}, "ab", "XXX") assert.same({"XXXc", "cab"}, t) end) it("can replace lua pattern chars", function() local actual = nvimpager._testable.replace_prefix( {"a-b-c"}, "a-b", "XXX") assert.same({"XXX-c"}, actual) end) end) describe("tokenize", function() local ansi2highlight = require("nvimpager/ansi2highlight") local function test(input, expected) local result = {} for token, c1, c2, c3 in ansi2highlight.tokenize(input) do table.insert(result, {token, c1, c2, c3}) end assert.same(expected, result) end it("treats empty strings as a single empty token", function() test("", {{""}}) end) it("simple numbers", function() test("42", {{"42"}}) end) it("trailing semicolons as extra empty token", function() test("42;", {{"42"}, {""}}) end) it("leading semicolons as extra empty token", function() test(";42", {{""}, {"42"}}) end) it("splits simple numbers at semicolons", function() test("1;2", {{"1"}, {"2"}}) end) it("recognizes special 8 bit color sequences", function() local input = "38;5;16" test(input, {{"foreground", "16"}}) end) it("recognizes next token after 8 bit color sequences", function() test("38;5;22;42", {{"foreground", "22"}, {"42"}}) end) it("recognizes special 24 bit color sequences", function() local input = "38;2;16;17;42" test(input, {{"foreground", "16", "17", "42"}}) end) it("recognizes next token after 24 bit color sequences", function() test("48;2;101;102;103;99", {{"background", "101", "102", "103"}, {"99"}}) end) it("two semicolon between proper tokens create an empty token", function() test("12;;13", {{"12"}, {""}, {"13"}}) end) -- These create one empty token less than what might be expected but that -- is not a problem because empty token reset all attributes and that is -- an idempotent operation. describe("sequences of semicolons:", function() it("one single semicolon => one empty token", function() test(";", {{""}}) end) it("two semicolons => two empty tokens", function() test(";;", {{""}, {""}}) end) end) end) describe("ansi parser", function() local state local ansi2highlight = require("nvimpager/ansi2highlight") setup(function() state = ansi2highlight.state end) before_each(function() state:clear() end) it("clears all attributes on 0", function() state.foreground = "foo" state.background = "bar" state.strikethrough = true state:parse("0") for _, val in pairs(state) do if type(val) == "string" then assert.equal("", val) elseif type(val) == "boolean" then assert.is_false(val) end end end) describe("can parse special terminal attributes:", function() local attrs = {[1]="bold", [3]="italic", [4]="underline", [7]="reverse", [8]="conceal", [9]="strikethrough"} for num, name in pairs(attrs) do it(""..num.." is "..name, function() state:parse(""..num) assert.is_true(state[name]) end) end end) local colors = {[0]="black", [1]="red", [2]="green", [3]="yellow", [4]="blue", [5]="magenta", [6]="cyan", [7]="lightgray"} describe("can parse foreground colors:", function() for num, name in pairs(colors) do it("3"..num.." is "..name, function() state:parse("3"..num) assert.equal(name, state.foreground) assert.equal(num, state.ctermfg) end) end end) describe("can parse background colors:", function() for num, name in pairs(colors) do it("4"..num.." is "..name, function() state:parse("4"..num) assert.equal(name, state.background) assert.equal(num, state.ctermbg) end) end end) it("can parse color combinations", function() state:parse("33;44") assert.equal("yellow", state.foreground) assert.equal(3, state.ctermfg) assert.equal("blue", state.background) assert.equal(4, state.ctermbg) end) it("parses sequences that partly override themself", function() state:parse("35;3;36") assert.equal("cyan", state.foreground) assert.equal(6, state.ctermfg) assert.is_true(state.italic) end) it("can turn off foreground colors", function() state:parse("37;45;39") assert.equal("", state.foreground) assert.equal("", state.ctermfg) assert.equal("magenta", state.background) assert.equal(5, state.ctermbg) end) it("can turn off background colors", function() state:parse("47;35;49") assert.equal("magenta", state.foreground) assert.equal(5, state.ctermfg) assert.equal("", state.background) assert.equal("", state.ctermbg) end) it("can turn off selected terminal attributes", function() state:parse("3;7;23") assert.is_false(state.italic) assert.is_true(state.reverse) end) describe("parses 24 bit sequences", function() it("parses simple 24 bit foreground colors", function() state:parse("38;2;1;2;3") assert.equal("#010203", state.foreground) end) it("parses 24 bit foreground colors", function() state:parse("38;2;100;200;250") assert.equal("#64c8fa", state.foreground) end) it("parses simple 24 bit background colors", function() state:parse("48;2;20;30;40") assert.equal("#141e28", state.background) end) end) describe("parse 256 colors", function() it("parses pallet terminal colors (fg)", function() state:parse("38;5;4") assert.equal("blue", state.foreground) assert.equal(4, state.ctermfg) end) it("parses pallet terminal colors (bg)", function() state:parse("48;5;5") assert.equal("magenta", state.background) assert.equal(5, state.ctermbg) end) it("parses high colors (fg)", function() state:parse("38;5;10") assert.equal("lightgreen", state.foreground) assert.equal(10, state.ctermfg) end) it("parses high colors (bg)", function() state:parse("48;5;11") assert.equal("lightyellow", state.background) assert.equal(11, state.ctermbg) end) it("parses color cube colors (fg)", function() state:parse("38;5;17") assert.equal("#00005f", state.foreground) assert.equal(17, state.ctermfg) end) it("parses color cube colors (bg)", function() state:parse("48;5;230") assert.equal("#ffffd7", state.background) assert.equal(230, state.ctermbg) end) it("parses grayscale ramp colors (fg)", function() state:parse("38;5;240") assert.equal("#585858", state.foreground) assert.equal(240, state.ctermfg) end) it("parses grayscale ramp colors (bg)", function() state:parse("48;5;250") assert.equal("#bcbcbc", state.background) assert.equal(250, state.ctermbg) end) end) describe("parse8bit", function() it("parses pallet terminal colors (fg)", function() state:parse8bit("foreground", "4") assert.equal("blue", state.foreground) end) it("parses pallet terminal colors (bg)", function() state:parse8bit("background", "5") assert.equal("magenta", state.background) end) it("parses high colors (fg)", function() state:parse8bit("foreground", "10") assert.equal("lightgreen", state.foreground) end) it("parses high colors (bg)", function() state:parse8bit("background", "11") assert.equal("lightyellow", state.background) end) it("parses color cube colors (fg)", function() state:parse8bit("foreground", "17") assert.equal("#00005f", state.foreground) end) it("parses color cube colors (bg)", function() state:parse8bit("background", "230") assert.equal("#ffffd7", state.background) end) it("parses grayscale ramp colors (fg)", function() state:parse8bit("foreground", "240") assert.equal("#585858", state.foreground) end) it("parses grayscale ramp colors (bg)", function() state:parse8bit("background", "250") assert.equal("#bcbcbc", state.background) end) end) end) describe("detect_man_page_helper", function() it("detects lines with each char overwritten by itself", function() local line = "N\bNA\bAM\bME\bE" assert.truthy(nvimpager._testable.detect_man_page_helper(line)) end) it("works with leading whitespace", function() local line = " N\bNA\bAM\bME\bE" assert.truthy(nvimpager._testable.detect_man_page_helper(line)) end) it("works for non capital letters", function() local line = "N\bNa\bam\bme\be" assert.truthy(nvimpager._testable.detect_man_page_helper(line)) end) it("fails if some chars are not overwritten", function() local line = "N\bNA\bAM\bME" assert.falsy(nvimpager._testable.detect_man_page_helper(line)) end) it("detects lines with underscores overwritten by anything", function() local line = "_\bI_\bn_\bi_\bt_\bi_\ba_\bl_\bi_\bz_\ba_\bt_\bi_\bo_\bn" assert.truthy(nvimpager._testable.detect_man_page_helper(line)) end) it("does not accept an empty line", function() assert.falsy(nvimpager._testable.detect_man_page_helper("")) end) it("does not accept a line with only spaces", function() assert.falsy(nvimpager._testable.detect_man_page_helper(" ")) end) end) describe("check_escape_sequences", function() local function filetype_text() return "" end local function filetype_something() return "something" end it("only checks files with filetype 'text'", function() local check_escape_sequences = load_nvimpager( "util", {nvim_buf_get_option = filetype_something} ).check_escape_sequences assert.is_false(check_escape_sequences()) end) it("finds ansi escape sequences", function() local function get_lines() return {"line 1", "escape \27[31mthis is red\27[m"} end local check_escape_sequences = load_nvimpager( "util", {nvim_buf_get_option = filetype_text, nvim_buf_get_lines = get_lines} ).check_escape_sequences assert.is_true(check_escape_sequences()) end) end) describe("detect_process", function() local test_data = { man = "man", pydoc = "pydoc", python27 = "pydoc", ["python3.11"] = "pydoc", ruby = "ri", perldoc = "perldoc", perl = "perldoc", git = "git", } local function load_with(name) local fut = load_nvimpager( "init", {nvim_get_proc = function() return { name = name } end} )._testable.detect_process return fut end for command, expected in pairs(test_data) do it("parses "..command.." as "..expected, function() local detect_process = load_with(command) assert.equal(expected, detect_process(42)) end) end it("returns nil for unknown parents", function() local detect_process = load_with("unknown") assert.is_nil(detect_process(42)) end) end) end)