diff --git a/configuration.nix b/configuration.nix index 18496f7..a3fec40 100644 --- a/configuration.nix +++ b/configuration.nix @@ -12,6 +12,8 @@ ./nameserver.nix ./custom ./secrets + ./fish.nix + ./neovim.nix ]; ### State diff --git a/fish.nix b/fish.nix new file mode 100644 index 0000000..c94de4b --- /dev/null +++ b/fish.nix @@ -0,0 +1,212 @@ +{ ... }: + +{ + programs.fish.shellAbbrs = + { e = "nvim"; + l = "ls -lh"; + ip = "ip -c"; + iftop = "iftop -m 70M"; + }; + + programs.fish.shellAliases = + { namecoin-cli = "namecoin-cli -conf=$XDG_CONFIG_HOME/namecoin"; }; + + programs.fish.loginShellInit = + '' + # Start abduco in ssh + if set -q SSH_CLIENT + # start abduco on ssh + if not set -q ABDUCO_SESSION + exec abduco -A ssh fish + else if test $SHLVL -eq 1 + tput rmcup + end + end + ''; + + programs.fish.interactiveShellInit = + '' + ## Fish settings + + # mixed emacs/vi + fish_hybrid_key_bindings + + # kj to normal mode + bind -M insert kj " + if commandline -P; + commandline -f cancel; + else; + set fish_bind_mode default; + commandline -f backward-char repaint-mode; + end" + + # fix unquoted URLs + set -U fish_features ampersand-nobg-in-token qmark-noglob + # change default cursor + set fish_cursor_insert underscore + + + ## Color scheme + + # syntax highlighting + set fish_color_command green + set fish_color_param normal + set fish_color_comment brcyan + set fish_color_operator purple + set fish_color_escape bryellow + set fish_color_redirection blue + set fish_color_selection --background=black + + # completion/history + set fish_pager_color_prefix yellow + set fish_pager_color_description brblue + set fish_pager_color_progress brblack --background=black + set fish_color_search_match --background=black + + # man pages colors + # bold, blink stop + set -x LESS_TERMCAP_md (set_color -o bryellow) + set -x LESS_TERMCAP_mb (set_color -u magenta) + set -x LESS_TERMCAP_me (set_color normal) + # standout start/stop + set -x LESS_TERMCAP_so (set_color brblue -b black) + set -x LESS_TERMCAP_se (set_color normal -b normal) + # underline start/stop + set -x LESS_TERMCAP_us (set_color -u brmagenta) + set -x LESS_TERMCAP_ue (set_color normal) + + # used default LS_COLORS + eval (dircolors | sed 's/\(\w\+\)=/set \1 /') + + + ## Aliases + + # start process and detach + function start + nohup $argv > /dev/null 2>&1 &; disown + end + + # start process without network access + function nnet + unshare -nc fish -ic "$argv" + end + + set conf (systemctl cat namecoind | awk -F 'conf=| ' '/ExecStart/ {print $3}') + namecoin-cli -conf=$conf $argv + + # interactively rename files + function vimv + set tmp (mktemp --tmpdir -d vimv.XXX) + if set -q argv[1] + # directory listing + find $argv[1] -maxdepth 1 | sort | cat -n | tee $tmp/before > $tmp/after + else + # read from stdin + cat -n - | tee $tmp/before > $tmp/after + end + $EDITOR $tmp/after + # only print differing lines + awk ' + NR==FNR { line=$0; sub($1, "", line); sub(/\s+/, "", line); lines[$1]=line; next } + NR!=FNR { line=$0; sub($1, "", line); sub(/\s+/, "", line); + if (!($1 in lines)) { printf("rm -vr \"%s\"\n", line); next } + if (lines[$1] != line) printf("mv -vin \"%s\" \"%s\"\n", line, lines[$1]) + } + ' $tmp/after $tmp/before | sh + rm -r $tmp + end + ''; + + programs.fish.promptInit = + '' + # Outputs colored text + function color + set_color $argv[1] + for i in $argv[2..-1] + echo -n $i + end + set_color normal + end + + # Git branch info + function git_branch + if not test -f .git/HEAD + return + end + + set branch (git rev-parse --abbrev-ref HEAD 2> /dev/null) + if test $status -ne 0 + set branch (cut -f 3 -d '/' .git/HEAD) + else if test $branch = HEAD + set branch (head -c 10 .git/HEAD) + end + + timeout 0.1 git diff-files --quiet 2>/dev/null + if test $status -eq 1 + set branch (color yellow $branch'*') + else + set branch (color green $branch) + end + + timeout 0.1 git status --porcelain 2>/dev/null 1>| read untracked + if test -n "$untracked" + set dirty (color red '*') + end + + echo " <$branch$dirty>" + end + + # Left prompt + function fish_prompt + if fish_is_root_user + set prompt (color blue Λ) + set user (color red (whoami)) + else + set prompt (color blue λ) + set user (color green (whoami)) + end + + if test \( "$LINES" -lt 10 \) -o \( "$COLUMNS" -lt 30 \) + echo "$prompt " + return + end + + if set -q SSH_CLIENT + set host (color yellow (hostname)) + else + set host (color green (hostname)) + end + + set git (git_branch) + set path (color cyan (prompt_pwd)) + + switch $fish_bind_mode + case default + set mode (color blue n) + case insert + set mode (color red i) + case visual + set mode (color yellow v) + end + + echo "$user@$host $mode$path$git" + echo "$prompt " + end + + # Right prompt + function fish_right_prompt + set code $status + if test $code -ne 0 + set exitcode (color red $code" ⏎") + end + if string match -q '/nix*' $PATH[1] + set nix " <"(color blue nix)">" + end + echo "$exitcode$nix" + end + + function fish_mode_prompt; end + function fish_greeting; end + ''; + +} diff --git a/neovim.nix b/neovim.nix new file mode 100644 index 0000000..25af9c4 --- /dev/null +++ b/neovim.nix @@ -0,0 +1,479 @@ +{ config, lib, pkgs, ... }: + +let + frameline = pkgs.callPackage (pkgs.fetchFromGitea + { domain = "maxwell.ydns.eu/git"; + owner = "rnhmjoj"; + repo = "nvim-frameline"; + rev = "v0.1.0"; + sha256 = "PrTSSoXbu+qtTsJUv81z+MuTUmB1RHLPEWFQQnu6+J8="; + }) { }; + + plugins = with pkgs.vimPlugins; + [ # UI + undotree gitsigns-nvim + frameline nvim-fzf + + # Syntax + vim-pandoc-syntax + nix-queries + (nvim-treesitter.withPlugins (p: with p; + [ bash fish + html css markdown + nix python + ])) + + # Misc + vim-fugitive supertab neomake + auto-pairs plenary-nvim + ]; + + pack = pkgs.linkFarm "neovim-plugins" + (map (pkg: + { name = "pack/${pkg.name}/start/${pkg.name}"; + path = toString pkg; + }) (lib.concatMap (p: [p] ++ p.dependencies or []) plugins)); + + neovim-wrapped = pkgs.runCommand "${pkgs.neovim-unwrapped.name}" + { nativeBuildInputs = [ pkgs.makeWrapper ]; } + '' + mkdir -p "$out" + makeWrapper '${pkgs.neovim-unwrapped}/bin/nvim' "$out/bin/nvim" \ + --add-flags "-u ${conf}" + ''; + + nix-queries = pkgs.writeTextDir "/queries/nix/injections.scm" + '' + ; /*language*/ highlight (too slow) + ; ((comment) @language + ; [ + ; (string_expression (string_fragment) @content) + ; (indented_string_expression (string_fragment) @content) + ; ] + ; (#gsub! @language "/%*%s*([%w%p]+)%s*%*/" "%1")) + ; @combined + + ; writeText highlight + (apply_expression + function: + (apply_expression + function: (_) @_func + argument: (string_expression (string_fragment) @language)) + argument: [(string_expression (string_fragment) @content) + (indented_string_expression (string_fragment) @content)] + (#match? @_func "(^|\\.)writeText(Dir)?$") + (#gsub! @language ".*%.(.*)" "%1")) + @combined + ''; + + conf = pkgs.writeText "init.lua" '' + local opt = vim.opt + local cmd = vim.api.nvim_command + local keymap = vim.keymap.set + local autocmd = vim.api.nvim_create_autocmd + + -- Load plugins + opt.packpath = ${builtins.toJSON pack} + opt.runtimepath:prepend(${builtins.toJSON pack}) + + + -- + -- Options + -- + + local cache = os.getenv('XDG_CACHE_HOME')..'/nvim' + opt.directory = cache..'/tmp' + opt.backupdir = cache..'/tmp' + opt.shadafile = cache..'/shada' + opt.undodir = cache..'/undo' + + opt.hidden = true -- Hide buffers + opt.mouse = 'a' -- Enable mouse support + opt.ignorecase = true -- Case insensitive search... + opt.smartcase = true -- ...for lowercase terms + + opt.fsync = true -- Sync writes + opt.swapfile = false -- Disable swap files + opt.writebackup = true -- Backup file before overwriting... + opt.backup = false -- ...but delete it on success + opt.undofile = true -- Store all changes + + opt.modeline = false -- Disable for Security + opt.showmatch = true -- Highlight matched parenthesis + opt.clipboard = 'unnamedplus' -- Yank to clipboard + + -- Files to ignore + opt.wildignore = { + '*.so', '*.hi', '*.a', '*.la', '*.mod', + '*/__pycache__/*', + '*/dist/*', + '*/result/*', + '*/.git/*' + } + + opt.shiftwidth = 2 -- Tabs + opt.tabstop = 2 -- + opt.expandtab = true -- + + opt.number = true -- Line numbering + opt.smartindent = true -- Indentation + opt.showmode = false -- Disable printing of mode changes + opt.ruler = false -- Already in statusline + opt.fillchars = { + eob=' ', -- Hide ~ on empty lines + vert='│', -- make vertical split sign better + fold=' ', -- Hide . in fold markers + } + + opt.foldmethod = "expr" -- Folding + opt.foldexpr = 'nvim_treesitter#foldexpr()' -- + opt.foldlevel = 99 -- open by default + + -- + -- OSC 52 clipboard + -- + -- + + if os.getenv('DISPLAY') == nil then + function copy(sel) + return function(lines, _) + local data = vim.fn.system([[base64 -w0]], lines) + io.stdout:write('\027]52;'..sel..';'..data..'\a') + vim.g.fallback_clip = lines + end + end + + function paste(sel) + -- currently impossible to implement + return function() return {vim.g.fallback_clip, ""} end + end + + vim.g.clipboard = { + name='ssh', + copy={['*']=copy's', ['+']=copy'c'}, + paste={['*']=paste's', ['+']=paste'c'}, + } + end + + -- + -- Terminal mode + -- + + -- Hide some UI elements + autocmd('TermOpen', {pattern='*', command='setlocal nonumber'}) + autocmd('TermEnter', {pattern='*', command='set noshowcmd'}) + autocmd('TermLeave', {pattern='*', command='set showcmd'}) + + -- Exit without confirmation + autocmd('TermClose', {pattern='*', command='call feedkeys("\\")'}) + + keymap('t', '', '', {noremap=true}) -- Easier escape + keymap('n', '', '', {noremap=true}) -- + keymap('n', '-', ':split +term', {silent=true}) -- Tmux-like moves + keymap('n', '|', ':vsplit +term', {silent=true}) -- + keymap('n', 't', ':tabnew +term', {silent=true}) -- + keymap('n', 'c', ':quit', {silent=true}) -- + + -- + -- Keybindings + -- + + vim.g.mapleader = ',' + + function listToggle() + for _, win in ipairs(vim.fn.getwininfo()) do + if win.loclist == 1 then return cmd('lclose') end + end + cmd('lopen') + end + + fzf = require'fzf' + + function searchFiles() + local query = [[\( -name .git -o -name __pycache__ -o -path ./dist -o -path ./build \) -prune -o -type f]] + coroutine.wrap(function() + res = fzf.fzf('find '..query, "", {border='none'}) + cmd('edit '..res[1]) + end)() + end + + function searchCommands() + coroutine.wrap(function() + local history = {} + for i = 1, vim.fn.histnr("cmd") do + history[i] = vim.fn.histget("cmd", i) + end + res = fzf.fzf(history, "", {border='none'}) + vim.fn.feedkeys(':'..res[1], 't') + end)() + end + + keymap('n', '', searchFiles, {silent=true}) -- Fuzzy search files + keymap('n', '', searchCommands, {silent=true}) -- Fuzzy search command history + keymap('n', 'u', ':UndotreeToggle', {silent=true}) -- Toggle UndoTree + keymap('n', 'l', listToggle, {silent=true}) -- Toggle Neomake errors + + keymap('i', 'kj', '', {noremap=true}) -- Exit with kj + keymap('n', 'o', 'o', {noremap=true}) -- Add empty lines + keymap('n', 'O', 'O', {noremap=true}) -- + keymap('x', 'p', 'p:let @+=@0', {noremap=true, silent=true}) -- Keep selection after p + keymap('c', 'w!!', 'w !sudo tee >/dev/null %', {silent=true}) -- Save with sudo + + + -- + -- Colors + -- + + opt.bg = 'light' -- Use dark colors + + function color(group, args) + vim.api.nvim_set_hl(0, group, args) + end + + -- Source code + color('Cursor', {ctermfg=14}) + color('Keyword', {ctermfg=04}) + color('Define', {ctermfg=03}) + color('Type', {ctermfg=06}) + color('Identifier', {ctermfg=13}) + color('Constant', {ctermfg=03}) + color('Function', {ctermfg=02}) + color('Include', {ctermfg=04}) + color('Statement', {ctermfg=11}) + color('String', {ctermfg=03}) + color('Number', {ctermfg=04}) + color('Comment', {ctermfg=07}) + color('SpecialComment', {ctermfg=15}) + color('Operator', {ctermfg='none'}) + color('Conceal', {ctermfg='none', ctermbg='none'}) + + -- Text + color('Title', {ctermfg=4}) + color('Special', {ctermfg=12}) + color('Delimiter', {ctermfg=1}) + color('PandocReferenceURL', {ctermfg=3}) + color('PandocCiteKey', {ctermfg=6}) + color('PandocTableDelims', {ctermfg=8}) + color('texBeginEndName', {ctermfg=2}) + color('Error', {ctermfg='none', ctermbg='none', underline=true}) + + -- Editor UI + color('NonText', {ctermfg=0}) + color('LineNr', {ctermfg=8}) + color('Pmenu', {ctermfg=12, ctermbg=0}) + color('Folded', {ctermfg=7, ctermbg=0, cterm={}}) + color('VertSplit', {ctermfg=8, cterm={}}) + color('FoldColumn', {ctermfg='none', cterm={}}) + color('Visual', {ctermfg='none', ctermbg=0}) + color('Search', {ctermfg='none', ctermbg=0}) + + -- Diff mode + color('DiffAdd', {ctermfg=2, ctermbg=0, underline=true}) + color('DiffChange', {ctermfg=3, ctermbg=0, underline=true}) + color('DiffText', {ctermfg=1, ctermbg=0, underline=true}) + color('DiffDelete', {ctermfg=0, ctermbg=1, underline=true}) + + -- Spelling + color('SpellBad', {ctermfg=1, ctermbg=0, underline=true}) + color('SpellCap', {ctermfg=3, underline=true}) + + -- Neomake + color('NeomakeWarning', {ctermfg=3, underline=true}) + color('ErrorMsg', {ctermfg=1, ctermbg='none'}) + color('WarningMsg', {ctermfg=3}) + + -- Git signs + color('SignColumn', {ctermfg=1, ctermbg='none', cterm={}}) + color('GitAdd', {ctermfg=2}) + color('GitChange', {ctermfg=3}) + color('GitDelete', {ctermfg=1}) + + -- Statusline + color('StatusLine', {ctermfg=8, ctermbg=0, cterm={}}) + color('StatusLineNC', {ctermfg=4, ctermbg=0, cterm={}}) + color('User1', {ctermfg=8, ctermbg=0}) -- base + color('User2', {ctermfg=3, ctermbg=0}) -- location + color('StatusLineErr', {ctermfg=0, ctermbg=1}) + color('StatusLineWarn', {ctermfg=0, ctermbg=3}) + color('TablineTab', {ctermfg=8, ctermbg=0}) + color('TablineTabCur', {ctermfg=7, ctermbg=8}) + color('ModeNormal', {ctermfg=7, ctermbg=14}) + color('ModeInsert', {ctermfg=7, ctermbg=1}) + color('ModeCommand', {ctermfg=7, ctermbg=4}) + color('ModeVisual', {ctermfg=7, ctermbg=3}) + color('ModeVLine', {ctermfg=7, ctermbg=11}) + color('ModeVBloc', {ctermfg=7, ctermbg=11}) + color('ModeTerm', {ctermfg=7, ctermbg=2}) + + -- + -- Plugin options + -- + + -- Neomake + vim.call('neomake#configure#automake', 'nwr', 750) + vim.g.neomake_warning_sign = {text='W→', texthl='WarningMsg'} + vim.g.neomake_error_sign = {text='E→', texthl='ErrorMsg'} + vim.g.neomake_highlight_lines = 1 + vim.g.neomake_virtualtext_current_error = 0 + + -- Pandoc Markdown + autocmd({'BufNewFile', 'BufFilePre', 'BufRead'}, + {pattern='*.md', command='set filetype=markdown.pandoc'}) + + -- Git signs + require'gitsigns'.setup{ + signs={ + add = {hl='GitAdd' , text='+'}, + change = {hl='GitChange', text='δ'}, + delete = {hl='GitDelete', text='-'}, + topdelete = {hl='GitDelete', text='‾'}, + changedelete = {hl='GitChange', text='~'}, + }, + keymaps={ + ['n gb'] = ':lua require"gitsigns".blame_line(true)', + ['n gp'] = ':lua require"gitsigns".preview_hunk()', + } + } + + -- Tree-sitter + require'nvim-treesitter.configs'.setup{ + highlight={enable=true}, + indent={enable=true}, + } + + -- Non built-in filetypes + autocmd({'BufNewFile', 'BufRead'}, + {pattern='*.nix', command='setlocal filetype=nix'}) + + -- + -- Statusline + -- + -- + + local frameline = require 'frameline' + local utils = frameline.utils + + function mode(win, buf) + if not win.is_active then return end + + local k = vim.api.nvim_get_mode().mode + local modes = { + n={'Normal', 'Normal'}, + i={'Insert', 'Insert'}, + R={'Replas', 'Insert'}, + v={'Visual', 'Visual'}, + V={'V⋅Line', 'VLine'}, + t={'Termin', 'Term'}, + ['']={'V⋅Bloc', 'VBloc'} + } + return utils.highlight('Mode'..modes[k][2], ' '..modes[k][1]..' ') + end + + function branch(_, buf) + local head = vim.fn.FugitiveHead() + if buf.modifiable and head ~= "" then return '⚑ '..head end + end + + function filename(_, buf) + local delta = "" + if buf.modified then delta = 'Δ' end + if not buf.modifiable then delta = '∇' end + local fname = buf.name ~= "" and utils.filename or 'new-file' + return delta..fname + end + + function neomake() + local res, msg = vim.call('neomake#statusline#LoclistCounts'), "" + if res.E then + msg = msg..utils.highlight('StatusLineErr', ' '..res.E..'E ') + end + if res.W then + msg = msg..utils.highlight('StatusLineWarn', ' '..res.W..'W ') + end + return msg + end + + function readonly(_, buf) + if buf.modifiable and buf.readonly then return '∅' end + end + + function encoding(_, buf) + return buf.fileencoding ~= "" and buf.fileencoding or nil + end + + function filetype(_, buf) + return buf.filetype ~= "" and buf.filetype or 'no ft' + end + + -- Tabline + frameline.setup_tabline(function() + local segments = {} + local api = vim.api + local color = '%#StatusLine#' + + -- Tabs + local current = api.nvim_get_current_tabpage() + local label = ' %d %s ' + for i, tab in pairs(api.nvim_list_tabpages()) do + -- tab -> active win -> active buf -> name + local active_buf = api.nvim_win_get_buf(api.nvim_tabpage_get_win(tab)) + local name = api.nvim_buf_get_name(active_buf) + name = vim.fn.fnamemodify(name, ':t') -- filename only + + local group = tab == current and 'TablineTabCur' or 'TablineTab' + table.insert(segments, utils.highlight(group, label:format(i, name))) + end + + table.insert(segments, color) + table.insert(segments, utils.split) + + -- Current date + table.insert(segments, vim.fn.strftime("%a %H:%M")) + + -- Battery level + local level = io.popen('cat /sys/class/power_supply/BAT*/capacity'):read() + local symbol = ' ∘ '..utils.highlight('User2', '⚡')..color + local battery = level and symbol..level..'%% ' or "" + table.insert(segments, battery) + + return table.concat(segments) + end) + + -- Statusline + frameline.setup_statusline(function() + local segments = {} + + -- Left section + table.insert(segments, utils.subsection{items={mode}}) + table.insert(segments, utils.subsection{ + separator=' → ', + items={branch, filename, readonly}, + }) + table.insert(segments, utils.split) + + -- Right section + table.insert(segments, utils.subsection{items={neomake}}) + table.insert(segments, utils.subsection{ + user=2, + separator=':', stop=' ', + items={utils.line_number, utils.column_number}, + }) + table.insert(segments, utils.subsection{ + user=1, + separator=' ∘ ', + items={utils.percent, encoding, filetype} + }) + + return segments + end) + ''; +in + +{ + + # nix build -f '' pkgs.neovim for testing + nixpkgs.overlays = lib.singleton (self: super: { + neovim = neovim-wrapped; + }); + +} diff --git a/packages.nix b/packages.nix index ed827af..187b3eb 100644 --- a/packages.nix +++ b/packages.nix @@ -17,13 +17,16 @@ jq ack sshfs abduco # backup - bup git nfs-utils + bup git # admin dnsutils matrix-synapse maxwell-notify smartmontools + + # namecoin + namecoin haskellPackages.rosa ]; }