MuPDF-backed PDF viewer written in Go with Lua configuration.
gopdf is built for keyboard-driven PDF reading with scriptable behavior. It supports Vim-like navigation, configurable keybindings, commands, search, outlines, session restore, custom colors, and Lua callbacks.
Open a PDF:
gopdf file.pdfUseful defaults:
| Key | What it does |
|---|---|
F1 |
Show and edit keybindings |
j / k |
Scroll down / up |
J / K |
Next / previous page |
/ / ? |
Search forward / backward |
n / N |
Next / previous search match |
o |
Open the PDF outline |
: |
Open the command prompt |
q |
Quit |
If no file is provided, gopdf reopens the most recently viewed file from its session database.
| I want to... | See |
|---|---|
| Install gopdf | Installation |
| Open a PDF or start on a specific page | Usage |
| Change viewer defaults | Configuration |
| Change keybindings | Keybindings |
| See every default shortcut | Default Keybindings |
| Search, jump pages, or open files by command | Commands |
| Customize the status bar | Status Bar |
| Write Lua callbacks | Lua API |
| Build a custom recent-files menu | Custom UI |
macOS
Install from the latest release, amd64 for Intel Macs or arm64 for Apple silicon Macs.
Or install using Homebrew:
brew install Aethar01/homebrew-gopdf/gopdfWindows
Install from the latest release. Use the installer for Start Menu shortcuts and optional PDF file association, or download the zip, unzip it, and run the exe directly.
From Source
Requirements:
- Go 1.25+
- MuPDF 1.25.6+
- SDL3
- pkg-config/pkgconf
- C compiler that works with CGO
go buildWindows with MSYS2 UCRT64:
pacman -S --needed mingw-w64-ucrt-x86_64-go mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-pkgconf mingw-w64-ucrt-x86_64-sdl3 mingw-w64-ucrt-x86_64-mupdf
go build -o gopdf.exegopdf /path/to/file.pdf # open file
gopdf --page 20 file.pdf # start at page 20
gopdf --config custom.lua file.pdf
gopdf -v # print version
gopdf -V # enable verbose logsPress F1 inside gopdf to see default keybindings and edit them interactively.
Start from config.example.lua when you want every supported option, or create a small config with only the settings you want to change.
gopdf.options.fit_mode = "width"
gopdf.options.dual_page = true
gopdf.options.status_bar_visible = true
gopdf.bind("H", gopdf.prev_page)
gopdf.bind("L", gopdf.next_page)Config is Lua. It is loaded once at startup and again when reload_config or :reload-config is used.
Generated keybind edits are loaded from autogen.lua before config.lua, so explicit user config always wins. The first existing user config file for the current OS is used:
| OS | Path |
|---|---|
| Any | --config <path> argument |
| Linux | ~/.config/gopdf/config.lua |
| Linux | $XDG_CONFIG_HOME/gopdf/config.lua |
| Linux | Each $XDG_CONFIG_DIRS/gopdf/config.lua |
| Linux | /etc/xdg/gopdf/config.lua |
| macOS | ~/Library/Application Support/gopdf/config.lua |
| Windows | %APPDATA%\gopdf\config.lua |
Generated keybind edits are written to autogen.lua next to an explicit --config file, or to the normal per-user config directory when no explicit config is used.
Session data is saved in session.sqlite under the matching per-user app data directory: $XDG_DATA_HOME/gopdf or ~/.local/share/gopdf on Linux, ~/Library/Application Support/gopdf on macOS, and %APPDATA%\gopdf on Windows.
gopdf.options.status_bar_visible = true
gopdf.options.mouse_text_select = true
gopdf.options.natural_scroll = false
gopdf.options.session_database = true -- per-document view state database
gopdf.options.anti_aliasing = 8 -- 0 disables AA; MuPDF clamps values to 0-8
gopdf.options.alt_colors = false
gopdf.options.render_oversample = 1 -- >1 supersamples, <1 undersamples
gopdf.options.render_mode = "continuous" -- "continuous" or "single"
gopdf.options.dual_page = false
gopdf.options.first_page_offset = true
gopdf.options.fit_mode = "page" -- "page", "width", or "manual"
gopdf.options.page_gap = 0 -- sets vertical gap too
gopdf.options.spread_gap = 0 -- sets horizontal gap too
gopdf.options.page_gap_vertical = 0
gopdf.options.page_gap_horizontal = 0
gopdf.options.scroll_step = 64
gopdf.options.status_bar_height = 28
gopdf.options.status_bar_padding = 8
gopdf.options.ui_font_size = 14
gopdf.options.ui_font_path = "" -- empty = default font
gopdf.options.sequence_timeout_ms = 700
gopdf.options.outline_initial_depth = 1
gopdf.options.outline_width_percent = 70
gopdf.options.outline_height_percent = 80
gopdf.options.completion_max_items = 10
gopdf.options.recent_files_max = 10
-- Colors are { red, green, blue }, 0-255.
gopdf.options.background = { 220, 220, 220 }
gopdf.options.page_background = { 255, 255, 255 }
gopdf.options.foreground = { 20, 20, 20 }
gopdf.options.status_bar_color = { 220, 220, 220 }
gopdf.options.alt_background = { 20, 20, 20 }
gopdf.options.alt_page_background = { 17, 17, 17 }
gopdf.options.alt_foreground = { 255, 255, 255 }
gopdf.options.alt_status_bar_color = { 20, 20, 20 }
gopdf.options.highlight_foreground = { 0, 0, 0 }
gopdf.options.highlight_background = { 255, 224, 102 }gopdf.status_bar.height = 28
gopdf.status_bar.left = "{message}"
gopdf.status_bar.right = "{page}/{total} {mode} fit={fit} rot={rot} {zoom}"Available placeholders:
| Placeholder | Description |
|---|---|
{message} |
Current status message or input prompt |
{page} |
Current page, or current spread range in dual-page mode |
{total} |
Total pages |
{mode} |
Render mode: continuous or single |
{fit} |
Fit mode: page, width, or manual |
{rot} |
Rotation in degrees |
{zoom} |
Zoom percentage |
{dual} |
dual or single |
{cover} |
cover or flat |
{search} |
Search match counter |
{document} |
Document filename |
{input} |
Current input text |
{prompt} |
Search prompt, / or ? |
Use $$ for a literal $ in status bar templates.
Example:
gopdf.status_bar.height = 32
gopdf.options.ui_font_size = 13
gopdf.options.ui_font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
gopdf.status_bar.left = " {document} {message}"
gopdf.status_bar.right = " {page}/{total} | {mode} | {search} | {zoom} "Use gopdf.bind(key, action) and gopdf.bind_mouse(event, action). The global aliases bind, unbind, bind_mouse, and unbind_mouse are also available in config files.
gopdf.bind("j", gopdf.scroll_down)
gopdf.bind("J", gopdf.next_page)
gopdf.bind("gg", gopdf.first_page)
gopdf.bind("<C-o>", gopdf.jump_backward)
gopdf.bind("<Space>", gopdf.pan) -- hold Space and move mouse to pan
gopdf.bind_mouse("wheel_down", gopdf.scroll_down)
gopdf.bind_mouse("<C-wheel_up>", gopdf.zoom_in)
gopdf.bind_mouse("middle_down", gopdf.pan)Custom callbacks run after the viewer is active:
gopdf.bind("H", function()
gopdf.goto_page(1)
gopdf.message("first page")
end)
gopdf.bind("<C-l>", function()
gopdf.command(":reload-config")
end)Unbind keys or mouse events:
gopdf.unbind("j")
gopdf.unbind_mouse("wheel_down")| Key | Action |
|---|---|
j, <Down> / k, <Up> |
Scroll down / up |
h, <Left> / l, <Right> |
Scroll left / right |
J / K |
Next / previous page |
Space / <PgDn> / <PgUp> |
Next page / next page / previous page |
gg / G |
First / last page |
<C-g> |
Page prompt |
Ng |
Jump to page N |
Nj, Nk, Nh, Nl |
Repeat scroll action N times |
NJ / NK |
Jump N pages/spreads forward / backward |
d |
Toggle dual-page mode |
m |
Toggle continuous/single render mode |
<C-r> |
Toggle alternate colors |
co |
Toggle first-page offset |
<C-n> |
Toggle status bar |
f |
Toggle fullscreen |
o |
Open/close outline menu |
<F1> |
Open keybinds menu |
<C-S-o> |
Open PDF file picker |
<C-S-r> |
Reload config |
<CR> |
Confirm input or selected outline item |
+ / = / - / 0 |
Zoom in / zoom in / zoom out / reset zoom |
w / z |
Fit width / fit page |
r / R |
Rotate clockwise / counter-clockwise |
/ / ? |
Search forward / backward |
n / N |
Next / previous search match |
: |
Command prompt |
<Tab> / <S-Tab> |
Show or cycle command completion / previous completion |
<Esc> |
Close active UI, clear search, or clear pending keys/count |
<C-i> / <C-o> |
Jump forward / backward in jump history |
"{letter} / '{letter} |
Set / jump to persistent mark; requires session_database |
q |
Quit |
Default mouse bindings:
| Mouse | Action |
|---|---|
wheel_up / wheel_down |
Scroll up / down |
wheel_left / wheel_right |
Scroll left / right |
<C-wheel_up> / <C-wheel_down> |
Zoom in / out |
middle_down |
Pan while held |
| Left-drag | Text selection when mouse_text_select is true |
Key names are case-sensitive for printable letters and normalized for angle-bracket names. SDL-named keys can be bound using their normalized angle-bracket names, for example <F13>, <Home>, <AudioMute>, or keypad key names reported by SDL.
| Form | Examples |
|---|---|
| Printable letters | a through z, A through Z |
| Printable digits | 0 through 9 |
| Printable punctuation | /, ?, ;, :, =, +, - |
| Space | " " or <Space> |
| Special keys | <CR>, <Enter>, <Return>, <Esc>, <BS>, <PgDn>, <PgUp>, <Tab> |
| Function keys | <F1> |
| Arrow keys | <Up>, <Down>, <Left>, <Right> |
| Ctrl keys | <C-a>, <C-S-a>, <C-1>, <C-S-1>, <C-Space>, <C-Tab>, <C-Enter>, <C-Esc>, <C-BS>, <C-PgDn>, <C-PgUp> |
| Shift special keys | <S-CR>, <S-Esc>, <S-BS>, <S-PgDn>, <S-PgUp>, <S-Tab> |
| Sequences | gg, tb, co, <C-x>g |
Multiple keys can map to the same action. The <F1> keybinds menu adds an additional key for the selected action and writes generated edits to autogen.lua. Press <Del> or <BS> on a selected keybind row to delete it.
Supported mouse events:
| Event | Description |
|---|---|
wheel_up, wheel_down |
Vertical wheel scroll |
wheel_left, wheel_right |
Horizontal wheel scroll |
<C-wheel_up>, <C-wheel_down> |
Ctrl-wheel events |
left_down, left_up |
Left mouse button |
middle_down, middle_up |
Middle mouse button |
right_down, right_up |
Right mouse button |
x1_down, x1_up |
Extra mouse button 1 |
x2_down, x2_up |
Extra mouse button 2 |
Open the command prompt with :.
| Command | Description |
|---|---|
:page N, :p N, :N |
Jump to page N |
:search <text> |
Search document |
:search re:<pattern> |
Search document with a Go regular expression |
:fit width / :fit page / :fit manual |
Set fit mode |
:mode continuous / :mode single |
Set render mode |
:colors normal / :colors alt |
Set color mode |
:set dual_page! |
Toggle dual-page mode |
:set alt_colors! |
Toggle alternate colors |
:set render_mode! |
Toggle render mode |
:set first_page_offset! |
Toggle first-page offset |
:set status_bar! |
Toggle status bar |
:open <filename> |
Open another PDF, relative to the current document directory |
:open_file_picker |
Open the PDF file picker |
:reload-config |
Reload config file |
:keybinds |
Toggle keybinds menu |
:lua <code> |
Execute Lua code inline |
:help |
Open command help window |
:quit, :q |
Exit |
| Function | Description |
|---|---|
gopdf.page() |
Current page number, 1-indexed |
gopdf.page_count() |
Total pages |
gopdf.goto_page(n) |
Jump to page n |
gopdf.mode() |
Current UI mode |
gopdf.current_count() |
Pending numeric count |
gopdf.pending_keys() |
Pending key sequence table |
gopdf.clear_pending_keys() |
Clear pending keys and count |
| Function | Description |
|---|---|
gopdf.fit_mode() / gopdf.set_fit_mode("width"|"page"|"manual") |
Get/set fit mode |
gopdf.render_mode() / gopdf.set_render_mode("continuous"|"single") |
Get/set render mode |
gopdf.zoom() / gopdf.set_zoom(n) |
Get/set zoom scale |
gopdf.rotation() / gopdf.set_rotation(deg) |
Get/set rotation |
gopdf.fullscreen() / gopdf.set_fullscreen(bool) |
Get/set fullscreen |
gopdf.status_bar_visible() / gopdf.set_status_bar_visible(bool) |
Get/set status bar visibility |
| Function | Description |
|---|---|
gopdf.search(query[, backward]) |
Search document |
gopdf.search_query() |
Current search term |
gopdf.search_match_index() |
Current match, 1-indexed, or nil |
gopdf.search_match_count() |
Total matches |
Prefix a query with re: to search with a Go regular expression, for example :search re:foo.*bar.
| Function | Description |
|---|---|
gopdf.message() / gopdf.message("text") |
Get/set status message |
gopdf.command(":fit width") |
Execute command |
gopdf.open(path) |
Open another PDF |
gopdf.recent_files([limit]) |
Return recent files from the session database |
gopdf.pick_file([callback]) |
Open native file picker; optional callback receives path |
gopdf.bind(key, action) / gopdf.unbind(key) |
Bind/unbind keyboard action |
gopdf.bind_mouse(event, action) / gopdf.unbind_mouse(event) |
Bind/unbind mouse action |
Lua callbacks can open a simple modal list overlay. The overlay uses the same navigation actions as the outline menu: scroll_down, scroll_up, confirm, and close.
| Function | Description |
|---|---|
gopdf.ui.show(spec) |
Show a modal list UI |
gopdf.ui.close() |
Close the active Lua UI without running on_close |
gopdf.ui.visible() |
Return whether a Lua UI is visible |
gopdf.ui.set_rows(rows) |
Replace the current UI rows |
gopdf.ui.set_selected(index) |
Set the selected row, 1-indexed |
show accepts this table:
| Field | Description |
|---|---|
title |
Optional title shown in the header |
rows |
Array of strings to show |
selected |
Optional initial selected row, 1-indexed |
on_select(index, value) |
Optional callback run when a row is confirmed or clicked |
on_close() |
Optional callback run when the UI is closed by the viewer |
Example recent-files menu bound to gr. It shows the most recent files from the session database and opens the selected row:
local function show_recent_files()
local rows = gopdf.recent_files(20)
if #rows == 0 then
gopdf.message("no recent files")
return
end
gopdf.ui.show({
title = "Recent Files",
rows = rows,
on_select = function(_, path)
gopdf.ui.close()
gopdf.open(path)
end,
})
end
gopdf.bind("gr", show_recent_files)Example file browser bound to fo. It starts in the user's home directory, shows directories first, lets you open .., and opens selected PDFs:
local function shell_quote(s)
return "'" .. s:gsub("'", "'\\''") .. "'"
end
local function list_dir(dir)
local rows = {}
local command = "find " .. shell_quote(dir) .. " -maxdepth 1 -mindepth 1 " ..
'\\( -type d -printf "d:%f\\n" -o -type f -iname "*.pdf" -printf "f:%f\\n" \\)'
local handle = io.popen(command)
if not handle then
return rows
end
for line in handle:lines() do
local kind, name = line:match("^([df]):(.*)$")
if kind == "d" then
rows[#rows + 1] = name .. "/"
elseif kind == "f" then
rows[#rows + 1] = name
end
end
handle:close()
table.sort(rows)
table.insert(rows, 1, "../")
return rows
end
local function join_path(dir, name)
if dir == "/" then
return "/" .. name
end
return dir .. "/" .. name
end
local function parent_dir(dir)
if dir == "/" then
return "/"
end
return dir:match("^(.*)/[^/]+/?$") or "/"
end
local function show_file_browser(dir)
gopdf.ui.show({
title = "Open PDF: " .. dir,
rows = list_dir(dir),
on_select = function(_, name)
if name == "../" then
show_file_browser(parent_dir(dir))
return
end
if name:sub(-1) == "/" then
show_file_browser(join_path(dir, name:sub(1, -2)))
return
end
gopdf.ui.close()
gopdf.open(dir .. "/" .. name)
end,
})
end
gopdf.bind("fo", function()
show_file_browser(os.getenv("HOME") or ".")
end)Actions can be bound directly, called from callbacks, or executed with gopdf.command where a matching command exists.
gopdf.next_page() gopdf.prev_page()
gopdf.scroll_down() gopdf.scroll_up()
gopdf.scroll_left() gopdf.scroll_right()
gopdf.next_spread() gopdf.prev_spread()
gopdf.first_page() gopdf.last_page()
gopdf.command_mode() gopdf.goto_page_prompt()
gopdf.search_prompt() gopdf.search_prompt_backward()
gopdf.search_next() gopdf.search_prev()
gopdf.clear_search() gopdf.close()
gopdf.toggle_dual_page() gopdf.toggle_render_mode()
gopdf.toggle_alt_colors()
gopdf.toggle_first_page_offset()
gopdf.toggle_status_bar()
gopdf.toggle_fullscreen()
gopdf.outline() gopdf.confirm()
gopdf.zoom_in() gopdf.zoom_out()
gopdf.reset_zoom()
gopdf.fit_width() gopdf.fit_page()
gopdf.rotate_cw() gopdf.rotate_ccw()
gopdf.jump_forward() gopdf.jump_backward()
gopdf.pan() gopdf.reload_config()
gopdf.show_completion() gopdf.next_completion()
gopdf.prev_completion()
gopdf.open_file_picker() gopdf.keybinds()
gopdf.quit()gopdf.cache.entries() -- rendered pages in cache
gopdf.cache.pending() -- pending renders
gopdf.cache.limit() -- current cache entry limit
gopdf.cache.set_limit(n) -- set cache entry limit
gopdf.cache.clear() -- clear rendered page cacheAvailable during config load:
| Property | Description |
|---|---|
gopdf.document.name |
Filename |
gopdf.document.path |
Full path |
gopdf.document.extension |
File extension |
gopdf.document.page_count |
Total pages, if readable |
gopdf.document.size_bytes |
File size, if the file exists |
gopdf.document.exists |
Whether the file exists |
Example:
if gopdf.document.page_count and gopdf.document.page_count > 200 then
gopdf.options.dual_page = true
endgopdf is licensed under AGPL.
Links against MuPDF, which is licensed under AGPL unless you have a separate commercial license.