~kris/dots

srice

3bdfd3e0c26b75a939850355c30bee6fa9cc9abd — Kris Yotam 3 months ago e5cf200
sync: lock down plan9 theme, wallpapers, nvim configs

- setbg: add -w (wallpaper-only), -d (default plan9 theme), terminal color reset
- wallpapers: plan9.png, plan9theme.png, plan9-grey.png, d1e2f1-horizontal
- nvim: add code/ and write/ NVIM_APPNAME configs (vacme + writing themes)
- bin: add write, code scripts (replace shell functions)
- xprofile: caps:none, dpms off, setfan, gnome-keyring, wireplumber, conky, protonmail
- wal: add colors-dwm-xresources template, remove stale btop.theme
- conky: add clock.conf, clock.lua, local.conf
A .config/code/colors/vacme.lua => .config/code/colors/vacme.lua +345 -0
@@ 0,0 1,345 @@
vim.cmd('hi clear')

if vim.fn.exists("syntax_on") then
	vim.cmd('syntax reset')
end

vim.g.colors_name = 'vacme'
vim.o.background = 'light'

local vcolors = {
	-- whites
	w1 = {term='15',hex='#FFFFEC'},
	w2 = {term='11',hex='#EEEEA7'},
	w3 = {term='7',hex='#999957'},
	w4 = {term='0',hex='#424242'},

	-- reds
	r1 = {term='9',hex='#F2ACAA'},
	r2 = {term='1',hex='#B85C57'},

	-- greens
	g1 = {term='193',hex='#EFFEEC'},
	g2 = {term='10',hex='#98CE8F'},
	g3 = {term='2',hex='#57864E'},

	-- yellows
	y1 = {term='187',hex='#EAEBDB'},
	y2 = {term='8',hex='#B7B19C'},
	y3 = {term='3',hex='#8F7634'},

	-- blues
	b1 = {term='195',hex='#E2F1F8'},
	b2 = {term='12',hex='#A6DCF8'},
	b3 = {term='4',hex='#2A8DC5'},

	-- magentas
	m1 = {term='13',hex='#D0D0F7'},
	m2 = {term='5',hex='#8888C7'},

	-- cyans
	c1 = {term='194',hex='#EAFFFF'},
	c2 = {term='14',hex='#B0ECED'},
	c3 = {term='6',hex='#6AA7A8'},

	-- misc (accent)
	a1 = {term='195',hex='#030093'}
}
local vstyles = {
	normal = {fg=vcolors.w4,bg=vcolors.w1},
	ghostly = {fg=vcolors.y3,bg=vcolors.w1},
	hilited = {fg=vcolors.w4,bg=vcolors.w2},
	justbold = {style='bold'},
	justuline = {style='underline'},
	justit = {style='italic'}
}

function syntax(group, rule)
	vim.cmd(string.format("hi! %s term=NONE cterm=NONE ctermfg=NONE ctermbg=NONE gui=NONE guifg=NONE guibg=NONE", group))
	local fg = ""
	if rule.fg then fg = string.format(' guifg=%s ctermfg=%s', rule.fg['hex'], rule.fg['term']) end
	local bg = ""
	if rule.bg then bg = string.format(' guibg=%s ctermbg=%s', rule.bg['hex'], rule.bg['term']) end
	local style = ""
	if rule.style then style = string.format(' gui=%s cterm=%s', rule.style, rule.style) end
	vim.cmd(string.format( 'hi! %s%s%s%s', group, fg, bg, style))
end
function link(group, model)
	vim.cmd(string.format("hi! link %s %s", group, model))
end

-- nvim UI elements
syntax('Normal', vstyles.normal)
syntax('Folded', vstyles.ghostly)
syntax('FoldColumn', vstyles.ghostly)
syntax('Terminal', vstyles.normal)
syntax('ToolbarButton', vstyles.normal)
syntax('ToolbarLine', vstyles.normal)
syntax('CursorLine', {bg=vcolors.g1})
syntax('LineNr', {fg=vcolors.w3,bg=vcolors.y1})
link('LineNrAbove', 'LineNr')
link('LineNrBelow', 'LineNr')
syntax('FloatBorder', {fg=vcolors.g1,bg=vcolors.g3})
syntax('NormalFloat', {fg=vcolors.g3,bg=vcolors.g1})
syntax('DiffAdd', {fg=vcolors.w4, bg=vcolors.g2})
syntax('DiffChange', {fg=vcolors.w4, bg=vcolors.c2})
syntax('DiffDelete', {fg=vcolors.w4, bg=vcolors.r1})
syntax('DiffText', {fg=vcolors.w4, bg=vcolors.g2})
syntax('StatusLine', {fg=vcolors.w4, bg=vcolors.c1, style='bold,underline'})
syntax('StatusLineNC', {fg=vcolors.w4, bg=vcolors.c1})
link('TabLine', 'StatusLineNC')
link('TabLineFill', 'StatusLineNC')
link('WinSeparator', 'StatusLineNC')
syntax('TabLineSel', {fg=vcolors.w1, bg=vcolors.m2})
syntax('CursorLineNr', {fg=vcolors.w1,bg=vcolors.m2})
syntax('NonText', {fg=vcolors.w3})
syntax('SpecialKey', vstyles.justbold)
syntax('SpellBad', {fg=vcolors.r2,style='underline'})
syntax('SpellCap', vstyles.justuline)
syntax('SpellLocal', vstyles.justuline)
syntax('SpellRare', vstyles.justuline)
syntax('Title', vstyles.justbold)
syntax('ColorColumn', vstyles.hilited)
syntax('Conceal', vstyles.ghostly)
syntax('CursorColumn', {bg=vcolors.g1})
syntax('Directory', vstyles.justbold)
syntax('EndOfBuffer', vstyles.ghostly)
syntax('ErrorMsg', vstyles.justbold)
syntax('IncSearch', {fg=vcolors.w1,bg=vcolors.m2})
syntax('MatchParen', {bg=vcolors.w2,style='bold'})
syntax('ModeMsg', vstyles.justbold)
syntax('MoreMsg', vstyles.justbold)
syntax('Pmenu', {fg=vcolors.g3,bg=vcolors.g1})
syntax('PmenuSbar', {fg=vcolors.g1,bg=vcolors.g3})
syntax('PmenuSel', {fg=vcolors.w4,bg=vcolors.g2,style='underline'})
syntax('PmenuKindSel', {fg=vcolors.w4,bg=vcolors.g2})
link('PmenuExtraSel', 'PmenuKindSel')
syntax('PmenuThumb', {fg=vcolors.g1,bg=vcolors.w4})
syntax('Question', vstyles.justbold)
syntax('Search', vstyles.hilited)
link('SignColumn', 'LineNr')
syntax('Visual', vstyles.hilited)
syntax('VisualNOS', vstyles.hilited)
syntax('WarningMsg', vstyles.justbold)
syntax('WildMenu', {fg=vcolors.w1,bg=vcolors.m2})
syntax('QuickFixLine', vstyles.justbold)

-- language syntax (acme philosophy: almost no color, just bold comments)
syntax('Comment', vstyles.justbold)

syntax('Constant', vstyles.normal)
syntax('String', vstyles.normal)
syntax('Character', vstyles.normal)
syntax('Number', vstyles.normal)
syntax('Boolean', vstyles.normal)
syntax('Float', vstyles.normal)

syntax('Identifier', vstyles.normal)
syntax('Function', vstyles.normal)

syntax('Statement', vstyles.normal)
syntax('Conditional', vstyles.normal)
syntax('Repeat', vstyles.normal)
syntax('Label', vstyles.normal)
syntax('Operator', vstyles.normal)
syntax('Keyword', vstyles.normal)
syntax('Exception', vstyles.normal)

syntax('PreProc', vstyles.normal)
syntax('Include', vstyles.normal)
syntax('Define', vstyles.normal)
syntax('Macro', vstyles.normal)
syntax('PreCondit', vstyles.normal)

syntax('Type', vstyles.normal)
syntax('StorageClass', vstyles.normal)
syntax('Structure', vstyles.normal)
syntax('Typedef', vstyles.normal)

syntax('Special', vstyles.normal)
syntax('SpecialChar', vstyles.normal)
syntax('Tag', vstyles.justuline)
syntax('Delimiter', vstyles.normal)
syntax('SpecialComment', vstyles.normal)
syntax('Debug', vstyles.normal)

syntax('Underlined', vstyles.justuline)

syntax('Ignore', vstyles.justbold)

syntax('Error', vstyles.normal)

syntax('Todo', vstyles.hilited)

-- TreeSitter (legacy names)
link('TSAnnotation', 'Normal')
link('TSBoolean', 'Normal')
link('TSCharacter', 'Normal')
link('TSComment', 'Comment')
link('TSConditional', 'Normal')
link('TSConstant', 'Normal')
link('TSConstBuiltin', 'Normal')
link('TSConstMacro', 'Normal')
link('TSError', 'Error')
link('TSException', 'Normal')
link('TSField', 'Normal')
link('TSFloat', 'Normal')
link('TSFunction', 'Normal')
link('TSFuncBuiltin', 'Normal')
link('TSFuncMacro', 'Normal')
link('TSInclude', 'Normal')
link('TSKeyword', 'Normal')
link('TSLabel', 'Normal')
link('TSMethod', 'Normal')
link('TSNamespace', 'Normal')
link('TSNumber', 'Normal')
link('TSOperator', 'Normal')
link('TSParameterReference', 'Normal')
link('TSProperty', 'Normal')
link('TSPunctDelimiter', 'Normal')
link('TSPunctBracket', 'Normal')
link('TSPunctSpecial', 'Normal')
link('TSRepeat', 'Normal')
link('TSString', 'Normal')
link('TSStringRegex', 'Normal')
link('TSStringEscape', 'Normal')
link('TSStrong', 'Normal')
link('TSConstructor', 'Normal')
link('TSKeywordFunction', 'Normal')
link('TSLiteral', 'Normal')
link('TSParameter', 'Normal')
link('TSVariable', 'Normal')
link('TSVariableBuiltin', 'Normal')
link('TSTag', 'Normal')
link('TSTagDelimiter', 'Normal')
link('TSTitle', 'Normal')
link('TSType', 'Normal')
link('TSTypeBuiltin', 'Normal')
link('TSEmphasis', 'Normal')

-- Neovim >= 0.8 treesitter captures
link('@comment', 'Comment')
link('@error', 'Error')
link('@none', 'Normal')
link('@preproc', 'Normal')
link('@keyword.directive', 'Normal')
link('@define', 'Normal')
link('@keyword.directive.define', 'Normal')
link('@operator', 'Normal')
link('@punctuation.delimiter', 'Normal')
link('@markup.raw.delimiter', 'Normal')
link('@punctuation.bracket', 'Normal')
link('@punctuation.special', 'Normal')
link('@markup.list', 'Normal')
link('@string', 'Normal')
link('@string.regex', 'Normal')
link('@string.regexp', 'Normal')
link('@string.escape', 'Normal')
link('@string.special', 'Normal')
link('@markup.link.label', 'Normal')
link('@character', 'Normal')
link('@character.special', 'Normal')
link('@boolean', 'Normal')
link('@number', 'Normal')
link('@float', 'Normal')
link('@number.float', 'Normal')
link('@function', 'Normal')
link('@function.call', 'Normal')
link('@function.builtin', 'Normal')
link('@function.macro', 'Normal')
link('@method', 'Normal')
link('@method.call', 'Normal')
link('@function.method', 'Normal')
link('@function.method.call', 'Normal')
link('@constructor', 'Normal')
link('@parameter', 'Normal')
link('@variable.parameter', 'Normal')
link('@keyword', 'Normal')
link('@keyword.function', 'Normal')
link('@keyword.operator', 'Normal')
link('@keyword.return', 'Normal')
link('@conditional', 'Normal')
link('@repeat', 'Normal')
link('@debug', 'Normal')
link('@keyword.conditional', 'Normal')
link('@keyword.repeat', 'Normal')
link('@keyword.debug', 'Normal')
link('@label', 'Normal')
link('@include', 'Normal')
link('@exception', 'Normal')
link('@keyword.import', 'Normal')
link('@keyword.exception', 'Normal')
link('@type', 'Normal')
link('@type.builtin', 'Normal')
link('@type.qualifier', 'Normal')
link('@type.definition', 'Normal')
link('@storageclass', 'Normal')
link('@keyword.storage', 'Normal')
link('@attribute', 'Normal')
link('@field', 'Normal')
link('@variable.member', 'Normal')
link('@property', 'Normal')
link('@variable', 'Normal')
link('@variable.builtin', 'Normal')
link('@constant', 'Normal')
link('@constant.builtin', 'Normal')
link('@constant.macro', 'Normal')
link('@namespace', 'Normal')
link('@module', 'Normal')
link('@symbol', 'Normal')
link('@string.special.symbol', 'Normal')
link('@text', 'Normal')
link('@text.strong', 'Normal')
link('@text.emphasis', 'Normal')
link('@text.underline', 'Underlined')
link('@text.strike', 'Normal')
link('@markup.strong', 'Normal')
link('@markup.emphasis', 'Normal')
link('@markup.italic', 'Normal')
link('@markup.underline', 'Underlined')
link('@markup.strike', 'Normal')
link('@markup.strikethrough', 'Normal')
link('@text.title', 'Title')
link('@text.literal', 'Normal')
link('@markup.heading', 'Title')
link('@markup.raw', 'Normal')
link('@text.uri', 'Underlined')
link('@string.special.url', 'Underlined')
link('@markup.link.url', 'Underlined')
link('@text.math', 'Normal')
link('@text.environment', 'Normal')
link('@text.environment.name', 'Normal')
link('@markup.math', 'Normal')
link('@markup.environment', 'Normal')
link('@markup.environment.name', 'Normal')
link('@text.reference', 'Normal')
link('@markup.link', 'Normal')
link('@text.todo', 'Todo')
link('@markup.list.checked', 'Todo')
link('@markup.list.unchecked', 'Todo')
link('@comment.todo', 'Todo')
link('@text.note', 'WarningMsg')
link('@comment.info', 'WarningMsg')
link('@comment.hint', 'WarningMsg')
link('@text.warning', 'WarningMsg')
link('@comment.warning', 'WarningMsg')
link('@text.danger', 'ErrorMsg')
link('@comment.danger', 'ErrorMsg')
link('@tag', 'Normal')
link('@tag.attribute', 'Normal')
link('@tag.delimiter', 'Normal')

-- Diagnostics
syntax('DiagnosticError', {fg=vcolors.r2})
syntax('DiagnosticWarn', {fg=vcolors.y3})
syntax('DiagnosticInfo', {fg=vcolors.b3})
syntax('DiagnosticHint', {fg=vcolors.g3})
syntax('DiagnosticUnderlineError', {style='underline'})
syntax('DiagnosticUnderlineWarn', {style='underline'})
syntax('DiagnosticUnderlineInfo', {style='underline'})
syntax('DiagnosticUnderlineHint', {style='underline'})

-- Git signs
link('GitSignsAdd', 'DiffAdd')
link('GitSignsChange', 'DiffChange')
link('GitSignsDelete', 'DiffDelete')

A .config/code/init.lua => .config/code/init.lua +149 -0
@@ 0,0 1,149 @@
-- nvim coding config
-- Launch: NVIM_APPNAME=code nvim

vim.g.mapleader = " "

-- Disable netrw (snacks explorer replaces it)
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1

-- Appearance
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.signcolumn = "yes"
vim.opt.termguicolors = true
vim.opt.cursorline = true
vim.opt.showmode = false
vim.opt.laststatus = 2
vim.opt.fillchars = { eob = " " }

-- Kill syntax highlighting
vim.cmd("syntax off")
vim.cmd("filetype plugin indent on")

-- Colorscheme
vim.cmd("colorscheme vacme")

-- Indentation
vim.opt.expandtab = true
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.smartindent = true
vim.opt.autoindent = true

-- Search
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.hlsearch = false
vim.opt.incsearch = true

-- Wrapping
vim.opt.wrap = false
vim.opt.scrolloff = 8
vim.opt.sidescrolloff = 8

-- 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"
vim.opt.updatetime = 250

-- snacks.nvim (explorer + project picker)
require("snacks").setup({
  explorer = { enabled = true },
  picker = {
    enabled = true,
    sources = {
      projects = {
        dev = { "~/dev" },
        recent = false,
      },
    },
  },
})

-- gitsigns
require("gitsigns").setup({
  signs = {
    add = { text = "+" },
    change = { text = "~" },
    delete = { text = "_" },
    topdelete = { text = "-" },
    changedelete = { text = "~" },
  },
})

-- lualine (statusline)
require("lualine").setup({
  options = {
    theme = {
      normal = {
        a = { fg = "#424242", bg = "#EAFFFF", gui = "bold" },
        b = { fg = "#424242", bg = "#EAEBDB" },
        c = { fg = "#424242", bg = "#FFFFEC" },
      },
      insert = {
        a = { fg = "#424242", bg = "#EFFEEC", gui = "bold" },
      },
      visual = {
        a = { fg = "#424242", bg = "#EEEEA7", gui = "bold" },
      },
      command = {
        a = { fg = "#424242", bg = "#D0D0F7", gui = "bold" },
      },
      inactive = {
        c = { fg = "#999957", bg = "#FFFFEC" },
      },
    },
    icons_enabled = false,
    component_separators = { left = "|", right = "|" },
    section_separators = { left = "", right = "" },
  },
  sections = {
    lualine_a = { "mode" },
    lualine_b = { "branch" },
    lualine_c = { "filename" },
    lualine_x = { "filetype" },
    lualine_y = { "progress" },
    lualine_z = { "location" },
  },
})

-- Keymaps
local map = vim.keymap.set

-- Explorer
map("n", "<leader>e", function() Snacks.explorer.open() end, { desc = "Toggle file explorer" })

-- Project switcher (browse ~/dev directories)
map("n", "<leader>fp", function()
  local dev_root = vim.fn.expand("~/dev")
  Snacks.picker.files({
    cwd = dev_root,
    cmd = "fd",
    args = { "--type", "d", "--max-depth", "1" },
    confirm = function(picker, item)
      picker:close()
      if item then
        local dir = dev_root .. "/" .. item.text:gsub("/$", "")
        vim.cmd("cd " .. vim.fn.fnameescape(dir))
        Snacks.picker.files({ cwd = dir })
      end
    end,
  })
end, { desc = "Switch 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" })

-- Basics
map("n", "<leader>w", "<cmd>w<cr>", { desc = "Save" })
map("n", "<leader>q", "<cmd>q<cr>", { desc = "Quit" })

A .config/conky/clock.conf => .config/conky/clock.conf +46 -0
@@ 0,0 1,46 @@
-- =============================================================================
-- Conky Clock - Plan 9 Analog Clock
-- Author: Kris Yotam
-- =============================================================================

conky.config = {
    alignment = 'top_right',
    background = false,
    border_width = 0,
    border_inner_margin = 0,

    lua_load = '/home/krisyotam/.config/conky/clock.lua',
    lua_draw_hook_post = 'draw_clock',

    default_color = '1a2028',

    double_buffer = true,
    draw_borders = false,
    draw_graph_borders = false,
    draw_outline = false,
    draw_shades = false,
    use_xft = true,
    font = 'JetBrainsMono Nerd Font:size=9',

    xinerama_head = 0,
    gap_x = 57,
    gap_y = 55,

    minimum_width = 180,
    maximum_width = 180,
    minimum_height = 180,

    own_window = true,
    own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
    own_window_class = 'ConkyClock',
    own_window_type = 'override',
    own_window_transparent = false,
    own_window_argb_visual = true,
    own_window_argb_value = 190,
    own_window_colour = '1a2028',

    update_interval = 1,
}

conky.text = [[
]]

A .config/conky/clock.lua => .config/conky/clock.lua +106 -0
@@ 0,0 1,106 @@
require "cairo"

-- Plan 9 style analog clock
-- Cream face, cyan border, simple black hands

local border_width = 8
local border_color = { 0.918, 1.0, 1.0, 1.0 }   -- #EAFFFF
local face_color   = { 1.0, 1.0, 0.918, 1.0 }    -- #FFFFEA (Plan 9 cream)
local hand_color   = { 0.05, 0.05, 0.05, 1.0 }   -- near black
local tick_color   = { 0.3, 0.3, 0.3, 1.0 }       -- dark grey
local second_color = { 0.83, 0.42, 0.42, 1.0 }    -- #D46A6A muted red
local center_color = { 0.3, 0.3, 0.3, 1.0 }

function conky_draw_clock()
    if conky_window == nil then return end

    local cs = cairo_xlib_surface_create(
        conky_window.display,
        conky_window.drawable,
        conky_window.visual,
        conky_window.width,
        conky_window.height
    )
    local cr = cairo_create(cs)

    local w = conky_window.width
    local h = conky_window.height
    local cx = w / 2
    local cy = h / 2
    local radius = math.min(w, h) / 2 - border_width - 8

    -- Border (4 filled rectangles)
    local bw = border_width
    cairo_set_source_rgba(cr, table.unpack(border_color))
    cairo_rectangle(cr, 0, 0, w, bw)
    cairo_rectangle(cr, 0, h - bw, w, bw)
    cairo_rectangle(cr, 0, 0, bw, h)
    cairo_rectangle(cr, w - bw, 0, bw, h)
    cairo_fill(cr)

    -- Clock face
    cairo_set_source_rgba(cr, table.unpack(face_color))
    cairo_arc(cr, cx, cy, radius, 0, 2 * math.pi)
    cairo_fill(cr)

    -- Face outline
    cairo_set_source_rgba(cr, table.unpack(tick_color))
    cairo_set_line_width(cr, 1.5)
    cairo_arc(cr, cx, cy, radius, 0, 2 * math.pi)
    cairo_stroke(cr)

    -- Hour ticks
    for i = 0, 11 do
        local angle = (i * 30 - 90) * math.pi / 180
        local inner = radius * 0.85
        local outer = radius * 0.95
        local tick_w = 2.0
        if i % 3 == 0 then
            inner = radius * 0.78
            tick_w = 3.0
        end
        cairo_set_line_width(cr, tick_w)
        cairo_set_source_rgba(cr, table.unpack(tick_color))
        cairo_move_to(cr, cx + inner * math.cos(angle), cy + inner * math.sin(angle))
        cairo_line_to(cr, cx + outer * math.cos(angle), cy + outer * math.sin(angle))
        cairo_stroke(cr)
    end

    -- Get time
    local hours = tonumber(os.date("%I"))
    local mins  = tonumber(os.date("%M"))
    local secs  = tonumber(os.date("%S"))

    -- Hour hand
    local hour_angle = ((hours + mins / 60) * 30 - 90) * math.pi / 180
    cairo_set_source_rgba(cr, table.unpack(hand_color))
    cairo_set_line_width(cr, 4)
    cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND)
    cairo_move_to(cr, cx, cy)
    cairo_line_to(cr, cx + radius * 0.5 * math.cos(hour_angle), cy + radius * 0.5 * math.sin(hour_angle))
    cairo_stroke(cr)

    -- Minute hand
    local min_angle = ((mins + secs / 60) * 6 - 90) * math.pi / 180
    cairo_set_source_rgba(cr, table.unpack(hand_color))
    cairo_set_line_width(cr, 2.5)
    cairo_move_to(cr, cx, cy)
    cairo_line_to(cr, cx + radius * 0.72 * math.cos(min_angle), cy + radius * 0.72 * math.sin(min_angle))
    cairo_stroke(cr)

    -- Second hand
    local sec_angle = (secs * 6 - 90) * math.pi / 180
    cairo_set_source_rgba(cr, table.unpack(second_color))
    cairo_set_line_width(cr, 1.2)
    cairo_move_to(cr, cx, cy)
    cairo_line_to(cr, cx + radius * 0.78 * math.cos(sec_angle), cy + radius * 0.78 * math.sin(sec_angle))
    cairo_stroke(cr)

    -- Center dot
    cairo_set_source_rgba(cr, table.unpack(center_color))
    cairo_arc(cr, cx, cy, 4, 0, 2 * math.pi)
    cairo_fill(cr)

    cairo_destroy(cr)
    cairo_surface_destroy(cs)
end

M .config/nvim/colors/acme.lua => .config/nvim/colors/acme.lua +2 -2
@@ 12,8 12,8 @@ local colors = {
  bg_dim = "#ffffca",
  bg_dark = "#eeee9e",
  cursor = "#98ce8f",
  tagbar = "#aeeeee",
  tagbar_inactive = "#eaffff",
  tagbar = "#eaffff",
  tagbar_inactive = "#bdc1bb",

  error = "#b85c57",
  warn = "#8f7634",

M .config/wal/postrun => .config/wal/postrun +2 -0
@@ 32,4 32,6 @@ fix_sequences() {

fix_sequences <"${XDG_CACHE_HOME:-$HOME/.cache}/wal/sequences"

xrdb -merge "${XDG_CACHE_HOME:-$HOME/.cache}/wal/colors-dwm-xresources"

pkill dunst; setsid -f dunst
\ No newline at end of file

D .config/wal/templates/btop.theme => .config/wal/templates/btop.theme +0 -62
@@ 1,62 0,0 @@
# btop pywal theme - auto-generated by pywal
# Background is transparent (set theme_background = False in btop.conf)

# Main background and foreground
theme[main_bg]="{background}"
theme[main_fg]="{foreground}"

# Title and highlighted text
theme[title]="{foreground}"
theme[hi_fg]="{color4}"

# Selected item
theme[selected_bg]="{color1}"
theme[selected_fg]="{foreground}"

# Inactive/disabled text
theme[inactive_fg]="{color8}"

# Graph colors
theme[proc_misc]="{color3}"

# Box borders
theme[cpu_box]="{color1}"
theme[mem_box]="{color2}"
theme[net_box]="{color3}"
theme[proc_box]="{color4}"

# CPU graph gradient (low to high)
theme[cpu_start]="{color2}"
theme[cpu_mid]="{color1}"
theme[cpu_end]="{color4}"

# Memory graph colors
theme[free_start]="{color2}"
theme[free_mid]="{color3}"
theme[free_end]="{color4}"

theme[cached_start]="{color1}"
theme[cached_mid]="{color2}"
theme[cached_end]="{color3}"

theme[available_start]="{color6}"
theme[available_mid]="{color5}"
theme[available_end]="{color8}"

theme[used_start]="{color4}"
theme[used_mid]="{color1}"
theme[used_end]="{color3}"

# Network graph
theme[download_start]="{color1}"
theme[download_mid]="{color2}"
theme[download_end]="{color4}"

theme[upload_start]="{color3}"
theme[upload_mid]="{color2}"
theme[upload_end]="{color6}"

# Process graph
theme[process_start]="{color4}"
theme[process_mid]="{color1}"
theme[process_end]="{color3}"

A .config/wal/templates/colors-dwm-xresources => .config/wal/templates/colors-dwm-xresources +33 -0
@@ 0,0 1,33 @@
! dwm xresources generated by pywal
dwm.normfgcolor: {foreground}
dwm.normbgcolor: {background}
dwm.normbordercolor: {color8}
dwm.normfloatcolor: {color8}
dwm.selfgcolor: {foreground}
dwm.selbgcolor: {color2}
dwm.selbordercolor: {foreground}
dwm.selfloatcolor: {color2}
dwm.titlenormfgcolor: {foreground}
dwm.titlenormbgcolor: {background}
dwm.titlenormbordercolor: {color8}
dwm.titlenormfloatcolor: {color8}
dwm.titleselfgcolor: {foreground}
dwm.titleselbgcolor: {color2}
dwm.titleselbordercolor: {foreground}
dwm.titleselfloatcolor: {color2}
dwm.tagsnormfgcolor: {foreground}
dwm.tagsnormbgcolor: {background}
dwm.tagsnormbordercolor: {color8}
dwm.tagsnormfloatcolor: {color8}
dwm.tagsselfgcolor: {foreground}
dwm.tagsselbgcolor: {color2}
dwm.tagsselbordercolor: {foreground}
dwm.tagsselfloatcolor: {color2}
dwm.hidnormfgcolor: {color2}
dwm.hidnormbgcolor: {background}
dwm.hidselfgcolor: {color6}
dwm.hidselbgcolor: {background}
dwm.urgfgcolor: {foreground}
dwm.urgbgcolor: {color1}
dwm.urgbordercolor: {color1}
dwm.urgfloatcolor: {color1}

M .config/wal/templates/zathurarc => .config/wal/templates/zathurarc +1 -1
@@ 43,4 43,4 @@ set completion-fg                  "{color4}"
set completion-highlight-bg        "{color3}"
set completion-highlight-fg        "{color4}"
set recolor-lightcolor             "{background}"
set recolor-darkcolor              "{foreground}"
\ No newline at end of file
set recolor-darkcolor              "{foreground}"

A .config/write/colors/writing.lua => .config/write/colors/writing.lua +99 -0
@@ 0,0 1,99 @@
vim.cmd("highlight clear")
vim.g.colors_name = "writing"

local bg = "#1a1a1a"
local fg = "#c8c8c8"
local dim = "#585858"
local cursorline = "#222222"

local hi = function(group, opts)
  vim.api.nvim_set_hl(0, group, opts)
end

-- Core
hi("Normal", { bg = bg, fg = fg })
hi("NormalFloat", { bg = bg, fg = fg })
hi("NormalNC", { bg = bg, fg = fg })
hi("EndOfBuffer", { bg = bg, fg = bg })
hi("CursorLine", { bg = cursorline })
hi("CursorLineNr", { bg = cursorline, fg = fg })
hi("LineNr", { fg = dim })
hi("SignColumn", { bg = bg })
hi("VertSplit", { bg = bg, fg = dim })
hi("WinSeparator", { bg = bg, fg = dim })
hi("StatusLine", { bg = bg, fg = bg })
hi("StatusLineNC", { bg = bg, fg = bg })
hi("TabLine", { bg = bg, fg = dim })
hi("TabLineFill", { bg = bg })
hi("TabLineSel", { bg = bg, fg = fg })

-- Popup/float
hi("Pmenu", { bg = "#222222", fg = fg })
hi("PmenuSel", { bg = "#333333", fg = fg })
hi("PmenuSbar", { bg = "#222222" })
hi("PmenuThumb", { bg = dim })

-- Search
hi("Search", { bg = "#333333", fg = fg })
hi("IncSearch", { bg = "#444444", fg = fg })

-- Visual
hi("Visual", { bg = "#333333" })
hi("VisualNOS", { bg = "#333333" })

-- Spelling
hi("SpellBad", { undercurl = true, sp = "#cc6666" })
hi("SpellCap", { undercurl = true, sp = "#6699cc" })
hi("SpellRare", { undercurl = true, sp = "#b294bb" })
hi("SpellLocal", { undercurl = true, sp = "#8abeb7" })

-- Diff
hi("DiffAdd", { bg = "#1d2e1d" })
hi("DiffChange", { bg = "#2e2e1d" })
hi("DiffDelete", { bg = "#2e1d1d" })
hi("DiffText", { bg = "#3e3e1d" })

-- Folding
hi("Folded", { bg = "#222222", fg = dim })
hi("FoldColumn", { bg = bg, fg = dim })

-- Misc UI
hi("Directory", { fg = fg })
hi("Title", { fg = fg, bold = true })
hi("Question", { fg = fg })
hi("MoreMsg", { fg = fg })
hi("WarningMsg", { fg = "#cc6666" })
hi("ErrorMsg", { fg = "#cc6666", bg = bg })
hi("NonText", { fg = dim })
hi("SpecialKey", { fg = dim })
hi("Conceal", { fg = dim })
hi("MatchParen", { bg = "#444444" })
hi("ColorColumn", { bg = "#222222" })
hi("Cursor", { bg = fg, fg = bg })

-- Everything text-like is just fg (no syntax highlighting)
for _, group in ipairs({
  "Comment", "Constant", "String", "Character", "Number", "Boolean", "Float",
  "Identifier", "Function", "Statement", "Conditional", "Repeat", "Label",
  "Operator", "Keyword", "Exception", "PreProc", "Include", "Define", "Macro",
  "PreCondit", "Type", "StorageClass", "Structure", "Typedef", "Special",
  "SpecialChar", "Tag", "Delimiter", "SpecialComment", "Debug", "Underlined",
  "Error", "Todo",
}) do
  hi(group, { fg = fg, bg = "NONE" })
end

-- NvimTree
hi("NvimTreeNormal", { bg = bg, fg = fg })
hi("NvimTreeNormalNC", { bg = bg, fg = fg })
hi("NvimTreeEndOfBuffer", { bg = bg, fg = bg })
hi("NvimTreeWinSeparator", { bg = bg, fg = bg })
hi("NvimTreeFolderIcon", { fg = dim })
hi("NvimTreeFolderName", { fg = fg })
hi("NvimTreeOpenedFolderName", { fg = fg })
hi("NvimTreeRootName", { fg = fg, bold = true })

-- Goyo padding windows
hi("NormalNC", { bg = bg, fg = bg })
hi("WinBar", { bg = bg, fg = bg })
hi("WinBarNC", { bg = bg, fg = bg })

A .config/write/init.lua => .config/write/init.lua +234 -0
@@ 0,0 1,234 @@
-- 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,
})

M .config/x11/xprofile => .config/x11/xprofile +11 -8
@@ 3,28 3,31 @@
# This file runs when a DM logs you into a graphical session.
# If you use startx/xinit like a Chad, this file will also be sourced.

# Load environment variables from login profile
. "$HOME/.profile"
# Add local bin to PATH for scripts
export PATH="$HOME/.local/bin:$HOME/.local/bin/statusbar:$PATH"

# Auto-detect device type and set DPI accordingly
DEVICE_TYPE_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/lazykris/device-type"
DEVICE_TYPE_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/srice/device-type"
if [ -f "$DEVICE_TYPE_FILE" ] && [ "$(cat "$DEVICE_TYPE_FILE")" = "laptop" ]; then
	xrandr --dpi 150	# Laptop HiDPI
else
	xrandr --dpi 128	# Desktop 1440p scaled
fi
~/.screenlayout/main.sh	# Set up monitor layout
setxkbmap -option caps:none	# Completely disable Caps Lock
xrdb -merge ${XDG_CONFIG_HOME:-$HOME/.config}/x11/xresources	# Load base Xresources
setbg -s -c "$HOME/.local/share/wallpapers/cs-winter-1920x960.webp" "$HOME/.local/share/wallpapers/p9blue-2560x1440.png"	# solid bg + plan9 colorscheme
xset s off -dpms s noblank	# Disable DPMS/screensaver (freezes with dual-GPU + 4 monitors)
setbg -s -c "$HOME/.local/share/wallpapers/plan9theme.png" "$HOME/.local/share/wallpapers/plan9.png"	# plan9 solid bg + plan9 winter colorscheme
setfan &	# Set case fan RGB (ice blue)

# Start gnome-keyring-daemon and export its environment variables
eval $(gnome-keyring-daemon --start --components=pkcs11,secrets,ssh)
# Start gnome-keyring-daemon with unlocked login keyring (needed for ProtonMail Bridge)
eval $(echo -n "kris" | gnome-keyring-daemon --replace --unlock --components=pkcs11,secrets,ssh)
export SSH_AUTH_SOCK

autostart="mpd picom dunst unclutter pipewire wireplumber conky"
autostart="mpd picom dunst unclutter pipewire wireplumber conky protonmail-bridge"

for program in $autostart; do
	pgrep -x "$program" >/dev/null 2>&1 || "$program" &
done >/dev/null 2>&1

# dwmblocks is started by dwm's cool_autostart patch
# dwmblocks is started by dwm autostart

M .config/x11/xresources => .config/x11/xresources +56 -0
@@ 4,6 4,62 @@
!! Set a default font and font size as below:
*.font: JetBrainsMono Nerd Font:size=14

!! Plan 9 defaults
*.foreground: #000000
*.background: #737573
*.color0: #000000
*.color1: #aa0000
*.color2: #448844
*.color3: #99994c
*.color4: #005dbb
*.color5: #8888cc
*.color6: #008888
*.color7: #ffffea
*.color8: #737573
*.color9: #ff0000
*.color10: #88cc88
*.color11: #eeee9e
*.color12: #4993dd
*.color13: #ff00ff
*.color14: #eaffff
*.color15: #ffffff
*.color256: #737573
*.color257: #ffffea

!! DWM Plan 9 defaults
dwm.normfgcolor: #000000
dwm.normbgcolor: #ffffea
dwm.normbordercolor: #eaffff
dwm.normfloatcolor: #eaffff
dwm.selfgcolor: #000000
dwm.selbgcolor: #eaffff
dwm.selbordercolor: #eaffff
dwm.selfloatcolor: #eaffff
dwm.titlenormfgcolor: #000000
dwm.titlenormbgcolor: #ffffea
dwm.titlenormbordercolor: #eaffff
dwm.titlenormfloatcolor: #eaffff
dwm.titleselfgcolor: #000000
dwm.titleselbgcolor: #eaffff
dwm.titleselbordercolor: #eaffff
dwm.titleselfloatcolor: #eaffff
dwm.tagsnormfgcolor: #000000
dwm.tagsnormbgcolor: #ffffea
dwm.tagsnormbordercolor: #eaffff
dwm.tagsnormfloatcolor: #eaffff
dwm.tagsselfgcolor: #000000
dwm.tagsselbgcolor: #eaffff
dwm.tagsselbordercolor: #eaffff
dwm.tagsselfloatcolor: #eaffff
dwm.hidnormfgcolor: #737573
dwm.hidnormbgcolor: #ffffea
dwm.hidselfgcolor: #000000
dwm.hidselbgcolor: #eaffff
dwm.urgfgcolor: #000000
dwm.urgbgcolor: #eeee9e
dwm.urgbordercolor: #eaffff
dwm.urgfloatcolor: #eaffff

/* name		dark	light */
/* black	0	8 */
/* red		1	9 */

A .local/bin/code => .local/bin/code +2 -0
@@ 0,0 1,2 @@
#!/bin/sh
NVIM_APPNAME=code exec nvim "$@"

M .local/bin/media/setbg => .local/bin/media/setbg +60 -7
@@ 12,9 12,14 @@
#	setbg <wallpaper> -c <colorimg>    # wallpaper from first, colors from second
#	setbg -c <colorimg> <wallpaper>    # same as above (order doesn't matter)
#	setbg -s <wallpaper>               # silent mode (no notifications)
#	setbg -w <wallpaper>               # wallpaper only (no colorscheme)
#	setbg -d                           # default Plan 9 theme + solid grey root

# Location of link to wallpaper link.
bgloc="${XDG_DATA_HOME:-$HOME/.local/share}/bg"
default_bg="#737573"
default_wallpaper="${XDG_DATA_HOME:-$HOME/.local/share}/wallpapers/plan9-grey.png"
xresources="${XDG_CONFIG_HOME:-$HOME/.config}/x11/xresources"

# Configuration files of applications that have their themes changed by pywal.
dunstconf="${XDG_CONFIG_HOME:-$HOME/.config}/dunst/dunstrc"


@@ 23,14 28,62 @@ zathuraconf="${XDG_CONFIG_HOME:-$HOME/.config}/zathura/zathurarc"
# Parse options
silent=""
colorimg=""
wallonly=""
default_theme=""

while getopts "sc:" o; do case "${o}" in
while getopts "sdwc:" o; do case "${o}" in
	s) silent='1' ;;
	d) default_theme='1' ;;
	w) wallonly='1' ;;
	c) colorimg="$OPTARG" ;;
esac done

shift $((OPTIND - 1))

refreshdwm() {
	pidof dwm >/dev/null && xdotool key super+F5
}

set_default_terminal_colors() {
	for term in /dev/pts/[0-9]*; do
		[ -w "$term" ] || continue
		{
			printf '\033]10;#000000\007'
			printf '\033]11;%s\007' "$default_bg"
			printf '\033]12;#000000\007'
			printf '\033]4;0;#000000\007'
			printf '\033]4;1;#aa0000\007'
			printf '\033]4;2;#448844\007'
			printf '\033]4;3;#99994c\007'
			printf '\033]4;4;#005dbb\007'
			printf '\033]4;5;#8888cc\007'
			printf '\033]4;6;#008888\007'
			printf '\033]4;7;#ffffea\007'
			printf '\033]4;8;%s\007' "$default_bg"
			printf '\033]4;9;#ff0000\007'
			printf '\033]4;10;#88cc88\007'
			printf '\033]4;11;#eeee9e\007'
			printf '\033]4;12;#4993dd\007'
			printf '\033]4;13;#ff00ff\007'
			printf '\033]4;14;#eaffff\007'
			printf '\033]4;15;#ffffff\007'
			printf '\033]4;256;#000000\007'
			printf '\033]4;258;%s\007' "$default_bg"
			printf '\033]4;259;#000000\007'
		} >"$term" 2>/dev/null
	done
}

if [ -n "$default_theme" ]; then
	[ -f "$xresources" ] && xrdb -merge "$xresources"
	[ -f "$default_wallpaper" ] && ln -sf "$default_wallpaper" "$bgloc"
	xwallpaper --tile "$default_wallpaper"
	set_default_terminal_colors
	[ -z "$silent" ] && notify-send "Default Plan 9 theme" "$default_bg"
	refreshdwm
	exit 0
fi

# If no argument, just apply existing wallpaper
if [ -z "$1" ]; then
	[ -f "$bgloc" ] && xwallpaper --zoom "$bgloc"


@@ 38,7 91,7 @@ if [ -z "$1" ]; then
fi

trueloc="$(readlink -f "$1")" &&
case "$(file --mime-type -b "$trueloc")" in
case "$(/usr/bin/file --mime-type -b "$trueloc")" in
	image/* ) ln -sf "$trueloc" "$bgloc" && [ -z "$silent" ] && notify-send -i "$bgloc" "Changing wallpaper..." ;;
	inode/directory ) ln -sf "$(find "$trueloc" -iregex '.*.\(jpg\|jpeg\|png\|gif\)' -type f | shuf -n 1)" "$bgloc" && [ -z "$silent" ] && notify-send -i "$bgloc" "Random Wallpaper chosen." ;;
	*) [ -z "$silent" ] && notify-send "🖼️ Error" "Not a valid image or directory." ; exit 1;;


@@ 48,7 101,7 @@ esac
if [ -n "$colorimg" ]; then
	colorloc="$(readlink -f "$colorimg")"
	# Verify it's a valid image
	case "$(file --mime-type -b "$colorloc")" in
	case "$(/usr/bin/file --mime-type -b "$colorloc")" in
		image/* ) ;;
		*) [ -z "$silent" ] && notify-send "🎨 Error" "Color image not valid." ; colorloc="" ;;
	esac


@@ 56,16 109,16 @@ else
	colorloc="$(readlink -f "$bgloc")"
fi

# If pywal is installed, use it.
if command -v wal >/dev/null 2>&1 ; then
# If pywal is installed and wallpaper-only mode is not set, use it.
if [ -z "$wallonly" ] && command -v wal >/dev/null 2>&1 ; then
	wal -n -i "$colorloc" -o "${XDG_CONFIG_HOME:-$HOME/.config}/wal/postrun" >/dev/null 2>&1
	[ -z "$silent" ] && [ -n "$colorimg" ] && notify-send -i "$colorloc" "Colors from separate image"
# If pywal is removed, return config files to normal.
else
elif [ -z "$wallonly" ]; then
	[ -f "$dunstconf.bak" ] && unlink "$dunstconf" && mv "$dunstconf.bak" "$dunstconf"
	[ -f "$zathuraconf.bak" ] && unlink "$zathuraconf" && mv "$zathuraconf.bak" "$zathuraconf"
fi

xwallpaper --zoom "$bgloc"
# If running, dwm hit the key to refresh the color scheme.
pidof dwm >/dev/null && xdotool key super+F5
refreshdwm

M .local/bin/misc/sync => .local/bin/misc/sync +1 -1
@@ 17,7 17,7 @@ set -euo pipefail
#   sync --quiet      # suppress per-repo output (for git.js integration)
###############################################################################

CONTENT_DIR="$HOME/content"
CONTENT_DIR="$HOME/.corpus/content"
CONTENT_TYPES=(blog diary essays fiction news notes ocs papers progymnasmata reviews verse)

QUIET=false

A .local/bin/nnn-write => .local/bin/nnn-write +6 -0
@@ 0,0 1,6 @@
#!/bin/sh
# nnn-write -- open .mdx/.md files in write (nvim writing config)
case "$1" in
    *.mdx|*.md) write "$1" ;;
    *) ${NNN_FALLBACK_OPENER:-xdg-open} "$1" >/dev/null 2>&1 & disown ;;
esac

A .local/bin/sds => .local/bin/sds +33 -0
@@ 0,0 1,33 @@
#!/bin/sh
# sds - subdomain scanner via Certificate Transparency logs (crt.sh)
# Usage: sds <domain>

[ -z "$1" ] && echo "Usage: sds <domain>" && exit 1

domain="$(echo "$1" | sed 's|^https\?://||;s|/.*||' | tr '[:upper:]' '[:lower:]')"

printf "Scanning CT logs for *.%s ...\n\n" "$domain"

curl -sf "https://crt.sh/?q=%25.${domain}&output=json" |
  jq -r '.[].common_name, .[].name_value' 2>/dev/null |
  tr '\n' '\n' |
  sed 's/\r//g' |
  tr '[:upper:]' '[:lower:]' |
  grep -v "^${domain}$" |
  grep "\.${domain}$\|^\*\.${domain}$" |
  sort -u |
  awk -v dom="$domain" '
    /^\*\./ { wild[NR] = $0; next }
    { reg[NR] = $0 }
    END {
      n = 0
      for (i in reg) n++
      for (i in wild) n++
      printf "%d subdomains found for %s\n\n", n, dom
      printf "%-4s  %s\n", "#", "SUBDOMAIN"
      printf "%-4s  %s\n", "---", "---"
      c = 0
      for (i in reg) { c++; printf "%-4d  %s\n", c, reg[i] }
      for (i in wild) { c++; printf "%-4d  %s\n", c, wild[i] }
    }
  '

A .local/bin/tome => .local/bin/tome +1068 -0
@@ 0,0 1,1068 @@
#!/usr/bin/env bash
set -euo pipefail

###############################################################################
# TOME -- Content manager for krisyotam.com
#
# Maintainer:   Kris Yotam <krisyotam@pm.me>
# License:      MIT
# Created:      2026-05-18
# Description:  Unified content creator, editor, and metadata manager.
#               Three actions (create, edit, data) with two interfaces
#               (nnn/fzf TUI or dmenu).
#
# Usage:
#   tome -nnn -create      Create new content entry (TUI)
#   tome -dmenu -create    Create new content entry (dmenu)
#   tome -nnn -edit        Browse content, open in write (TUI)
#   tome -dmenu -edit      Browse content, open in write (dmenu)
#   tome -nnn -data        Edit content metadata (TUI)
#   tome -dmenu -data      Edit content metadata (dmenu)
###############################################################################

###############################################################################
# config
###############################################################################

DB="$HOME/dev/krisyotam.com/data/content.db"
CONTENT_DIR="$HOME/.corpus/content"
TIL_DIR="$HOME/.corpus/til"

# Menu entries (order matters -- displayed as-is)
# Indented entries are news publications
MENU_ENTRIES=(
    "papers"
    "essays"
    "blog"
    "diary"
    "reviews"
    "verse"
    "news"
    "  the-soapbox"
    "  field-notes"
    "  off-the-record"
    "til"
    "fiction"
    "ocs"
    "prayers"
    "progymnasmata"
)

# Types that are actual content directories
ALL_TYPES=(papers essays blog diary reviews verse news til fiction ocs prayers progymnasmata)

# News publications
NEWS_PUBS=("the-soapbox" "field-notes" "off-the-record")

# Diary has no status/confidence/importance columns
DIARY_FIELDS=(title preview category_slug state)
STANDARD_FIELDS=(title preview category_slug status confidence importance state)

VALID_STATUSES=("Notes" "Draft" "In Progress" "Finished" "Abandoned")
VALID_CONFIDENCES=(
    "certain" "highly likely" "likely" "possible"
    "unlikely" "highly unlikely" "remote" "impossible"
)
VALID_STATES=("active" "hidden")

# nerd font icon map
declare -A TYPE_ICONS=(
    [papers]="󰈙"    [essays]="󰬝"   [blog]="󰬞"
    [diary]="󰃮"     [reviews]="󰓥"  [verse]="󱗝"
    [news]="󰎕"      [fiction]="󰂺"  [ocs]="󰏫"
    [prayers]="󰥍"   [progymnasmata]="󰑴"  [til]="󰛄"
    [the-soapbox]="󰎕" [field-notes]="󰎕" [off-the-record]="󰎕"
)

###############################################################################
# helpers
###############################################################################

have_cmd() { command -v "$1" >/dev/null 2>&1; }
die()      { echo "ERROR: $1" >&2; exit 1; }
sql()      { sqlite3 "$DB" "$1"; }
notify()   { notify-send "tome" "$1" 2>/dev/null || true; }

slugify() {
    echo "$1" |
        tr '[:upper:]' '[:lower:]' |
        sed 's/[^a-z0-9 -]//g' |
        sed 's/  */ /g; s/ /-/g; s/--*/-/g; s/^-//; s/-$//'
}

check_slug_globally() {
    local slug="$1"
    for dir in "$CONTENT_DIR"/*/; do
        [ -d "$dir" ] || continue
        if [ -f "$dir/${slug}.mdx" ]; then
            echo "$(basename "$dir"):${slug}.mdx"
            return
        fi
    done
    # Also check til
    for subdir in "$TIL_DIR"/*/; do
        [ -d "$subdir" ] || continue
        if [ -f "$subdir/${slug}.mdx" ]; then
            echo "til/$(basename "$subdir"):${slug}.mdx"
            return
        fi
    done
}

# Get content directory for a type (or news publication)
content_dir_for() {
    local selection="$1"
    case "$selection" in
        the-soapbox|field-notes|off-the-record)
            echo "$CONTENT_DIR/news"
            ;;
        til)
            echo "$TIL_DIR"
            ;;
        *)
            echo "$CONTENT_DIR/$selection"
            ;;
    esac
}

# Resolve a menu selection to its content type (for DB operations)
db_type_for() {
    local selection="$1"
    case "$selection" in
        the-soapbox|field-notes|off-the-record) echo "news" ;;
        *) echo "$selection" ;;
    esac
}

today() { date +%Y-%m-%d; }

###############################################################################
# argument parsing
###############################################################################

UI_MODE=""
ACTION_MODE=""

for arg in "$@"; do
    case "$arg" in
        -nnn)    UI_MODE="nnn" ;;
        -dmenu)  UI_MODE="dmenu" ;;
        -create) ACTION_MODE="create" ;;
        -edit)   ACTION_MODE="edit" ;;
        -data)   ACTION_MODE="data" ;;
        *)
            echo "Unknown flag: $arg" >&2
            echo "Usage: tome -nnn|-dmenu -create|-edit|-data" >&2
            exit 1
            ;;
    esac
done

if [[ -z "$UI_MODE" || -z "$ACTION_MODE" ]]; then
    echo "Usage: tome -nnn|-dmenu -create|-edit|-data" >&2
    exit 1
fi

###############################################################################
# dependency checks
###############################################################################

check_deps_nnn() {
    have_cmd fzf     || die "fzf not found"
    have_cmd nnn     || die "nnn not found"
}

check_deps_dmenu() {
    have_cmd dmenu   || die "dmenu not found"
}

check_deps_data() {
    have_cmd sqlite3 || die "sqlite3 not found"
    [[ -f "$DB" ]]   || die "content.db not found at $DB"
}

###############################################################################
# menu rendering
###############################################################################

# Build the fzf menu string
fzf_menu() {
    for entry in "${MENU_ENTRIES[@]}"; do
        echo "$entry"
    done
    echo "quit"
}

# Build the dmenu menu string (with icons)
dmenu_menu() {
    for entry in "${MENU_ENTRIES[@]}"; do
        local trimmed="${entry## }"
        local indent="${entry%%[! ]*}"
        local icon="${TYPE_ICONS[$trimmed]:-󰈙}"
        echo "${indent}${icon}  ${trimmed}"
    done
}

# Parse a menu selection back to a clean type/publication name
parse_selection() {
    echo "$1" | sed 's/^[[:space:]]*//' | sed 's/^[^ ]* *//'
}

###############################################################################
# banners
###############################################################################

print_banner_main() {
    local cols
    cols="$(tput cols 2>/dev/null || echo 80)"

    if have_cmd figlet; then
        figlet -w "$cols" -f big "KRISYOTAM.COM" 2>/dev/null \
            || figlet -w "$cols" "KRISYOTAM.COM"
    else
        echo "KRISYOTAM.COM"
    fi

    echo
    echo "  est. 2025"
    echo "  author: Kris Yotam"
    echo "  script: tome"
    echo
    echo "  \"Do I contradict myself? Very well then I contradict myself,"
    echo "   (I am large, I contain multitudes.)\""
    echo "  -- Walt Whitman"
    echo
}

print_banner_data() {
    local cols
    cols="$(tput cols 2>/dev/null || echo 80)"

    if have_cmd figlet; then
        figlet -w "$cols" -f big "DATA" 2>/dev/null \
            || figlet -w "$cols" "DATA"
    else
        echo "=== DATA ==="
    fi

    echo
    echo "  Content metadata editor for krisyotam.com"
    echo
}

###############################################################################
# create: write frontmatter + .mdx file (pure shell)
###############################################################################

# Collect metadata and write the .mdx file
write_new_entry() {
    local selection="$1"
    local type
    type="$(db_type_for "$selection")"
    local target_dir
    target_dir="$(content_dir_for "$selection")"

    mkdir -p "$target_dir"

    # For news publications browsing into subdirectory is not needed
    # since all news lives flat in content/news/

    local title slug preview category status confidence importance
    local verse_type="" prayer_type="" form="" publication=""

    # Title
    if [[ "$UI_MODE" == "dmenu" ]]; then
        title="$(echo "" | dmenu -p "title:")"
    else
        read -rp "  Title: " title
    fi
    [[ -z "$title" ]] && return

    # Slug
    slug="$(slugify "$title")"
    local collision
    collision="$(check_slug_globally "$slug")"
    if [[ -n "$collision" ]]; then
        if [[ "$UI_MODE" == "dmenu" ]]; then
            notify "Slug collision: $slug ($collision)"
            slug="$(echo "$slug" | dmenu -p "collision! alt slug:")"
        else
            echo "  Slug collision: $slug ($collision)"
            read -rp "  Alt slug: " slug
        fi
        [[ -z "$slug" ]] && return
        slug="$(slugify "$slug")"
        collision="$(check_slug_globally "$slug")"
        if [[ -n "$collision" ]]; then
            if [[ "$UI_MODE" == "dmenu" ]]; then
                notify "Still colliding: $slug. Aborting."
            else
                echo "  Still colliding: $slug. Aborting."
            fi
            return
        fi
    fi

    # Preview
    if [[ "$UI_MODE" == "dmenu" ]]; then
        preview="$(echo "" | dmenu -p "preview:")"
    else
        read -rp "  Preview: " preview
    fi

    # Category
    check_deps_data
    if [[ "$UI_MODE" == "dmenu" ]]; then
        category="$(sql "SELECT slug FROM categories ORDER BY slug" | dmenu -i -l 20 -p "category:")"
    else
        category="$(sql "SELECT slug FROM categories ORDER BY slug" | fzf --prompt="category> " --height=20 --reverse)" || category=""
    fi
    [[ -z "$category" ]] && return

    # Status (skip for diary)
    if [[ "$type" == "diary" ]]; then
        status="" confidence="" importance=""
    else
        if [[ "$UI_MODE" == "dmenu" ]]; then
            status="$(printf "%s\n" "${VALID_STATUSES[@]}" | dmenu -i -l 5 -p "status:")"
            confidence="$(printf "%s\n" "${VALID_CONFIDENCES[@]}" | dmenu -i -l 8 -p "confidence:")"
            importance="$(seq 1 10 | dmenu -i -l 10 -p "importance:")"
        else
            status="$(printf "%s\n" "${VALID_STATUSES[@]}" | fzf --prompt="status> " --height=8 --reverse)" || status="Draft"
            confidence="$(printf "%s\n" "${VALID_CONFIDENCES[@]}" | fzf --prompt="confidence> " --height=10 --reverse)" || confidence="possible"
            read -rp "  Importance (1-10): " importance
        fi
        [[ -z "$status" ]] && status="Draft"
        [[ -z "$confidence" ]] && confidence="possible"
        [[ -z "$importance" ]] && importance="5"
    fi

    # Type-specific fields
    case "$type" in
        verse)
            if [[ "$UI_MODE" == "dmenu" ]]; then
                verse_type="$(printf 'haiku\nsonnet\nfree-verse\node\nlimerick\nvillanelle\nother' | dmenu -i -l 7 -p "verse type:")"
            else
                verse_type="$(printf 'haiku\nsonnet\nfree-verse\node\nlimerick\nvillanelle\nother' | fzf --prompt="verse type> " --height=10 --reverse)" || verse_type=""
            fi
            ;;
        prayers)
            if [[ "$UI_MODE" == "dmenu" ]]; then
                prayer_type="$(echo "" | dmenu -p "prayer type:")"
                form="$(echo "" | dmenu -p "form:")"
            else
                read -rp "  Prayer type: " prayer_type
                read -rp "  Form: " form
            fi
            ;;
        news)
            # Publication is determined by the menu selection
            case "$selection" in
                the-soapbox|field-notes|off-the-record)
                    publication="$selection"
                    ;;
                news)
                    # Selected the parent "news" -- ask which publication
                    if [[ "$UI_MODE" == "dmenu" ]]; then
                        publication="$(printf '%s\n' "${NEWS_PUBS[@]}" | dmenu -i -l 3 -p "publication:")"
                    else
                        publication="$(printf '%s\n' "${NEWS_PUBS[@]}" | fzf --prompt="publication> " --height=5 --reverse)" || publication="the-soapbox"
                    fi
                    [[ -z "$publication" ]] && publication="the-soapbox"
                    ;;
            esac
            ;;
    esac

    # Tags
    local tags=""
    if [[ "$UI_MODE" == "dmenu" ]]; then
        # Collect up to 5 tags
        local tag_list=()
        for _ in 1 2 3 4 5; do
            local tag
            tag="$(sql "SELECT slug FROM tags ORDER BY slug" | dmenu -i -l 20 -p "tag (empty=done):")" || break
            [[ -z "$tag" ]] && break
            tag_list+=("$tag")
        done
        tags="$(printf '%s\n' "${tag_list[@]}" 2>/dev/null | paste -sd',' -)"
    else
        echo "  Tags (comma-separated, or empty):"
        read -rp "  > " tags
    fi

    # Build the .mdx file
    local target_file="$target_dir/${slug}.mdx"
    local date
    date="$(today)"

    {
        echo "---"
        echo "title: \"$title\""
        echo "slug: $slug"
        echo "type: $type"
        echo "category: $category"
        echo "start_date: '$date'"
        echo "end_date: ''"
        echo "state: active"
        [[ -n "$preview" ]] && echo "preview: \"$preview\""

        if [[ "$type" != "diary" ]]; then
            echo "status: $status"
            echo "confidence: $confidence"
            echo "importance: $importance"
        fi

        # Type-specific fields
        [[ -n "$verse_type" ]] && echo "verse_type: $verse_type"
        [[ -n "$prayer_type" ]] && echo "prayer_type: $prayer_type"
        [[ -n "$form" ]] && echo "form: $form"
        [[ -n "$publication" ]] && echo "publication: $publication"

        # Tags as inline YAML array
        if [[ -n "$tags" ]]; then
            local tag_arr
            tag_arr="$(echo "$tags" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | awk '{printf "%s\"%s\"", (NR>1 ? ", " : ""), $0}')"
            echo "tags: [$tag_arr]"
        else
            echo "tags: []"
        fi

        echo "---"
        echo ""
    } > "$target_file"

    if [[ "$UI_MODE" == "dmenu" ]]; then
        notify "Created: $title ($slug)"
        # Open in write
        write "$target_file" &
        disown
    else
        echo
        echo "  Created: $target_file"
        echo
        read -rp "  Open in write? [Y/n] " yn
        case "$yn" in
            n|N) ;;
            *) write "$target_file" ;;
        esac
    fi
}

###############################################################################
# mode: -nnn -create
###############################################################################

run_nnn_create() {
    check_deps_nnn

    clear
    print_banner_main

    local selection
    selection="$(fzf_menu | fzf --prompt="create> " --height=20 --reverse)" || exit 0

    [[ "$selection" == "quit" ]] && exit 0

    # Trim whitespace (for indented news publications)
    selection="${selection## }"

    # If they selected "news" (parent), let them pick publication or create generic
    write_new_entry "$selection"
}

###############################################################################
# mode: -dmenu -create
###############################################################################

run_dmenu_create() {
    check_deps_dmenu

    local raw
    raw="$(dmenu_menu | dmenu -i -l 16 -p "create:")"
    [[ -z "$raw" ]] && exit 0

    local selection
    selection="$(parse_selection "$raw")"
    [[ -z "$selection" ]] && exit 0

    write_new_entry "$selection"
}

###############################################################################
# mode: -nnn -edit  (browse and open existing content in write)
###############################################################################

run_nnn_edit() {
    check_deps_nnn

    local nnn_write_helper="$HOME/.local/bin/nnn-write"
    [[ ! -x "$nnn_write_helper" ]] && die "nnn-write helper not found at $nnn_write_helper"

    clear
    print_banner_main

    while true; do
        local selection
        selection="$(fzf_menu | fzf --prompt="edit> " --height=20 --reverse)" || exit 0

        [[ "$selection" == "quit" ]] && exit 0

        selection="${selection## }"
        local target_dir
        target_dir="$(content_dir_for "$selection")"

        if [[ ! -d "$target_dir" ]]; then
            echo "Directory not found: $target_dir"
            read -rp "Press Enter..." _
            continue
        fi

        # For news publications, filter to only show matching files
        case "$selection" in
            the-soapbox|field-notes|off-the-record)
                # Show only files matching this publication
                local tmpdir
                tmpdir="$(mktemp -d)"
                # Symlink matching files into tmpdir for nnn browsing
                for f in "$target_dir"/*.mdx; do
                    [[ -f "$f" ]] || continue
                    if grep -q "^publication: $selection" "$f" 2>/dev/null; then
                        ln -s "$f" "$tmpdir/$(basename "$f")"
                    fi
                done
                NNN_OPENER="$nnn_write_helper" \
                NNN_FALLBACK_OPENER="${NNN_OPENER:-xdg-open}" \
                    nnn "$tmpdir"
                rm -rf "$tmpdir"
                ;;
            til)
                # TIL has subdirectories
                NNN_OPENER="$nnn_write_helper" \
                NNN_FALLBACK_OPENER="${NNN_OPENER:-xdg-open}" \
                    nnn "$target_dir"
                ;;
            *)
                NNN_OPENER="$nnn_write_helper" \
                NNN_FALLBACK_OPENER="${NNN_OPENER:-xdg-open}" \
                    nnn "$target_dir"
                ;;
        esac

        clear
        print_banner_main
    done
}

###############################################################################
# mode: -dmenu -edit
###############################################################################

run_dmenu_edit() {
    check_deps_dmenu

    local raw
    raw="$(dmenu_menu | dmenu -i -l 16 -p "edit:")"
    [[ -z "$raw" ]] && exit 0

    local selection
    selection="$(parse_selection "$raw")"
    [[ -z "$selection" ]] && exit 0

    local target_dir
    target_dir="$(content_dir_for "$selection")"
    [[ ! -d "$target_dir" ]] && { notify "Directory not found: $target_dir"; exit 1; }

    local file

    case "$selection" in
        the-soapbox|field-notes|off-the-record)
            # Filter news files by publication
            file="$(
                for f in "$target_dir"/*.mdx; do
                    [[ -f "$f" ]] || continue
                    if grep -q "^publication: $selection" "$f" 2>/dev/null; then
                        basename "$f" .mdx
                    fi
                done | sort | dmenu -i -l 20 -p "$selection:"
            )"
            [[ -z "$file" ]] && exit 0
            write "$target_dir/$file.mdx"
            ;;
        til)
            # TIL has subdirs -- first pick subdir, then file
            local subdir
            subdir="$(ls "$target_dir" | dmenu -i -l 10 -p "til topic:")"
            [[ -z "$subdir" ]] && exit 0
            file="$(ls "$target_dir/$subdir"/*.mdx 2>/dev/null | xargs -I{} basename {} .mdx | sort | dmenu -i -l 20 -p "$subdir:")"
            [[ -z "$file" ]] && exit 0
            write "$target_dir/$subdir/$file.mdx"
            ;;
        *)
            file="$(
                ls "$target_dir"/*.mdx 2>/dev/null |
                    xargs -I{} basename {} .mdx |
                    sort |
                    dmenu -i -l 20 -p "$selection:"
            )"
            [[ -z "$file" ]] && exit 0
            write "$target_dir/$file.mdx"
            ;;
    esac
}

###############################################################################
# metadata display (for -data mode)
###############################################################################

show_metadata() {
    local type="$1" slug="$2"

    local title preview category state tags id

    title="$(sql "SELECT title FROM $type WHERE slug='$slug'")"
    preview="$(sql "SELECT preview FROM $type WHERE slug='$slug'")"
    category="$(sql "SELECT category_slug FROM $type WHERE slug='$slug'")"
    state="$(sql "SELECT state FROM $type WHERE slug='$slug'")"

    id="$(sql "SELECT id FROM $type WHERE slug='$slug'")"
    tags="$(sql "SELECT t.slug FROM tags t JOIN content_tags ct ON ct.tag_id = t.id WHERE ct.content_type = '$type' AND ct.content_id = $id ORDER BY t.slug" | paste -sd', ' -)"

    echo
    echo "  ┌─ $slug ─────────────────────────────────"
    printf "  │ %-14s %s\n" "Title:" "$title"
    printf "  │ %-14s %s\n" "Preview:" "${preview:0:60}"
    printf "  │ %-14s %s\n" "Category:" "$category"
    printf "  │ %-14s %s\n" "Tags:" "$tags"

    if [[ "$type" != "diary" ]]; then
        local status confidence importance
        status="$(sql "SELECT status FROM $type WHERE slug='$slug'")"
        confidence="$(sql "SELECT confidence FROM $type WHERE slug='$slug'")"
        importance="$(sql "SELECT importance FROM $type WHERE slug='$slug'")"
        printf "  │ %-14s %s\n" "Status:" "$status"
        printf "  │ %-14s %s\n" "Confidence:" "$confidence"
        printf "  │ %-14s %s\n" "Importance:" "$importance"
    fi

    if [[ "$type" == "news" ]]; then
        local pub
        pub="$(sql "SELECT publication FROM $type WHERE slug='$slug'")"
        printf "  │ %-14s %s\n" "Publication:" "$pub"
    fi

    printf "  │ %-14s %s\n" "State:" "$state"
    echo "  └──────────────────────────────────────────"
    echo
}

###############################################################################
# field editing -- nnn/fzf mode (for -data)
###############################################################################

edit_tags_nnn() {
    local type="$1" slug="$2"

    local id
    id="$(sql "SELECT id FROM $type WHERE slug='$slug'")"

    echo
    echo "  Current tags:"
    sql "SELECT t.slug FROM tags t JOIN content_tags ct ON ct.tag_id = t.id WHERE ct.content_type = '$type' AND ct.content_id = $id ORDER BY t.slug" | while read -r tag; do
        echo "    - $tag"
    done

    echo
    echo "  Actions: [a]dd tag, [r]emove tag, [d]one"
    while true; do
        read -rp "  tag> " action
        case "$action" in
            a|add)
                local new_tag
                new_tag="$(
                    sql "SELECT slug FROM tags ORDER BY slug" |
                    fzf --prompt="add tag> " --height=20 --reverse
                )" || continue

                local existing
                existing="$(sql "SELECT COUNT(*) FROM content_tags WHERE content_type='$type' AND content_id=$id AND tag_id=(SELECT id FROM tags WHERE slug='$new_tag')")"
                if [[ "$existing" -gt 0 ]]; then
                    echo "  Tag '$new_tag' already linked."
                    continue
                fi

                local tag_id
                tag_id="$(sql "SELECT id FROM tags WHERE slug='$new_tag'")"
                sql "INSERT INTO content_tags (content_type, content_id, tag_id) VALUES ('$type', $id, $tag_id)"
                echo "  Added tag: $new_tag"
                ;;
            r|remove)
                local rm_tag
                rm_tag="$(
                    sql "SELECT t.slug FROM tags t JOIN content_tags ct ON ct.tag_id = t.id WHERE ct.content_type = '$type' AND ct.content_id = $id ORDER BY t.slug" |
                    fzf --prompt="remove tag> " --height=10 --reverse
                )" || continue

                sql "DELETE FROM content_tags WHERE content_type='$type' AND content_id=$id AND tag_id=(SELECT id FROM tags WHERE slug='$rm_tag')"
                echo "  Removed tag: $rm_tag"
                ;;
            d|done|q|quit)
                break
                ;;
            *)
                echo "  Unknown action. Use [a]dd, [r]emove, or [d]one."
                ;;
        esac
    done
}

edit_field_nnn() {
    local type="$1" slug="$2" field="$3"
    local new_value=""

    case "$field" in
        title|preview)
            local current
            current="$(sql "SELECT $field FROM $type WHERE slug='$slug'")"
            echo "  Current $field: $current"
            read -rp "  New $field: " new_value
            [[ -z "$new_value" ]] && return
            new_value="${new_value//\'/\'\'}"
            sql "UPDATE $type SET $field='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            echo "  Updated $field."
            ;;
        category_slug)
            new_value="$(
                sql "SELECT slug FROM categories ORDER BY slug" |
                fzf --prompt="category> " --height=20 --reverse
            )" || return
            sql "UPDATE $type SET category_slug='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            echo "  Updated category to: $new_value"
            ;;
        tags)
            edit_tags_nnn "$type" "$slug"
            ;;
        status)
            new_value="$(
                printf "%s\n" "${VALID_STATUSES[@]}" |
                fzf --prompt="status> " --height=10 --reverse
            )" || return
            sql "UPDATE $type SET status='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            echo "  Updated status to: $new_value"
            ;;
        confidence)
            new_value="$(
                printf "%s\n" "${VALID_CONFIDENCES[@]}" |
                fzf --prompt="confidence> " --height=12 --reverse
            )" || return
            sql "UPDATE $type SET confidence='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            echo "  Updated confidence to: $new_value"
            ;;
        importance)
            read -rp "  Importance (1-10): " new_value
            if [[ "$new_value" =~ ^[0-9]+$ ]] && (( new_value >= 1 && new_value <= 10 )); then
                sql "UPDATE $type SET importance=$new_value, updated_at=datetime('now') WHERE slug='$slug'"
                echo "  Updated importance to: $new_value"
            else
                echo "  Invalid. Must be 1-10."
            fi
            ;;
        publication)
            new_value="$(
                printf "%s\n" "${NEWS_PUBS[@]}" |
                fzf --prompt="publication> " --height=5 --reverse
            )" || return
            sql "UPDATE $type SET publication='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            echo "  Updated publication to: $new_value"
            ;;
        state)
            new_value="$(
                printf "%s\n" "${VALID_STATES[@]}" |
                fzf --prompt="state> " --height=5 --reverse
            )" || return
            sql "UPDATE $type SET state='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            echo "  Updated state to: $new_value"
            ;;
        *)
            echo "  Unknown field: $field"
            ;;
    esac
}

###############################################################################
# field editing -- dmenu mode (for -data)
###############################################################################

edit_field_dmenu() {
    local type="$1" slug="$2" field="$3"
    local current new_value

    case "$field" in
        title|preview)
            current="$(sql "SELECT $field FROM $type WHERE slug='$slug'")"
            new_value="$(echo "$current" | dmenu -p "$field:")"
            [[ -z "$new_value" ]] && return
            new_value="${new_value//\'/\'\'}"
            sql "UPDATE $type SET $field='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            notify "Updated $field"
            ;;
        category_slug)
            new_value="$(sql "SELECT slug FROM categories ORDER BY slug" | dmenu -i -l 15 -p "category:")"
            [[ -z "$new_value" ]] && return
            sql "UPDATE $type SET category_slug='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            notify "Category: $new_value"
            ;;
        tags)
            local id
            id="$(sql "SELECT id FROM $type WHERE slug='$slug'")"
            local action
            action="$(printf "add\nremove" | dmenu -i -p "tags:")"
            [[ -z "$action" ]] && return

            if [[ "$action" == "add" ]]; then
                local tag
                tag="$(sql "SELECT slug FROM tags ORDER BY slug" | dmenu -i -l 20 -p "add tag:")"
                [[ -z "$tag" ]] && return
                local tag_id
                tag_id="$(sql "SELECT id FROM tags WHERE slug='$tag'")"
                [[ -z "$tag_id" ]] && return
                sql "INSERT OR IGNORE INTO content_tags (content_type, content_id, tag_id) VALUES ('$type', $id, $tag_id)"
                notify "Added tag: $tag"
            else
                local tag
                tag="$(sql "SELECT t.slug FROM tags t JOIN content_tags ct ON ct.tag_id=t.id WHERE ct.content_type='$type' AND ct.content_id=$id ORDER BY t.slug" | dmenu -i -l 10 -p "remove:")"
                [[ -z "$tag" ]] && return
                sql "DELETE FROM content_tags WHERE content_type='$type' AND content_id=$id AND tag_id=(SELECT id FROM tags WHERE slug='$tag')"
                notify "Removed tag: $tag"
            fi
            ;;
        status)
            new_value="$(printf "Notes\nDraft\nIn Progress\nFinished\nAbandoned" | dmenu -i -l 5 -p "status:")"
            [[ -z "$new_value" ]] && return
            sql "UPDATE $type SET status='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            notify "Status: $new_value"
            ;;
        confidence)
            new_value="$(printf "certain\nhighly likely\nlikely\npossible\nunlikely\nhighly unlikely\nremote\nimpossible" | dmenu -i -l 8 -p "confidence:")"
            [[ -z "$new_value" ]] && return
            sql "UPDATE $type SET confidence='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            notify "Confidence: $new_value"
            ;;
        importance)
            new_value="$(seq 1 10 | dmenu -i -l 10 -p "importance:")"
            [[ -z "$new_value" ]] && return
            sql "UPDATE $type SET importance=$new_value, updated_at=datetime('now') WHERE slug='$slug'"
            notify "Importance: $new_value"
            ;;
        publication)
            new_value="$(printf '%s\n' "${NEWS_PUBS[@]}" | dmenu -i -l 3 -p "publication:")"
            [[ -z "$new_value" ]] && return
            sql "UPDATE $type SET publication='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            notify "Publication: $new_value"
            ;;
        state)
            new_value="$(printf "active\nhidden" | dmenu -i -l 2 -p "state:")"
            [[ -z "$new_value" ]] && return
            sql "UPDATE $type SET state='$new_value', updated_at=datetime('now') WHERE slug='$slug'"
            notify "State: $new_value"
            ;;
    esac
}

###############################################################################
# entry selection helpers
###############################################################################

select_entry_fzf() {
    local type="$1" filter_pub="${2:-}"

    local query="SELECT title || '  (' || slug || ')' || char(9) || slug FROM $type WHERE state != '' "
    [[ -n "$filter_pub" ]] && query+="AND publication = '$filter_pub' "
    query+="ORDER BY title"

    sql "$query" |
    fzf --prompt="$type> " --delimiter=$'\t' --with-nth=1 \
        --preview="sqlite3 '$DB' \"SELECT 'Title: ' || title || char(10) || 'Preview: ' || COALESCE(preview,'') || char(10) || 'Category: ' || COALESCE(category_slug,'') || char(10) || 'State: ' || COALESCE(state,'active') FROM $type WHERE slug='{2}'\"" \
        --height=80% --reverse |
    awk -F'\t' '{print $2}'
}

select_entry_nnn_browse() {
    local type="$1"
    local content_path="$CONTENT_DIR/$type"

    [[ ! -d "$content_path" ]] && { echo ""; return; }

    local tmpfile
    tmpfile="$(mktemp)"

    NNN_TMPFILE="$tmpfile" nnn -p "$tmpfile" "$content_path"

    if [[ -s "$tmpfile" ]]; then
        local selected
        selected="$(cat "$tmpfile")"
        rm -f "$tmpfile"
        basename "$selected" .mdx
    else
        rm -f "$tmpfile"
        echo ""
    fi
}

###############################################################################
# mode: -nnn -data
###############################################################################

run_nnn_data() {
    check_deps_nnn
    check_deps_data

    clear
    print_banner_data

    while true; do
        local selection
        selection="$(fzf_menu | fzf --prompt="data> " --height=20 --reverse)" || exit 0

        [[ "$selection" == "quit" ]] && exit 0

        selection="${selection## }"
        local type
        type="$(db_type_for "$selection")"

        # Determine publication filter for news
        local pub_filter=""
        case "$selection" in
            the-soapbox|field-notes|off-the-record) pub_filter="$selection" ;;
        esac

        local mode
        mode="$(
            printf "%s\n" "fzf (search by title)" "nnn (browse files)" "back" |
            fzf --prompt="select via> " --height=5 --reverse
        )" || continue

        [[ "$mode" == "back" ]] && continue

        local slug=""
        if [[ "$mode" == *"fzf"* ]]; then
            slug="$(select_entry_fzf "$type" "$pub_filter")" || continue
        else
            slug="$(select_entry_nnn_browse "$type")" || continue
        fi

        [[ -z "$slug" ]] && continue

        local count
        count="$(sql "SELECT COUNT(*) FROM $type WHERE slug='$slug'")"
        if [[ "$count" -eq 0 ]]; then
            echo "  Slug '$slug' not found in $type table."
            read -rp "  Press Enter..." _
            continue
        fi

        while true; do
            clear
            print_banner_data
            show_metadata "$type" "$slug"

            local fields
            if [[ "$type" == "diary" ]]; then
                fields=("${DIARY_FIELDS[@]}" "tags" "done")
            elif [[ "$type" == "news" ]]; then
                fields=("${STANDARD_FIELDS[@]}" "publication" "tags" "done")
            else
                fields=("${STANDARD_FIELDS[@]}" "tags" "done")
            fi

            local field
            field="$(
                printf "%s\n" "${fields[@]}" |
                fzf --prompt="edit field> " --height=14 --reverse
            )" || break

            [[ "$field" == "done" ]] && break

            edit_field_nnn "$type" "$slug" "$field"
            read -rp "  Press Enter..." _
        done
    done
}

###############################################################################
# mode: -dmenu -data
###############################################################################

run_dmenu_data() {
    check_deps_dmenu
    check_deps_data

    local raw
    raw="$(dmenu_menu | dmenu -i -l 16 -p "data:")"
    [[ -z "$raw" ]] && exit 0

    local selection
    selection="$(parse_selection "$raw")"
    [[ -z "$selection" ]] && exit 0

    local type
    type="$(db_type_for "$selection")"

    # Publication filter for news subs
    local pub_filter=""
    case "$selection" in
        the-soapbox|field-notes|off-the-record) pub_filter="$selection" ;;
    esac

    # Select entry
    local query="SELECT title || '  [' || slug || ']' FROM $type "
    [[ -n "$pub_filter" ]] && query+="WHERE publication = '$pub_filter' "
    query+="ORDER BY title"

    local slug
    slug="$(
        sql "$query" |
            dmenu -i -l 20 -p "$type:" |
            sed 's/.*\[\(.*\)\]/\1/'
    )"
    [[ -z "$slug" ]] && exit 0

    # Edit loop
    while true; do
        local fields="title
preview
category_slug
tags
state"

        if [[ "$type" != "diary" ]]; then
            fields+="
status
confidence
importance"
        fi

        if [[ "$type" == "news" ]]; then
            fields+="
publication"
        fi

        local field
        field="$(echo "$fields" | dmenu -i -l 12 -p "field:")"
        [[ -z "$field" ]] && break
        edit_field_dmenu "$type" "$slug" "$field"
    done
}

###############################################################################
# dispatch
###############################################################################

case "${UI_MODE}-${ACTION_MODE}" in
    nnn-create)   run_nnn_create ;;
    dmenu-create) run_dmenu_create ;;
    nnn-edit)     run_nnn_edit ;;
    dmenu-edit)   run_dmenu_edit ;;
    nnn-data)     run_nnn_data ;;
    dmenu-data)   run_dmenu_data ;;
esac

A .local/bin/write => .local/bin/write +2 -0
@@ 0,0 1,2 @@
#!/bin/sh
NVIM_APPNAME=write exec nvim "$@"

A .local/share/wallpapers/d1e2f1-horizontal.png => .local/share/wallpapers/d1e2f1-horizontal.png +0 -0
A .local/share/wallpapers/plan9-grey.png => .local/share/wallpapers/plan9-grey.png +0 -0
A .local/share/wallpapers/plan9.png => .local/share/wallpapers/plan9.png +0 -0
A .local/share/wallpapers/plan9theme.png => .local/share/wallpapers/plan9theme.png +0 -0
A .local/share/wallpapers/winter-color-scheme.png => .local/share/wallpapers/winter-color-scheme.png +0 -0
M .mkshrc => .mkshrc +14 -0
@@ 49,6 49,20 @@ alias top='btop'
alias htop='btop'
alias vim='nvim'

# Nvim profiles
alias write='NVIM_APPNAME=write nvim'
alias code='NVIM_APPNAME=code nvim'

# Corpus directories
alias content='cd $HOME/.corpus/content'
alias til='cd $HOME/.corpus/til'
alias sb='cd $HOME/.corpus/slipbox'

# Tome shortcuts
alias tc='tome -nnn -create'
alias te='tome -nnn -edit'
alias td='tome -nnn -data'

# Claude Code
alias claude='claude --dangerously-skip-permissions'


M .xprofile => .xprofile +20 -9
@@ 3,20 3,31 @@
# This file runs when a DM logs you into a graphical session.
# If you use startx/xinit like a Chad, this file will also be sourced.

# Load environment variables from login profile
. "$HOME/.profile"
# Add local bin to PATH for scripts
export PATH="$HOME/.local/bin:$HOME/.local/bin/statusbar:$PATH"

xrandr --dpi 128	# Set DPI. Scaled up for 1440p to match 1080p UI size.
# Auto-detect device type and set DPI accordingly
DEVICE_TYPE_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/srice/device-type"
if [ -f "$DEVICE_TYPE_FILE" ] && [ "$(cat "$DEVICE_TYPE_FILE")" = "laptop" ]; then
	xrandr --dpi 150	# Laptop HiDPI
else
	xrandr --dpi 128	# Desktop 1440p scaled
fi
~/.screenlayout/main.sh	# Set up monitor layout
setxkbmap -option caps:none	# Completely disable Caps Lock
xrdb -merge ${XDG_CONFIG_HOME:-$HOME/.config}/x11/xresources	# Load base Xresources
setbg -s			# set the background and colorscheme (sync, not background)
xset s off -dpms s noblank	# Disable DPMS/screensaver (freezes with dual-GPU + 4 monitors)
setbg -s -c "$HOME/.local/share/wallpapers/plan9theme.png" "$HOME/.local/share/wallpapers/plan9.png"	# plan9 solid bg + plan9 winter colorscheme
setfan &	# Set case fan RGB (ice blue)

autostart="mpd picom dunst unclutter pipewire"
# Start gnome-keyring-daemon with unlocked login keyring (needed for ProtonMail Bridge)
eval $(echo -n "kris" | gnome-keyring-daemon --replace --unlock --components=pkcs11,secrets,ssh)
export SSH_AUTH_SOCK

autostart="mpd picom dunst unclutter pipewire wireplumber conky protonmail-bridge"

for program in $autostart; do
	pidof -sx "$program" || "$program" &
	pgrep -x "$program" >/dev/null 2>&1 || "$program" &
done >/dev/null 2>&1

# Start conky (lean-conky-config)
pidof -sx conky || ~/.config/conky/lean-conky/start.sh &

# dwmblocks is started by dwm autostart