-- nvim writing config
-- Launch: NVIM_APPNAME=write nvim
-- Plugins: native pack/ (no plugin manager)
vim.g.mapleader = " "
-- Disable netrw (snacks explorer replaces it)
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
-- Plugin settings (must be set before they load)
vim.g.goyo_width = 80
vim.g["goyo_height"] = "90%"
vim.g.limelight_conceal_ctermfg = 240
vim.g.limelight_conceal_guifg = "#585858"
vim.g["pencil#wrapModeDefault"] = "soft"
vim.g["pencil#textwidth"] = 80
-- Appearance
vim.opt.number = false
vim.opt.relativenumber = false
vim.opt.signcolumn = "no"
vim.opt.showmode = false
vim.opt.ruler = false
vim.opt.laststatus = 0
vim.opt.cmdheight = 1
vim.opt.fillchars = { eob = " " }
vim.opt.termguicolors = true
-- Kill all syntax highlighting
vim.cmd("syntax off")
vim.cmd("filetype plugin on")
vim.cmd("filetype indent off")
-- Colorscheme
vim.opt.background = "dark"
vim.cmd("colorscheme writing")
-- Writing defaults
vim.opt.wrap = true
vim.opt.linebreak = true
vim.opt.breakindent = true
vim.opt.textwidth = 0
vim.opt.wrapmargin = 0
vim.opt.spell = true
vim.opt.spelllang = "en_us"
vim.opt.cursorline = true
vim.opt.scrolloff = 8
vim.opt.sidescrolloff = 8
-- Tabs
vim.opt.expandtab = true
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
-- Search
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.hlsearch = false
vim.opt.incsearch = true
-- Misc
vim.opt.swapfile = false
vim.opt.backup = false
vim.opt.undofile = true
vim.opt.undodir = vim.fn.stdpath("data") .. "/undo"
vim.opt.clipboard = "unnamedplus"
vim.opt.mouse = "a"
-- snacks.nvim (explorer + project picker)
require("snacks").setup({
explorer = { enabled = true },
picker = {
enabled = true,
sources = {
projects = {
dev = { "~/.corpus/content" },
recent = false,
},
},
},
})
-- Word count in statusline (shown in cmdline area)
vim.api.nvim_create_autocmd({ "BufEnter", "CursorHold", "InsertLeave" }, {
pattern = "*",
callback = function()
local wc = vim.fn.wordcount()
local words = wc.visual_words or wc.words or 0
vim.opt.rulerformat = string.format("%%=%d words", words)
vim.opt.ruler = true
end,
})
-- Goyo enter/leave hooks
vim.api.nvim_create_autocmd("User", {
pattern = "GoyoEnter",
callback = function()
vim.cmd("colorscheme writing")
for _, win in ipairs(vim.api.nvim_list_wins()) do
vim.api.nvim_win_set_option(win, "winhighlight", "Normal:Normal,NormalNC:Normal,EndOfBuffer:EndOfBuffer")
end
vim.cmd("Limelight")
vim.opt.showmode = false
vim.opt.showcmd = false
end,
})
vim.api.nvim_create_autocmd("User", {
pattern = "GoyoLeave",
callback = function()
vim.cmd("Limelight!")
vim.opt.showmode = false
vim.opt.showcmd = false
end,
})
-- Goyo/explorer coordination
local goyo_was_on = false
local function goyo_is_active()
return vim.fn.exists("#goyo") == 1
end
local function goyo_suspend()
if goyo_is_active() then
goyo_was_on = true
vim.cmd("Goyo!")
end
end
local function goyo_restore()
if goyo_was_on then
goyo_was_on = false
vim.defer_fn(function()
vim.cmd("Goyo")
end, 50)
end
end
-- Keymaps
local map = vim.keymap.set
-- Explorer (suspend/restore Goyo around it)
map("n", "<leader>e", function()
local explorers = Snacks.picker.get({ source = "explorer" })
if #explorers > 0 then
explorers[1]:close()
goyo_restore()
else
goyo_suspend()
Snacks.explorer.open()
end
end, { desc = "Toggle file explorer" })
-- Project switcher (content directories + extras)
map("n", "<leader>fp", function()
local content_root = vim.fn.expand("~/.corpus/content")
local dirs = {}
-- Grab subdirs from ~/.corpus/content
local handle = vim.loop.fs_scandir(content_root)
if handle then
while true do
local name, typ = vim.loop.fs_scandir_next(handle)
if not name then break end
if typ == "directory" and name:sub(1, 1) ~= "." then
table.insert(dirs, { name = name, path = content_root .. "/" .. name, text = name })
end
end
end
-- Extra content dirs outside .corpus/content
table.insert(dirs, { name = "slipbox", path = vim.fn.expand("~/dev/slipbox"), text = "slipbox" })
table.sort(dirs, function(a, b) return a.name < b.name end)
Snacks.picker({
title = "Content",
items = dirs,
format = function(item) return { { item.name } } end,
confirm = function(picker, item)
picker:close()
if item then
vim.cmd("cd " .. vim.fn.fnameescape(item.path))
Snacks.picker.files({ cwd = item.path })
end
end,
})
end, { desc = "Switch content project" })
-- File search
map("n", "<leader>sf", function() Snacks.picker.files() end, { desc = "Search files" })
map("n", "<leader>fg", function() Snacks.picker.grep() end, { desc = "Grep" })
map("n", "<leader>ff", function() Snacks.picker.smart() end, { desc = "Smart find" })
map("n", "<leader>s.", function() Snacks.picker.recent() end, { desc = "Recent files" })
map("n", "<leader><leader>", function() Snacks.picker.buffers() end, { desc = "Buffers" })
map("n", "<leader>/", function() Snacks.picker.lines() end, { desc = "Search in buffer" })
-- Writing tools
map("n", "<leader>g", "<cmd>Goyo<cr>", { desc = "Toggle Goyo" })
map("n", "<leader>l", "<cmd>Limelight!!<cr>", { desc = "Toggle Limelight" })
map("n", "<leader>s", "<cmd>set spell!<cr>", { desc = "Toggle spell check" })
map("n", "<leader>w", "<cmd>w<cr>", { desc = "Save" })
map("n", "<leader>q", "<cmd>q<cr>", { desc = "Quit" })
-- Navigate wrapped lines naturally
map("n", "j", "gj")
map("n", "k", "gk")
-- Pencil + Goyo auto-init for prose files
vim.api.nvim_create_autocmd("FileType", {
pattern = { "markdown", "text", "tex" },
callback = function()
vim.cmd("PencilSoft")
end,
})
-- Autosave on focus loss, idle, or leaving insert mode
vim.api.nvim_create_autocmd({ "FocusLost", "CursorHold", "InsertLeave" }, {
pattern = "*",
callback = function()
if vim.bo.modified and vim.bo.buftype == "" and vim.fn.expand("%") ~= "" then
vim.cmd("silent! write")
end
end,
})
-- Goyo on by default
vim.api.nvim_create_autocmd("VimEnter", {
callback = function()
vim.cmd("Goyo")
end,
})