commit 2a3ea3149114bfb04492a441d765800ebe09273a Author: Louis Chih-Ming Lee Date: Mon Jan 26 01:16:35 2026 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6df0f03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.txt +*.key +*.age diff --git a/.sops.yaml b/.sops.yaml new file mode 100644 index 0000000..8c6b894 --- /dev/null +++ b/.sops.yaml @@ -0,0 +1,10 @@ +keys: + - &local_T14p "age1y38cnfvt42ar4seqx8n2cw0ynwls2rdl6jyzhv9wuv6jcfqrwckqwlh9p0" + - &server_hetzner "age1sjq27nh4znq75fzzx6epwd6700wgg23v9xenzvmam4kmncvjdcesakv9e2" + +creation_rules: + - path_regex: secrets.yaml$ + key_groups: + - age: + - *local_T14p + - *server_hetzner diff --git a/apps/fuzzel/default.nix b/apps/fuzzel/default.nix new file mode 100644 index 0000000..c3be95c --- /dev/null +++ b/apps/fuzzel/default.nix @@ -0,0 +1,29 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.my.apps.fuzzel; +in +{ + options.my.apps.fuzzel = { + enable = lib.mkEnableOption "Fuzzel launcher"; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.fuzzel; + defaultText = "pkgs.fuzzel"; + description = "Fuzzel package to install."; + }; + }; + + config = lib.mkIf cfg.enable { + home.packages = [ + cfg.package + pkgs.jetbrains-mono + ]; + + xdg.enable = true; + + # fuzzel uses ~/.config/fuzzel/fuzzel.ini + xdg.configFile."fuzzel/fuzzel.ini".source = ./fuzzel.ini; + }; +} diff --git a/apps/fuzzel/fuzzel.ini b/apps/fuzzel/fuzzel.ini new file mode 100644 index 0000000..0e3bd99 --- /dev/null +++ b/apps/fuzzel/fuzzel.ini @@ -0,0 +1,41 @@ +# Fuzzel config +# Catppuccin Mocha-ish palette + Emacs navigation keys + +[main] +font=JetBrains Mono:size=14 +dpi-aware=yes +prompt=Search: +terminal=foot +layer=overlay + +# Window sizing +width=45 +lines=8 +horizontal-pad=20 +vertical-pad=16 +inner-pad=10 + +[colors] +# Catppuccin Mocha palette +background=1e1e2eff +text=cdd6f4ff +match=cba6f7ff +selection=585b70ff +selection-text=f5e0dcff +border=cba6f7ff + +[border] +width=2 +radius=12 + +[key-bindings] +# List navigation +next=Control+n +prev=Control+p + +# Optional extras +next-page=Page_Down +prev-page=Page_Up +accept=Return +cancel=Escape + diff --git a/apps/neovim/autopairs.lua b/apps/neovim/autopairs.lua new file mode 100644 index 0000000..ba62d44 --- /dev/null +++ b/apps/neovim/autopairs.lua @@ -0,0 +1 @@ +require("nvim-autopairs").setup {} diff --git a/apps/neovim/catppuccin.lua b/apps/neovim/catppuccin.lua new file mode 100644 index 0000000..4e546b9 --- /dev/null +++ b/apps/neovim/catppuccin.lua @@ -0,0 +1,76 @@ +require("catppuccin").setup({ + flavour = "auto", -- latte, frappe, macchiato, mocha + background = { -- :h background + light = "latte", + dark = "mocha", + }, + transparent_background = false, -- disables setting the background color. + float = { + transparent = false, -- enable transparent floating windows + solid = false, -- use solid styling for floating windows, see |winborder| + }, + show_end_of_buffer = false, -- shows the '~' characters after the end of buffers + term_colors = false, -- sets terminal colors (e.g. `g:terminal_color_0`) + dim_inactive = { + enabled = false, -- dims the background color of inactive window + shade = "dark", + percentage = 0.15, -- percentage of the shade to apply to the inactive window + }, + no_italic = false, -- Force no italic + no_bold = false, -- Force no bold + no_underline = false, -- Force no underline + styles = { -- Handles the styles of general hi groups (see `:h highlight-args`): + comments = { "italic" }, -- Change the style of comments + conditionals = { "italic" }, + loops = {}, + functions = {}, + keywords = {}, + strings = {}, + variables = {}, + numbers = {}, + booleans = {}, + properties = {}, + types = {}, + operators = {}, + -- miscs = {}, -- Uncomment to turn off hard-coded styles + }, + lsp_styles = { -- Handles the style of specific lsp hl groups (see `:h lsp-highlight`). + virtual_text = { + errors = { "italic" }, + hints = { "italic" }, + warnings = { "italic" }, + information = { "italic" }, + ok = { "italic" }, + }, + underlines = { + errors = { "underline" }, + hints = { "underline" }, + warnings = { "underline" }, + information = { "underline" }, + ok = { "underline" }, + }, + inlay_hints = { + background = true, + }, + }, + color_overrides = {}, + custom_highlights = {}, + default_integrations = true, + auto_integrations = false, + integrations = { + cmp = true, + gitsigns = true, + nvimtree = true, + notify = false, + mini = { + enabled = true, + indentscope_color = "", + }, + telescope = { + enabled = true, + }, + lsp_trouble = true, + }, +}) + +vim.cmd.colorscheme "catppuccin" diff --git a/apps/neovim/cmp.lua b/apps/neovim/cmp.lua new file mode 100644 index 0000000..3e9fe1c --- /dev/null +++ b/apps/neovim/cmp.lua @@ -0,0 +1,86 @@ +-- https://github.com/hrsh7th/nvim-cmp +local cmp = require'cmp' + +cmp.setup({ + snippet = { + -- REQUIRED - you must specify a snippet engine + expand = function(args) + vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users. + -- require('luasnip').lsp_expand(args.body) -- For `luasnip` users. + -- require('snippy').expand_snippet(args.body) -- For `snippy` users. + -- vim.fn["UltiSnips#Anon"](args.body) -- For `ultisnips` users. + -- vim.snippet.expand(args.body) -- For native neovim snippets (Neovim v0.10+) + + -- For `mini.snippets` users: + -- local insert = MiniSnippets.config.expand.insert or MiniSnippets.default_insert + -- insert({ body = args.body }) -- Insert at cursor + -- cmp.resubscribe({ "TextChangedI", "TextChangedP" }) + -- require("cmp.config").set_onetime({ sources = {} }) + end, + }, + window = { + completion = cmp.config.window.bordered(), + documentation = cmp.config.window.bordered(), + }, + mapping = cmp.mapping.preset.insert({ + [''] = cmp.mapping.scroll_docs(-4), + [''] = cmp.mapping.scroll_docs(4), + [''] = cmp.mapping.complete(), + [''] = cmp.mapping.abort(), + [''] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items. + }), + sources = cmp.config.sources({ + { name = 'nvim_lsp' }, + { name = 'vsnip' }, -- For vsnip users. + -- { name = 'luasnip' }, -- For luasnip users. + -- { name = 'ultisnips' }, -- For ultisnips users. + -- { name = 'snippy' }, -- For snippy users. + }, { + { name = 'buffer' }, + }) +}) + +-- To use git you need to install the plugin petertriho/cmp-git and uncomment lines below +-- Set configuration for specific filetype. +--[[ cmp.setup.filetype('gitcommit', { + sources = cmp.config.sources({ + { name = 'git' }, + }, { + { name = 'buffer' }, + }) +) +require("cmp_git").setup() ]]-- + +-- Use buffer source for `/` and `?` (if you enabled `native_menu`, this won't work anymore). +cmp.setup.cmdline({ '/', '?' }, { + mapping = cmp.mapping.preset.cmdline(), + sources = { + { name = 'buffer' } + } +}) + +-- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore). +cmp.setup.cmdline(':', { + mapping = cmp.mapping.preset.cmdline(), + sources = cmp.config.sources({ + { name = 'path' } + }, { + { name = 'cmdline' } + }), + matching = { disallow_symbol_nonprefix_matching = false } +}) + +-- Set up lspconfig. +local capabilities = require('cmp_nvim_lsp').default_capabilities() + +function config_and_enable_lsp(language, extra_config) + --lsp_config = { capabilities = capabilities } + --vim.lsp.config(language, lsp_config) + vim.lsp.enable(language) +end + +vim.lsp.config('*', { + capabilities = capabilities +}) + + diff --git a/apps/neovim/default.nix b/apps/neovim/default.nix new file mode 100644 index 0000000..3ba297f --- /dev/null +++ b/apps/neovim/default.nix @@ -0,0 +1,113 @@ +{ config, pkgs, inputs, my-r,... }: +let + nvim-config-store = builtins.filterSource (path: type: baseNameOf path != "default.nix") ./.; + nvim-config-files = builtins.attrNames (builtins.readDir nvim-config-store); + nvim-config-lis = map (path: builtins.readFile "${nvim-config-store}/${path}") (nvim-config-files); + nvim-config = builtins.concatStringsSep "\n" nvim-config-lis; + +in +{ + programs.neovim = { + enable = true; + defaultEditor = true; + + plugins = with pkgs.vimPlugins; [ + nvim-treesitter.withAllGrammars + nvim-lspconfig + telescope-nvim + nvim-cmp + cmp-nvim-lsp + nvim-tree-lua + which-key-nvim + trouble-nvim + kanagawa-nvim + lazygit-nvim + quarto-nvim + otter-nvim + otter-nvim + image-nvim + molten-nvim + toggleterm-nvim + markdown-preview-nvim + lualine-nvim + gitsigns-nvim + indent-blankline-nvim + guess-indent-nvim + nvim-autopairs + catppuccin-nvim + ]; + + extraLuaConfig = nvim-config; + + + extraPackages = with pkgs; [ + #quarto + pyright + ripgrep + lua-language-server + rust-analyzer + lazygit + tectonic + imagemagick + jupyter + pnglatex + nil + nixfmt + + rustc + cargo + rustfmt + clippy + rust-analyzer + ]; + + extraLuaPackages = p: with p; [ + magick + ]; + + + extraPython3Packages = ps: with ps; [ + pynvim + jupyter-client + cairosvg + pnglatex + plotly + pyperclip + ipython + nbformat + ]; + + + + }; + + xdg.dataFile."jupyter/kernels/ir-nix/kernel.json".text = '' + { + "argv": [ + "${my-r.env}/bin/R", + "--slave", + "-e", + "IRkernel::main()", + "--args", + "{connection_file}" + ], + "display_name": "R (nix)", + "language": "R" + } + ''; + + home.file.".Rprofile".text = '' +graphics::par(bg = "white") +options( + jupyter.rich_display = TRUE, + #jupyter.display_mimetypes = c("text/plain", "image/png", "text/latex"), + jupyter.display_mimetypes = c("text/plain", "image/png"), + jupyter.plot_mimetypes = "image/png" +) + ''; + + #sessionVariables = { + # EDITOR = "nvim"; + # VISUAL = "nvim"; + #}; +} diff --git a/apps/neovim/diagnostic.lua b/apps/neovim/diagnostic.lua new file mode 100644 index 0000000..474ec65 --- /dev/null +++ b/apps/neovim/diagnostic.lua @@ -0,0 +1,21 @@ +local sign = function(opts) + vim.fn.sign_define(opts.name, { + texthl = opts.name, + text = opts.text, + numhl = '' + }) +end + +sign({name = 'DiagnosticSignError', text = '✘'}) +sign({name = 'DiagnosticSignWarn', text = '▲'}) +sign({name = 'DiagnosticSignHint', text = '⚑'}) +sign({name = 'DiagnosticSignInfo', text = '»'}) + +vim.diagnostic.config({ + virtual_text = false, + severity_sort = true, + float = { + border = 'rounded', + source = 'always', + }, +}) diff --git a/apps/neovim/gitsigns.lua b/apps/neovim/gitsigns.lua new file mode 100644 index 0000000..c54dac8 --- /dev/null +++ b/apps/neovim/gitsigns.lua @@ -0,0 +1,66 @@ +require('gitsigns').setup{ + on_attach = function(bufnr) + local gitsigns = require('gitsigns') + + local function map(mode, l, r, opts) + opts = opts or {} + opts.buffer = bufnr + vim.keymap.set(mode, l, r, opts) + end + + -- Navigation + map('n', ']c', function() + if vim.wo.diff then + vim.cmd.normal({']c', bang = true}) + else + gitsigns.nav_hunk('next') + end + end) + + map('n', '[c', function() + if vim.wo.diff then + vim.cmd.normal({'[c', bang = true}) + else + gitsigns.nav_hunk('prev') + end + end) + + -- Actions + map('n', 'hs', gitsigns.stage_hunk) + map('n', 'hr', gitsigns.reset_hunk) + + map('v', 'hs', function() + gitsigns.stage_hunk({ vim.fn.line('.'), vim.fn.line('v') }) + end) + + map('v', 'hr', function() + gitsigns.reset_hunk({ vim.fn.line('.'), vim.fn.line('v') }) + end) + + map('n', 'hS', gitsigns.stage_buffer) + map('n', 'hR', gitsigns.reset_buffer) + map('n', 'hp', gitsigns.preview_hunk) + map('n', 'hi', gitsigns.preview_hunk_inline) + + map('n', 'hb', function() + gitsigns.blame_line({ full = true }) + end) + + map('n', 'hd', gitsigns.diffthis) + + map('n', 'hD', function() + gitsigns.diffthis('~') + end) + + map('n', 'hQ', function() gitsigns.setqflist('all') end) + map('n', 'hq', gitsigns.setqflist) + + -- Toggles + map('n', 'tb', gitsigns.toggle_current_line_blame) + map('n', 'tw', gitsigns.toggle_word_diff) + + + -- Text object + map({'o', 'x'}, 'ih', gitsigns.select_hunk) + end +} diff --git a/apps/neovim/guess-indent.lua b/apps/neovim/guess-indent.lua new file mode 100644 index 0000000..f0523fd --- /dev/null +++ b/apps/neovim/guess-indent.lua @@ -0,0 +1 @@ +require("guess-indent").setup {} diff --git a/apps/neovim/ibl.lua b/apps/neovim/ibl.lua new file mode 100644 index 0000000..c605fed --- /dev/null +++ b/apps/neovim/ibl.lua @@ -0,0 +1,10 @@ +require("ibl").setup { + indent = { + char = "", + highlight = "IblIndent", + } +} +--│, ┆, ⋅, ╎ + +-- Use very subtle color +vim.api.nvim_set_hl(0, "IblIndent", { fg = "#3a3a3a" }) diff --git a/apps/neovim/init.lua b/apps/neovim/init.lua new file mode 100644 index 0000000..b7f40dc --- /dev/null +++ b/apps/neovim/init.lua @@ -0,0 +1,61 @@ +vim.g.mapleader = ' ' +vim.g.maplocalleader = ' ' + +vim.g.have_nerd_font = true + +vim.o.number = true + +vim.o.mouse = 'a' + +--vim.o.showmode = false + +vim.schedule(function() + vim.o.clipboard = 'unnamedplus' +end) + + +vim.o.breakindent = true +vim.o.undofile = true + +vim.o.ignorecase = true +vim.o.smartcase = true + +vim.o.signcolumn = 'yes' + +vim.o.colorcolumn = "80" + +vim.o.updatetime = 250 + +vim.o.timeoutlen = 300 + +vim.o.splitright = true +vim.o.splitbelow = true + +vim.o.list = true +vim.opt.listchars = { tab = '» ', trail = '·', nbsp = '␣' } + +vim.o.inccommand = 'split' + +vim.o.cursorline = true + +vim.o.scrolloff = 15 + +vim.o.confirm = true + +vim.keymap.set('i', 'jk', '') +vim.keymap.set('i', 'kj', '') +--vim.keymap.set('i', 'jf', '') + + +-- See `:help wincmd` for a list of all window commands +vim.keymap.set('n', '', '', { desc = 'Move focus to the left window' }) +vim.keymap.set('n', '', '', { desc = 'Move focus to the right window' }) +vim.keymap.set('n', '', '', { desc = 'Move focus to the lower window' }) +vim.keymap.set('n', '', '', { desc = 'Move focus to the upper window' }) + + + +vim.o.tabstop = 2 +vim.o.smartindent = true +vim.o.shiftwidth = 2 +vim.o.expandtab = true diff --git a/apps/neovim/lazygit.lua b/apps/neovim/lazygit.lua new file mode 100644 index 0000000..84b4c09 --- /dev/null +++ b/apps/neovim/lazygit.lua @@ -0,0 +1,14 @@ +vim.g.lazygit_floating_window_winblend = 0 -- transparency of floating window +vim.g.lazygit_floating_window_scaling_factor = 0.9 -- scaling factor for floating window +vim.g.lazygit_floating_window_border_chars = {'╭','─', '╮', '│', '╯','─', '╰', '│'} -- customize lazygit popup window border characters +vim.g.lazygit_floating_window_use_plenary = 0 -- use plenary.nvim to manage floating window if available +vim.g.lazygit_use_neovim_remote = 1 -- fallback to 0 if neovim-remote is not installed + +vim.g.lazygit_use_custom_config_file_path = 0 -- config file path is evaluated if this value is 1 +vim.g.lazygit_config_file_path = '' -- custom config file path +-- OR +vim.g.lazygit_config_file_path = {} -- table of custom config file paths + +vim.g.lazygit_on_exit_callback = nil -- optional function callback when exiting lazygit (useful for example to refresh some UI elements after lazy git has made some changes) + +vim.keymap.set("n", "gg", ":LazyGit", { desc = "LazyGit" }) diff --git a/apps/neovim/lsp.lua b/apps/neovim/lsp.lua new file mode 100644 index 0000000..8dd7903 --- /dev/null +++ b/apps/neovim/lsp.lua @@ -0,0 +1,101 @@ +local lspconfig = require('lspconfig') + +vim.lsp.config('lua_ls', { cmd = { 'lua-language-server' }, + settings = { + Lua = { + runtime = { + version = 'LuaJIT', + path = vim.split(package.path, ';'), + }, + diagnostics = { + globals = { 'vim' }, + }, + }, + }, +}) + +vim.lsp.config('rust_analyzer', { + settings = { + ['rust-analyzer'] = { + diagnostics = { + enable = true; + }; + }; + } +}) + + + +local languages = { + 'rust_analyzer', + 'r_language_server', + 'lua_ls', + 'quarto', + 'nil_ls', + 'pyright' +} + +for _, language in ipairs(languages) do + vim.lsp.enable(language) +end + +vim.diagnostic.config { + severity_sort = true, + float = { border = 'rounded', source = 'if_many' }, + underline = { severity = vim.diagnostic.severity.ERROR }, + signs = vim.g.have_nerd_font and { + text = { + [vim.diagnostic.severity.ERROR] = '󰅚 ', + [vim.diagnostic.severity.WARN] = '󰀪 ', + [vim.diagnostic.severity.INFO] = '󰋽 ', + [vim.diagnostic.severity.HINT] = '󰌶 ', + }, + } or {}, + virtual_text = { + source = 'if_many', + spacing = 2, + format = function(diagnostic) + local diagnostic_message = { + [vim.diagnostic.severity.ERROR] = diagnostic.message, + [vim.diagnostic.severity.WARN] = diagnostic.message, + [vim.diagnostic.severity.INFO] = diagnostic.message, + [vim.diagnostic.severity.HINT] = diagnostic.message, + } + return diagnostic_message[diagnostic.severity] + end, + }, +} + +vim.api.nvim_create_autocmd("LspAttach", { + callback = function(event) + local buf = event.buf + local opts = function(desc) + return { buffer = buf, desc = desc } + end + + -- Jumping + finding + vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts("[G]oto Definition")) + vim.keymap.set("n", "gD", vim.lsp.buf.declaration, opts("Goto [D]eclaration")) + vim.keymap.set("n", "gi", vim.lsp.buf.implementation, opts("[G]oto [I]mplementation")) + vim.keymap.set("n", "gr", vim.lsp.buf.references, opts("[G]oto [R]eferences")) + + -- Info + documentation + vim.keymap.set("n", "K", vim.lsp.buf.hover, opts("Hover Documentation")) + vim.keymap.set("n", "", vim.lsp.buf.signature_help, opts("Signature Help")) + + -- Actions + rename + vim.keymap.set("n", "rn", vim.lsp.buf.rename, opts("[R]e[n]ame")) + vim.keymap.set({ "n", "v" }, "ca", vim.lsp.buf.code_action, opts("[C]ode [A]ction")) + + -- Diagnostics navigation + vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, opts("Prev Diagnostic")) + vim.keymap.set("n", "]d", vim.diagnostic.goto_next, opts("Next Diagnostic")) + vim.keymap.set("n", "e", vim.diagnostic.open_float, opts("Show Error")) + vim.keymap.set("n", "q", vim.diagnostic.setloclist, opts("Diagnostic Quickfix")) + + -- Formatting + vim.keymap.set("n", "f", function() + vim.lsp.buf.format({ async = true }) + end, opts("[F]ormat Buffer")) + end, +}) diff --git a/apps/neovim/lua-line.lua b/apps/neovim/lua-line.lua new file mode 100644 index 0000000..76ad3f6 --- /dev/null +++ b/apps/neovim/lua-line.lua @@ -0,0 +1,54 @@ +require('lualine').setup { + options = { + icons_enabled = true, + theme = 'auto', + component_separators = { left = '', right = ''}, + section_separators = { left = '', right = ''}, + disabled_filetypes = { + statusline = {}, + winbar = {}, + }, + ignore_focus = {}, + always_divide_middle = true, + always_show_tabline = true, + globalstatus = false, + refresh = { + statusline = 1000, + tabline = 1000, + winbar = 1000, + refresh_time = 16, -- ~60fps + events = { + 'WinEnter', + 'BufEnter', + 'BufWritePost', + 'SessionLoadPost', + 'FileChangedShellPost', + 'VimResized', + 'Filetype', + 'CursorMoved', + 'CursorMovedI', + 'ModeChanged', + }, + } + }, + sections = { + lualine_a = {'mode'}, + lualine_b = {'branch', 'diff', 'diagnostics'}, + lualine_c = {'filename'}, + lualine_x = {'encoding', 'fileformat', 'filetype'}, + lualine_y = {'progress'}, + lualine_z = {'location'} + }, + inactive_sections = { + lualine_a = {}, + lualine_b = {}, + lualine_c = {'filename'}, + lualine_x = {'location'}, + lualine_y = {}, + lualine_z = {} + }, + tabline = {}, + winbar = {}, + inactive_winbar = {}, + extensions = {} +} diff --git a/apps/neovim/molten.lua b/apps/neovim/molten.lua new file mode 100644 index 0000000..e6a46cb --- /dev/null +++ b/apps/neovim/molten.lua @@ -0,0 +1,48 @@ +vim.g.molten_auto_open_output = true +vim.g.molten_wrap_output = true +vim.g.molten_output_show_exec_time = true +vim.g.molten_output_win_hide_on_leave = false +vim.g.molten_use_border_highlights = true +vim.g.molten_virt_text_output = true +vim.g.molten_image_provider = "image.nvim" +vim.g.molten_image_location = "both" +vim.g.molten_output_virt_lines = true +vim.g.molten_cover_empty_lines = true +vim.g.molten_output_truncate = "bottom" +vim.g.molten_virt_text_max_lines = 20 +--vim.g.molten_output = "pane" + +vim.api.nvim_set_hl(0, "NormalFloat", { bg = "#1e1e2e" }) +vim.api.nvim_set_hl(0, "FloatBorder", { bg = "#1e1e2e" }) +-- vim.api.nvim_set_hl(0, "MoltenVirtualText", { +-- bg = "#ae0606", +-- fg = "#ffd787", +-- italic = true, +-- }) + +require("image").setup({ + editor = { + -- force a visible background for plots + background = "white", -- or "#ffffff" or "black" + inline = { + enable = true, + }; + }, +}) + +vim.keymap.set("n", "mi", ":MoltenInit", + { silent = true, desc = "Initialize the plugin" }) +vim.keymap.set("n", "e", ":MoltenEvaluateOperator", + { silent = true, desc = "run operator selection" }) +vim.keymap.set("n", "rl", ":MoltenEvaluateLine", + { silent = true, desc = "evaluate line" }) +vim.keymap.set("n", "rr", ":MoltenReevaluateCell", + { silent = true, desc = "re-evaluate cell" }) +vim.keymap.set("v", "r", ":MoltenEvaluateVisualgv", + { silent = true, desc = "evaluate visual selection" }) +vim.keymap.set("n", "rd", ":MoltenDelete", + { silent = true, desc = "molten delete cell" }) +vim.keymap.set("n", "oh", ":MoltenHideOutput", + { silent = true, desc = "hide output" }) +vim.keymap.set("n", "os", ":noautocmd MoltenEnterOutput", + { silent = true, desc = "show/enter output" }) diff --git a/apps/neovim/nvim-tree.lua b/apps/neovim/nvim-tree.lua new file mode 100644 index 0000000..1219166 --- /dev/null +++ b/apps/neovim/nvim-tree.lua @@ -0,0 +1,25 @@ +-- disable netrw at the very start of your init.lua +vim.g.loaded_netrw = 1 +vim.g.loaded_netrwPlugin = 1 + +-- optionally enable 24-bit colour +vim.opt.termguicolors = true + + +-- OR setup with some options +require("nvim-tree").setup({ + sort = { + sorter = "case_sensitive", + }, + view = { + width = 30, + }, + renderer = { + group_empty = true, + }, + filters = { + dotfiles = true, + }, +}) + +vim.keymap.set('n', 'd', ':NvimTreeToggle', { desc = 'Open or close the tree' }) diff --git a/apps/neovim/otter.lua b/apps/neovim/otter.lua new file mode 100644 index 0000000..6424721 --- /dev/null +++ b/apps/neovim/otter.lua @@ -0,0 +1,5 @@ +require("otter").setup({ + buffers = { + write_to_disk = true, + }, +}) diff --git a/apps/neovim/quarto.lua b/apps/neovim/quarto.lua new file mode 100644 index 0000000..60e5f49 --- /dev/null +++ b/apps/neovim/quarto.lua @@ -0,0 +1,37 @@ +local quarto = require('quarto') +require('quarto').setup{ + debug = true, + lspFeatures = { + enabled = true, + hover = true, + chunks = "curly", + languages = { "r", "python", "julia", "bash", "html" }, + diagnostics = { + enabled = false, + lint_cache = false, + triggers = { "BufWritePost" }, + }, + completion = { + enabled = true, + }, + }, + codeRunner = { + enabled = true, + default_method = "molten", -- "molten", "slime", "iron" or + ft_runners = { r = "molten", python = "molten" }, -- filetype to runner, ie. `{ python = "molten" }`. + -- Takes precedence over `default_method` + never_run = { 'yaml' }, -- filetypes which are never sent to a code runner + }, +} + +vim.keymap.set('n', 'qp', quarto.quartoPreview, { silent = true, noremap = true }) + +local runner = require("quarto.runner") +vim.keymap.set("n", "rr", runner.run_cell, { desc = "run cell", silent = true }) +vim.keymap.set("n", "ra", runner.run_above, { desc = "run cell and above", silent = true }) +vim.keymap.set("n", "rR", runner.run_all, { desc = "run all cells", silent = true }) +vim.keymap.set("n", "rl", runner.run_line, { desc = "run line", silent = true }) +vim.keymap.set("v", "r", runner.run_range, { desc = "run visual range", silent = true }) +vim.keymap.set("n", "RA", function() + runner.run_all(true) +end, { desc = "run all cells of all languages", silent = true }) diff --git a/apps/neovim/telescope.lua b/apps/neovim/telescope.lua new file mode 100644 index 0000000..ace9fa3 --- /dev/null +++ b/apps/neovim/telescope.lua @@ -0,0 +1,36 @@ +local builtin = require('telescope.builtin') +vim.keymap.set('n', 'ff', builtin.find_files, { desc = 'Telescope find files' }) +vim.keymap.set('n', 'fg', builtin.live_grep, { desc = 'Telescope live grep' }) +vim.keymap.set('n', 'fb', builtin.buffers, { desc = 'Telescope buffers' }) +vim.keymap.set('n', 'fh', builtin.help_tags, { desc = 'Telescope help tags' }) + +require('telescope').setup{ + defaults = { + -- Default configuration for telescope goes here: + -- config_key = value, + mappings = { + i = { + -- map actions.which_key to (default: ) + -- actions.which_key shows the mappings for your picker, + -- e.g. git_{create, delete, ...}_branch for the git_branches picker + [""] = "which_key" + } + } + }, + pickers = { + -- Default configuration for builtin pickers goes here: + -- picker_name = { + -- picker_config_key = value, + -- ... + -- } + -- Now the picker_config_key will be applied every time you call this + -- builtin picker + }, + extensions = { + -- Your extension configuration goes here: + -- extension_name = { + -- extension_config_key = value, + -- } + -- please take a look at the readme of the extension you want to configure + } +} diff --git a/apps/neovim/toggleterm.lua b/apps/neovim/toggleterm.lua new file mode 100644 index 0000000..f104404 --- /dev/null +++ b/apps/neovim/toggleterm.lua @@ -0,0 +1,5 @@ +require("toggleterm").setup() + +vim.keymap.set("n", "tt", 'ToggleTerm direction=float', { noremap = ture, silent = true }) +vim.keymap.set("n", "tv", 'ToggleTerm direction=vertical', { noremap = ture, silent = true }) +vim.keymap.set("n", "th", 'ToggleTerm direction=horizontal', { noremap = ture, silent = true }) diff --git a/apps/neovim/treesitter.lua b/apps/neovim/treesitter.lua new file mode 100644 index 0000000..9e4d0d1 --- /dev/null +++ b/apps/neovim/treesitter.lua @@ -0,0 +1,23 @@ +vim.api.nvim_create_autocmd("FileType", { + pattern = "*", + callback = function() + local ft = vim.bo.filetype + local lang = vim.treesitter.language.get_lang(ft) + + if not lang or not vim.treesitter.language.add(lang) then + return + end + + vim.treesitter.start() + + -- Set folding if available + if vim.treesitter.query.get(lang, "folds") then + vim.wo.foldexpr = "v:lua.vim.treesitter.foldexpr()" + end + + -- Set indentation if available (overrides traditional indent) + if vim.treesitter.query.get(lang, "indents") then + vim.bo.indentexpr = "nvim_treesitter#indent()" + end + end +}) diff --git a/apps/neovim/trouble.lua b/apps/neovim/trouble.lua new file mode 100644 index 0000000..a38d40d --- /dev/null +++ b/apps/neovim/trouble.lua @@ -0,0 +1,204 @@ +---@class trouble.Mode: trouble.Config,trouble.Section.spec +---@field desc? string +---@field sections? string[] + +---@class trouble.Config +---@field mode? string +---@field config? fun(opts:trouble.Config) +---@field formatters? table custom formatters +---@field filters? table custom filters +---@field sorters? table custom sorters +local defaults = { + auto_close = false, -- auto close when there are no items + auto_open = false, -- auto open when there are items + auto_preview = true, -- automatically open preview when on an item + auto_refresh = true, -- auto refresh when open + auto_jump = false, -- auto jump to the item when there's only one + focus = false, -- Focus the window when opened + restore = true, -- restores the last location in the list when opening + follow = true, -- Follow the current item + indent_guides = true, -- show indent guides + max_items = 200, -- limit number of items that can be displayed per section + multiline = true, -- render multi-line messages + pinned = false, -- When pinned, the opened trouble window will be bound to the current buffer + warn_no_results = true, -- show a warning when there are no results + open_no_results = false, -- open the trouble window when there are no results + ---@type trouble.Window.opts + win = {}, -- window options for the results window. Can be a split or a floating window. + -- Window options for the preview window. Can be a split, floating window, + -- or `main` to show the preview in the main editor window. + ---@type trouble.Window.opts + preview = { + type = "main", + -- when a buffer is not yet loaded, the preview window will be created + -- in a scratch buffer with only syntax highlighting enabled. + -- Set to false, if you want the preview to always be a real loaded buffer. + scratch = true, + }, + -- Throttle/Debounce settings. Should usually not be changed. + ---@type table + throttle = { + refresh = 20, -- fetches new data when needed + update = 10, -- updates the window + render = 10, -- renders the window + follow = 100, -- follows the current item + preview = { ms = 100, debounce = true }, -- shows the preview for the current item + }, + -- Key mappings can be set to the name of a builtin action, + -- or you can define your own custom action. + ---@type table + keys = { + ["?"] = "help", + r = "refresh", + R = "toggle_refresh", + q = "close", + o = "jump_close", + [""] = "cancel", + [""] = "jump", + ["<2-leftmouse>"] = "jump", + [""] = "jump_split", + [""] = "jump_vsplit", + -- go down to next item (accepts count) + -- j = "next", + ["}"] = "next", + ["]]"] = "next", + -- go up to prev item (accepts count) + -- k = "prev", + ["{"] = "prev", + ["[["] = "prev", + dd = "delete", + d = { action = "delete", mode = "v" }, + i = "inspect", + p = "preview", + P = "toggle_preview", + zo = "fold_open", + zO = "fold_open_recursive", + zc = "fold_close", + zC = "fold_close_recursive", + za = "fold_toggle", + zA = "fold_toggle_recursive", + zm = "fold_more", + zM = "fold_close_all", + zr = "fold_reduce", + zR = "fold_open_all", + zx = "fold_update", + zX = "fold_update_all", + zn = "fold_disable", + zN = "fold_enable", + zi = "fold_toggle_enable", + gb = { -- example of a custom action that toggles the active view filter + action = function(view) + view:filter({ buf = 0 }, { toggle = true }) + end, + desc = "Toggle Current Buffer Filter", + }, + s = { -- example of a custom action that toggles the severity + action = function(view) + local f = view:get_filter("severity") + local severity = ((f and f.filter.severity or 0) + 1) % 5 + view:filter({ severity = severity }, { + id = "severity", + template = "{hl:Title}Filter:{hl} {severity}", + del = severity == 0, + }) + end, + desc = "Toggle Severity Filter", + }, + }, + ---@type table + modes = { + -- sources define their own modes, which you can use directly, + -- or override like in the example below + lsp_references = { + -- some modes are configurable, see the source code for more details + params = { + include_declaration = true, + }, + }, + -- The LSP base mode for: + -- * lsp_definitions, lsp_references, lsp_implementations + -- * lsp_type_definitions, lsp_declarations, lsp_command + lsp_base = { + params = { + -- don't include the current location in the results + include_current = false, + }, + }, + -- more advanced example that extends the lsp_document_symbols + symbols = { + desc = "document symbols", + mode = "lsp_document_symbols", + focus = false, + win = { position = "right" }, + filter = { + -- remove Package since luals uses it for control flow structures + ["not"] = { ft = "lua", kind = "Package" }, + any = { + -- all symbol kinds for help / markdown files + ft = { "help", "markdown" }, + -- default set of symbol kinds + kind = { + "Class", + "Constructor", + "Enum", + "Field", + "Function", + "Interface", + "Method", + "Module", + "Namespace", + "Package", + "Property", + "Struct", + "Trait", + }, + }, + }, + }, + }, + -- stylua: ignore + icons = { + ---@type trouble.Indent.symbols + indent = { + top = "│ ", + middle = "├╴", + last = "└╴", + -- last = "-╴", + -- last = "╰╴", -- rounded + fold_open = " ", + fold_closed = " ", + ws = " ", + }, + folder_closed = " ", + folder_open = " ", + kinds = { + Array = " ", + Boolean = "󰨙 ", + Class = " ", + Constant = "󰏿 ", + Constructor = " ", + Enum = " ", + EnumMember = " ", + Event = " ", + Field = " ", + File = " ", + Function = "󰊕 ", + Interface = " ", + Key = " ", + Method = "󰊕 ", + Module = " ", + Namespace = "󰦮 ", + Null = " ", + Number = "󰎠 ", + Object = " ", + Operator = " ", + Package = " ", + Property = " ", + String = " ", + Struct = "󰆼 ", + TypeParameter = " ", + Variable = "󰀫 ", + }, + }, +} + diff --git a/apps/neovim/which-key.lua b/apps/neovim/which-key.lua new file mode 100644 index 0000000..fd2000b --- /dev/null +++ b/apps/neovim/which-key.lua @@ -0,0 +1 @@ +local wk = require("which-key") diff --git a/apps/sway/default.nix b/apps/sway/default.nix new file mode 100644 index 0000000..c21fcb4 --- /dev/null +++ b/apps/sway/default.nix @@ -0,0 +1,175 @@ +{ config, pkgs, lib, ... }: +let + mod = "Mod4"; + + terminal = "ghostty"; + menu = "fuzzel"; + + pactl = "${pkgs.pulseaudio}/bin/pactl"; + light = "${pkgs.brightnessctl}/bin/brightnessctl"; + grim = "${pkgs.grim}/bin/grim"; + slurp = "${pkgs.slurp}/bin/slurp"; + wlcopy = "${pkgs.wl-clipboard}/bin/wl-copy"; + + colors = { + base = "#1e1e2e"; + surface0 = "#313244"; + surface1 = "#45475a"; + + text = "#cdd6f4"; + subtext = "#bac2de"; + + blue = "#89b4fa"; + yellow = "#f9e2af"; + peach = "#fab387"; + red = "#f38ba8"; + }; +in +{ + wayland.windowManager.sway = { + enable = true; + + config = { + modifier = mod; + terminal = terminal; + menu = menu; + bars = [ ]; + + fonts = { + names = [ "sans-serif" ]; + size = 10.0; + }; + + window = { + border = 3; + titlebar = false; + }; + + gaps = { + inner = 0; + outer = 0; + smartGaps = false; + smartBorders = "on"; + }; + + + colors = { + focused = { + border = colors.yellow; + background = colors.yellow; + text = colors.base; + indicator = colors.yellow; + childBorder = colors.yellow; + }; + + focusedInactive = { + border = colors.surface1; + background = colors.surface1; + text = colors.text; + indicator = colors.surface1; + childBorder = colors.surface1; + }; + + unfocused = { + border = colors.surface1; + background = colors.surface1; + text = colors.text; + indicator = colors.surface1; + childBorder = colors.surface1; + }; + + urgent = { + border = colors.red; + background = colors.red; + text = colors.base; + indicator = colors.red; + childBorder = colors.red; + }; + }; + + keybindings = lib.mkOptionDefault { + # launcher + "${mod}+Return" = "exec ${terminal}"; + "${mod}+d" = "exec ${menu}"; + + # basic + "${mod}+Shift+q" = "kill"; + "${mod}+Shift+r" = "reload"; + "${mod}+Shift+e" = "exec swaynag -t warning -m 'Exit sway?' -b 'Yes, exit' 'swaymsg exit'"; + + # focus movement + "${mod}+h" = "focus left"; + "${mod}+j" = "focus down"; + "${mod}+k" = "focus up"; + "${mod}+l" = "focus right"; + + # move windows + "${mod}+Shift+h" = "move left"; + "${mod}+Shift+j" = "move down"; + "${mod}+Shift+k" = "move up"; + "${mod}+Shift+l" = "move right"; + + # split/layout + "${mod}+b" = "splith"; + "${mod}+v" = "splitv"; + "${mod}+e" = "layout toggle split"; + "${mod}+s" = "layout stacking"; + "${mod}+w" = "layout tabbed"; + "${mod}+f" = "fullscreen toggle"; + "${mod}+space" = "floating toggle"; + "${mod}+a" = "focus parent"; + + # workspaces 1..10 + "${mod}+1" = "workspace number 1"; + "${mod}+2" = "workspace number 2"; + "${mod}+3" = "workspace number 3"; + "${mod}+4" = "workspace number 4"; + "${mod}+5" = "workspace number 5"; + "${mod}+6" = "workspace number 6"; + "${mod}+7" = "workspace number 7"; + "${mod}+8" = "workspace number 8"; + "${mod}+9" = "workspace number 9"; + "${mod}+0" = "workspace number 10"; + + "${mod}+Shift+1" = "move container to workspace number 1"; + "${mod}+Shift+2" = "move container to workspace number 2"; + "${mod}+Shift+3" = "move container to workspace number 3"; + "${mod}+Shift+4" = "move container to workspace number 4"; + "${mod}+Shift+5" = "move container to workspace number 5"; + "${mod}+Shift+6" = "move container to workspace number 6"; + "${mod}+Shift+7" = "move container to workspace number 7"; + "${mod}+Shift+8" = "move container to workspace number 8"; + "${mod}+Shift+9" = "move container to workspace number 9"; + "${mod}+Shift+0" = "move container to workspace number 10"; + + # screenshots + "Print" = "exec ${grim} - | ${wlcopy}"; + "Shift+Print" = "exec ${grim} -g \"$(${slurp})\" - | ${wlcopy}"; + + # audio keys + "XF86AudioRaiseVolume" = "exec ${pactl} set-sink-volume @DEFAULT_SINK@ +5%"; + "XF86AudioLowerVolume" = "exec ${pactl} set-sink-volume @DEFAULT_SINK@ -5%"; + "XF86AudioMute" = "exec ${pactl} set-sink-mute @DEFAULT_SINK@ toggle"; + "XF86AudioMicMute" = "exec ${pactl} set-source-mute @DEFAULT_SOURCE@ toggle"; + + # brightness keys + "XF86MonBrightnessUp" = "exec ${light} set +5%"; + "XF86MonBrightnessDown" = "exec ${light} set 5%-"; + }; + + startup = [ + { + command = "mako"; + always = true; + } + { + command = "fcitx5"; + always = true; + } + #{ command = "waybar"; always = true; } + ]; + }; + + #extraConfig = ''''; + }; +} diff --git a/apps/waybar/config.jsonc b/apps/waybar/config.jsonc new file mode 100644 index 0000000..48265e9 --- /dev/null +++ b/apps/waybar/config.jsonc @@ -0,0 +1,91 @@ +{ + "layer": "top", + "position": "top", + "height": 25, + "spacing": 1, + + "modules-left": ["sway/workspaces", "sway/mode"], + "modules-right": ["cpu", "memory", "network", "pulseaudio","battery", "clock"], + + "sway/workspaces": { + "disable-scroll": true, + }, + + "sway/window": { + "max-length": 60 + }, + + "network": { + "interval": 5, + "format-alt": "{icon} ↓{bandwidthDownBits} ↑{bandwidthUpBits}", + "format-wifi": "{icon} {signalStrength}%", + "format-icons": ["󰤯 ", "󰤟 ", "󰤢 ", "󰤥 ", "󰤨 "], + + "format-ethernet": "󰈀 ", + "format-disconnected": "󰤭 ", + + "tooltip": true, + "tooltip-format": "{essid} {ipaddr}" + }, + + "pulseaudio": { + "format": "{icon} {volume}%", + "format-muted": " 0%", + "format-icons": { + "default": [" "], + }, + "on-click": "pactl set-sink-mute @DEFAULT_SINK@ toggle", + }, + + "battery": { + "format": "{icon} {capacity}%", + "format-alt": "{icon} {time} 󱐋 {power}W", + "format-charging": "{icon} {capacity}%", + + "states": { + "warning": 25, + "critical": 10 + }, + "format-icons": { + "default": ["󰂃", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹"], + "charging": ["󰢜 ", "󰂆 ", "󰂇 ", "󰂈 ", "󰢝 ", "󰂉 ", "󰢞 ", "󰂊 ", "󰂋 ", "󰂅 "], + } + + }, + "clock": { + "format": "󰥔 {:%H:%M}", + "tooltip-format": "{:%a %d %b %Y}" + }, + + "cpu": { + "interval": 2, + "format": " {usage}%", + "format-alt":" {usage}% {icon0}{icon1}{icon2}{icon3}{icon4}{icon5}{icon6}{icon7}{icon8}{icon9}{icon10}{icon11}{icon12}{icon13}{icon14}{icon15}  {avg_frequency} GHz", + "states": { + "warning": 60, + "critical": 85, + }, + "format-icons": [ + "", // lavender (cool) + "", // blue + "", // text + "", // text + "", // yellow (warm) + "", // yellow + "", // peach (hot) + "" // red (very hot) + ], + "tooltip": true + }, + + "memory": { + "interval": 5, + "format": " {percentage}%", + "format-alt": " {used}GiB / {avail}GiB", + "states": { + "warning": 70, + "critical": 90, + }, + "tooltip": true + } +} diff --git a/apps/waybar/default.nix b/apps/waybar/default.nix new file mode 100644 index 0000000..acb7171 --- /dev/null +++ b/apps/waybar/default.nix @@ -0,0 +1,12 @@ +{ config, pkgs, ... }: +{ + programs.waybar = { + enable = true; + systemd.enable = true; + }; + + xdg.configFile = { + "waybar/config.jsonc".source = ./config.jsonc; + "waybar/style.css".source = ./style.css; + }; +} diff --git a/apps/waybar/style.css b/apps/waybar/style.css new file mode 100644 index 0000000..48b4e2d --- /dev/null +++ b/apps/waybar/style.css @@ -0,0 +1,150 @@ +/* ========================================================= + Waybar style.css — Catppuccin Mocha (GTK-safe) + Unified palette + consistent workspaces + severity states + ========================================================= */ + +/* ---------- Catppuccin Mocha palette ---------- */ +@define-color base #1e1e2e; +@define-color mantle #181825; +@define-color crust #11111b; + +@define-color text #cdd6f4; +@define-color subtext #bac2de; +@define-color muted #9399b2; + +@define-color surface0 #313244; +@define-color surface1 #45475a; + +/* Accents */ +@define-color blue #89b4fa; +@define-color green #a6e3a1; +@define-color peach #fab387; +@define-color yellow #f9e2af; +@define-color red #f38ba8; +@define-color teal #94e2d5; +@define-color lavender #b4befe; + +/* Bar + state backgrounds */ +@define-color bar_bg rgba(30, 30, 46, 0.88); +@define-color warn_bg rgba(249, 226, 175, 0.28); +@define-color crit_bg rgba(243, 139, 168, 0.38); + +/* Workspace backgrounds */ +@define-color ws_hover rgba(137, 180, 250, 0.16); +@define-color ws_focus rgba(137, 180, 250, 0.26); + +/* ---------- Global reset ---------- */ +* { + border: none; + border-radius: 0; + min-height: 0; + box-shadow: none; +} + +/* ---------- Bar ---------- */ +window#waybar { + background: @bar_bg; + color: @text; + font-size: 12px; +} + +/* ========================================================= + Workspaces + ========================================================= */ + +#workspaces { + padding: 0 5px; +} + +#workspaces button { + padding: 0 5px; + margin: 0px 0px; + color: @subtext; + background: transparent; +} + +#workspaces button.focused { + color: @text; + background: @ws_focus; +} + +#workspaces button:hover { + background: @ws_hover; +} + +#workspaces button.urgent { + color: @text; + background: @crit_bg; +} + +#mode { + padding: 0 12px; + + background: #f9e2af; /* fully opaque yellow */ + color: #1e1e2e; /* dark text */ + + font-weight: 600; +} + +/* ========================================================= + Modules (shared) + ========================================================= */ + +#network, +#pulseaudio, +#battery, +#clock, +#cpu, +#memory { + padding: 0 5px; /* literal spacing — GTK-safe */ + margin: 0px 0px; + background: transparent; + color: @text; +} + +/* Identity colors */ +#network { color: @blue; } +#pulseaudio { color: @peach; } +#battery { color: @green; } +#clock { color: @text; font-weight: 500; } +#cpu { color: @lavender; } +#memory { color: @teal; } + +/* ========================================================= + Severity states (single, consistent approach) + ========================================================= */ + +#battery.warning { + background: @warn_bg; + color: @text; +} + +#battery.critical { + background: @crit_bg; + color: @text; +} + +#network.disconnected { + background: @crit_bg; + color: @text; +} + +#cpu.warning { + background: @warn_bg; + color: @text; +} + +#cpu.critical { + background: @crit_bg; + color: @text; +} + +#memory.warning { + background: @warn_bg; + color: @text; +} + +#memory.critical { + background: @crit_bg; + color: @text; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..0251732 --- /dev/null +++ b/flake.lock @@ -0,0 +1,92 @@ +{ + "nodes": { + "disko": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1746728054, + "narHash": "sha256-eDoSOhxGEm2PykZFa/x9QG5eTH0MJdiJ9aR00VAofXE=", + "owner": "nix-community", + "repo": "disko", + "rev": "ff442f5d1425feb86344c028298548024f21256d", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "latest", + "repo": "disko", + "type": "github" + } + }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1758463745, + "narHash": "sha256-uhzsV0Q0I9j2y/rfweWeGif5AWe0MGrgZ/3TjpDYdGA=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "3b955f5f0a942f9f60cdc9cacb7844335d0f21c3", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-25.05", + "repo": "home-manager", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1765762245, + "narHash": "sha256-3iXM/zTqEskWtmZs3gqNiVtRTsEjYAedIaLL0mSBsrk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c8cfcd6ccd422e41cc631a0b73ed4d5a925c393d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "disko": "disko", + "home-manager": "home-manager", + "nixpkgs": "nixpkgs", + "sops-nix": "sops-nix" + } + }, + "sops-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768863606, + "narHash": "sha256-1IHAeS8WtBiEo5XiyJBHOXMzECD6aaIOJmpQKzRRl64=", + "owner": "Mic92", + "repo": "sops-nix", + "rev": "c7067be8db2c09ab1884de67ef6c4f693973f4a2", + "type": "github" + }, + "original": { + "owner": "Mic92", + "repo": "sops-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2b12318 --- /dev/null +++ b/flake.nix @@ -0,0 +1,124 @@ +{ + description = "My NixOS flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + + disko = { + url = "github:nix-community/disko/latest"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + home-manager = { + url = "github:nix-community/home-manager/release-25.05"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + sops-nix = { + url = "github:Mic92/sops-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + }; + + outputs = + { + self, + nixpkgs, + disko, + home-manager, + sops-nix, + ... + }@inputs: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + in + { + nixosConfigurations = + let + my-r = import ./languages/r.nix { inherit pkgs; }; + my-python = import ./languages/python.nix { inherit pkgs; }; + + T14pModules = [ + disko.nixosModules.disko + sops-nix.nixosModules.sops + home-manager.nixosModules.home-manager + + ./hosts/T14p/configuration.nix + + { + home-manager.extraSpecialArgs = { + inherit inputs my-r my-python; + }; + + home-manager.useGlobalPkgs = true; + home-manager.useUserPackages = true; + + home-manager.users.louis = import ./home/full.nix; + home-manager.users.root = import ./home/root.nix; + } + ]; + + mkT14p = nixpkgs.lib.nixosSystem { + inherit system; + specialArgs = { inherit inputs; }; + modules = T14pModules; + }; + + mkServer = + domain: isProd: + nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + specialArgs = { inherit inputs domain isProd; }; + modules = [ + inputs.disko.nixosModules.disko + sops-nix.nixosModules.sops + + ./hosts/hetzner/configuration.nix + ]; + }; + in + { + T14p = mkT14p; + + hetzner-prod = mkServer "louisclee.com" true; + hetzner-test = mkServer "localhost" false; + }; + + apps.${system} = + let + target = "root@louisclee.com"; + + deployScript = pkgs.writeShellScriptBin "deploy" '' + ${pkgs.nixos-rebuild}/bin/nixos-rebuild switch \ + --flake .#hetzner-prod \ + --target-host ${target} \ + --use-remote-sudo + ''; + + initScript = pkgs.writeShellScriptBin "init" '' + nix run github:nix-community/nixos-anywhere -- \ + --flake .#hetzner-prod \ + --extra-files ./hosts/hetzner/extra-files \ + ${target} + ''; + in + { + deploy = { + type = "app"; + program = "${deployScript}/bin/deploy"; + }; + + init = { + type = "app"; + program = "${initScript}/bin/init"; + }; + + vm = { + type = "app"; + program = "${self.nixosConfigurations.hetzner-test.config.system.build.vm}/bin/run-webserver-vm"; + }; + }; + }; +} diff --git a/home/full.nix b/home/full.nix new file mode 100644 index 0000000..ba84a6e --- /dev/null +++ b/home/full.nix @@ -0,0 +1,86 @@ +{ config, pkgs, inputs, my-r, my-python,... }: { + home.username = "louis"; + home.homeDirectory = "/home/louis"; + + home.enableNixpkgsReleaseCheck = false; + + imports = [ + ../apps/neovim + ../apps/sway + ../apps/waybar + ../apps/fuzzel + #../modules/model-downloads.nix + ]; + + home.packages = with pkgs; [ + fzf + chromium + poppler-utils + my-r.env + my-r.rstudio + my-python + which + ripgrep + kitty + gcc + gnumake + kdePackages.okular + quarto + julia + wechat + wemeet + zotero + sqlite + anki-bin + sqlitebrowser + dig + ghostty + inkscape + tig + wofi + grim + slurp + wl-clipboard + brightnessctl + pulseaudio + neofetch + llama-cpp + ]; + + my.apps.fuzzel.enable = true; + + programs.fish = { + enable = true; + interactiveShellInit = '' + export QUARTO_R="/home/louis/.nix-profile/bin/R"; + export EDITOR="nvim"; + ''; + }; + + #programs.zsh = { + # enable = true; + # enableCompletion = true; + # autosuggestion.enable = true; + # syntaxHighlighting.enable = true; + # sessionVariables = { + # QUARTO_R = "/home/louis/.nix-profile/bin/R"; + # EDITOR = "nvim"; + # }; + #}; + + programs.git = { + enable = true; + userName = "Louis Chih-Ming Lee"; + userEmail = "louis@louisclee.com"; + }; + + #modelDownloads = { + # enable = true; + # dir = "${config.home.homeDirectory}/data/ai/models"; + # urls = [ + # "https://huggingface.co/bartowski/Meta-Llama-3.1-8B-Instruct-GGUF/resolve/main/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf" + # ]; + #}; + + home.stateVersion = "25.05"; +} diff --git a/home/root.nix b/home/root.nix new file mode 100644 index 0000000..98467ac --- /dev/null +++ b/home/root.nix @@ -0,0 +1,12 @@ +{ config, pkgs, inputs, ... }: +{ + home.username = "root"; + + home.enableNixpkgsReleaseCheck = false; + + imports = [ + ./../apps/neovim + ]; + + home.stateVersion = "25.05"; +} diff --git a/hosts/T14p/configuration.nix b/hosts/T14p/configuration.nix new file mode 100644 index 0000000..4cd969e --- /dev/null +++ b/hosts/T14p/configuration.nix @@ -0,0 +1,275 @@ +# Edit this configuration file to define what should be installed on +# your system. Help is available in the configuration.nix(5) man page, on +# https://search.nixos.org/options and in the NixOS manual (`nixos-help`). + +{ + inputs, + config, + lib, + pkgs, + ... +}: +{ + imports = [ + # Include the results of the hardware scan. + ./hardware-configuration.nix + #./impermanence.nix { inherit lib; } + ./disko.nix + ./network.nix + ]; + + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; + + # Use the systemd-boot EFI boot loader. + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + boot.kernelPackages = pkgs.linuxPackages_latest; + + networking.hostName = "T14p"; # Define your hostname. + # Pick only one of the below networking options. + # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant. + networking.networkmanager.enable = true; # Easiest to use and most distros use this by default. + + services.mullvad-vpn.enable = true; + + sops = { + defaultSopsFile = ./../../secrets.yaml; + defaultSopsFormat = "yaml"; + age.keyFile = "/home/louis/.config/sops/age/keys.txt"; + }; + + + hardware.enableAllFirmware = true; + + hardware.graphics = { + enable = true; + extraPackages = with pkgs; [ + mesa + ]; + }; + + hardware.bluetooth = { + enable = true; + powerOnBoot = true; + #settings = { + # General = { + # Experimental = true; + # FastConnectable = true; + # }; + # Policy = { + # AutoEnable = true; + # }; + #}; + }; + + # Set your time zone. + time.timeZone = "Europe/Amsterdam"; + + # Configure network proxy if necessary + # networking.proxy.default = "http://user:password@proxy:port/"; + # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain"; + + # Select internationalisation properties. + i18n = { + defaultLocale = "en_US.UTF-8"; + extraLocales = [ + "zh_TW.UTF-8/UTF-8" + "zh_CN.UTF-8/UTF-8" + "nl_NL.UTF-8/UTF-8" + ]; + }; + + i18n.inputMethod = { + type = "fcitx5"; + enable = true; + fcitx5.addons = with pkgs; [ + fcitx5-gtk + fcitx5-rime + fcitx5-chewing + qt6Packages.fcitx5-chinese-addons + rime-data + librime + ]; + fcitx5.waylandFrontend = true; + }; + environment.variables = { + GTK_IM_MODULE = "fcitx"; + QT_IM_MODULE = "fcitx"; + XMODIFIERS = "@im=fcitx"; + GLFW_IM_MODULE = "fcitx"; + SDL_IM_MODULE = "fcitx"; + MOZ_ENABLE_WAYLAND = 1; + QT_QPA_PLATFORM = "wayland"; + }; + + # electron blurring in wayland + environment.sessionVariables = { + NIXOS_OZONE_WL = "1"; + }; + + console = { + font = "Lat2-Terminus16"; + #keyMap = "us"; + useXkbConfig = true; # use xkb.options in tty. + }; + + # Enable the X11 windowing system. + services.xserver.enable = true; + + #services.displayManager.cosmic-greeter.enable = true; + services.displayManager.gdm.enable = true; + services.desktopManager.cosmic.enable = true; + + programs.sway = { + enable = true; + wrapperFeatures.gtk = true; + extraPackages = with pkgs; [ + adwaita-icon-theme + gnome-themes-extra + ]; + }; + + fonts.packages = with pkgs; [ + lxgw-fusionkai + noto-fonts + noto-fonts-cjk-sans + noto-fonts-cjk-serif + winePackages.fonts + noto-fonts-color-emoji + nerd-fonts.fira-code + ]; + + services.keyd = { + enable = true; + keyboards = { + default = { + ids = [ "*" ]; + settings = { + main = { + capslock = "layer(control)"; + control = "capslock"; + leftmeta = "layer(alt)"; + leftalt = "layer(meta)"; + rightalt = "esc"; + }; + }; + }; + hhkb = { + ids = [ "04fe:0021:f2a164d2" ]; + settings = { }; + }; + }; + }; + + # Configure keymap in X11 + # services.xserver.xkb.layout = "us"; + # services.xserver.xkb.options = "eurosign:e,caps:escape"; + + # Enable CUPS to print documents. + services.printing.enable = true; + + # Enable sound. + # services.pulseaudio.enable = true; + # OR + services.pipewire = { + enable = true; + pulse.enable = true; + }; + + # Enable touchpad support (enabled default in most desktopManager). + services.libinput.enable = true; + + # Define a user account. Don't forget to set a password with ‘passwd’. + users.users.louis = { + isNormalUser = true; + extraGroups = [ + "wheel" + "networkmanager" + "video" + ]; + shell = pkgs.fish; + ignoreShellProgramCheck = true; + packages = with pkgs; [ + keyd + tree + wl-clipboard + mako + killall + ]; + }; + + programs.firefox.enable = true; + + # List packages installed in system profile. + # You can use https://search.nixos.org/ to find more packages (and options). + environment.systemPackages = with pkgs; [ + vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default. + kdePackages.kdeconnect-kde + usbutils + wget + neovim + acpi + texliveFull + zathura + libreoffice + htop + ranger + teams-for-linux + gcc + gnumake + mullvad-vpn + mullvad + sops + age + ssh-to-age + ]; + + # Some programs need SUID wrappers, can be configured further or are + # started in user sessions. + programs.mtr.enable = true; + programs.gnupg.agent = { + enable = true; + enableSSHSupport = true; + }; + nixpkgs.config.allowUnfree = true; + + # List services that you want to enable: + + # Enable the OpenSSH daemon. + services.openssh.enable = true; + + # Open ports in the firewall. + # networking.firewall.allowedTCPPorts = [ ... ]; + # networking.firewall.allowedUDPPorts = [ ... ]; + # Or disable the firewall altogether. + networking.firewall.enable = false; + + # Copy the NixOS configuration file and link it from the resulting system + # (/run/current-system/configuration.nix). This is useful in case you + # accidentally delete configuration.nix. + #system.copySystemConfiguration = true; + + # This option defines the first version of NixOS you have installed on this particular machine, + # and is used to maintain compatibility with application data (e.g. databases) created on older NixOS versions. + # + # Most users should NEVER change this value after the initial install, for any reason, + # even if you've upgraded your system to a new NixOS release. + # + # This value does NOT affect the Nixpkgs version your packages and OS are pulled from, + # so changing it will NOT upgrade your system - see https://nixos.org/manual/nixos/stable/#sec-upgrading for how + # to actually do that. + # + # This value being lower than the current NixOS release does NOT mean your system is + # out of date, out of support, or vulnerable. + # + # Do NOT change this value unless you have manually inspected all the changes it would make to your configuration, + # and migrated your data accordingly. + # + # For more information, see `man configuration.nix` or https://nixos.org/manual/nixos/stable/options#opt-system.stateVersion . + system.stateVersion = "25.05"; # Did you read the comment? + +} diff --git a/hosts/T14p/disko.nix b/hosts/T14p/disko.nix new file mode 100644 index 0000000..e95725e --- /dev/null +++ b/hosts/T14p/disko.nix @@ -0,0 +1,70 @@ +{ + disko.devices = { + disk = { + main = { + type = "disk"; + device = "/dev/nvme1n1"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "crypted"; + # disable settings.keyFile if you want to use interactive password entry + #passwordFile = "/tmp/secret.key"; # Interactive + settings = { + allowDiscards = true; + #keyFile = "/tmp/secret.key"; + }; + #additionalKeyFiles = [ "/tmp/additionalSecret.key" ]; + content = { + type = "btrfs"; + extraArgs = [ "-f" ]; + subvolumes = { + "@root" = { + mountpoint = "/"; + mountOptions = [ + "compress=zstd" + "noatime" + ]; + }; + "@persist" = { + mountpoint = "/persist"; + mountOptions = [ + "compress=zstd" + "noatime" + ]; + }; + "@nix" = { + mountpoint = "/nix"; + mountOptions = [ + "compress=zstd" + "noatime" + ]; + }; + "@swap" = { + mountpoint = "/.swapvol"; + swap.swapfile.size = "20G"; + }; + }; + }; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/hosts/T14p/hardware-configuration.nix b/hosts/T14p/hardware-configuration.nix new file mode 100644 index 0000000..3c34ddb --- /dev/null +++ b/hosts/T14p/hardware-configuration.nix @@ -0,0 +1,26 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" "usb_storage" "sd_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.useDHCP = lib.mkDefault true; + # networking.interfaces.enp0s31f6.useDHCP = lib.mkDefault true; + # networking.interfaces.wlp0s20f3.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/hosts/T14p/impermanence.nix b/hosts/T14p/impermanence.nix new file mode 100644 index 0000000..4f6349a --- /dev/null +++ b/hosts/T14p/impermanence.nix @@ -0,0 +1,56 @@ +{ lib, ... }: +let + impermanence = builtins.fetchTarball "https://github.com/nix-community/impermanence/archive/master.tar.gz"; +in +{ + boot.initrd.postResumeCommands = lib.mkAfter '' + mkdir /btrfs_tmp + mount /dev/mapper/crypted /btrfs_tmp + if [[ -e /btrfs_tmp/@root ]]; then + mkdir -p /btrfs_tmp/@old_roots + timestamp=$(date --date="@$(stat -c %Y /btrfs_tmp/root)" "+%Y-%m-%-d_%H:%M:%S") + mv /btrfs_tmp/@root "/btrfs_tmp/@old_roots/$timestamp" + fi + + delete_subvolume_recursively() { + IFS=$'\n' + for i in $(btrfs subvolume list -o "$1" | cut -f 9- -d ' '); do + delete_subvolume_recursively "/btrfs_tmp/$i" + done + btrfs subvolume delete "$1" + } + + for i in $(find /btrfs_tmp/@old_roots/ -maxdepth 1 -mtime +10); do + delete_subvolume_recursively "$i" + done + + btrfs subvolume create /btrfs_tmp/@root + umount /btrfs_tmp + ''; + + imports = [ "${impermanence}/nixos.nix" ]; + + environment.persistence."/persist" = { + enable = true; + hideMounts = true; + directories = [ + "/var/log" + "/var/lib/bluetooth" + "/var/lib/nixox" + "/var/lib/systemd/coredump" + "/etc/NetworkManager/system-connections" + ]; + files = [ + "/etc/machine-id" + ]; + users.louis = { + directories = [ + "Documents" + "Downloads" + ]; + }; + }; + + fileSystems."/persist".neededForBoot = true; + +} diff --git a/hosts/T14p/leiden.crt b/hosts/T14p/leiden.crt new file mode 100644 index 0000000..2795cf3 --- /dev/null +++ b/hosts/T14p/leiden.crt @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- diff --git a/hosts/T14p/network.nix b/hosts/T14p/network.nix new file mode 100644 index 0000000..4ab14bf --- /dev/null +++ b/hosts/T14p/network.nix @@ -0,0 +1,83 @@ +{ + pkgs, + lib, + config, + ... +}: +let + wifi = ssid: alias: priority: { + sops.secrets."wifi_${alias}" = { }; + sops.templates."wifi_${alias}.nmconnection" = { + path = "/etc/NetworkManager/system-connections/wifi_${ssid}.nmconnection"; + owner = "root"; + group = "root"; + mode = "0600"; + content = '' + [connection] + id=${ssid} + type=wifi + autoconnect=true + autoconnect-priority=${toString priority} + + [wifi] + ssid=${ssid} + mode=infrastructure + + [wifi-security] + key-mgmt=wpa-psk + psk=${config.sops.placeholder."wifi_${alias}"} + + [ipv4] + method=auto + + [ipv6] + addr-gen-mode=default + method=auto + ''; + }; + }; +in +lib.mkMerge [ + (wifi "Ziggo966936B" "home" 100) + { + sops.secrets.wifi_leiden_identity = { }; + sops.secrets.wifi_leiden_password = { }; + sops.templates."wifi_eduroam.nmconnection" = { + path = "/etc/NetworkManager/system-connections/eduroam.nmconnection"; + + owner = "root"; + group = "root"; + mode = "0600"; + + content = '' + [connection] + id=eduroam + type=wifi + + [wifi] + ssid=eduroam + mode=infrastructure + + [wifi-security] + key-mgmt=wpa-eap + + [802-1x] + eap=peap + identity=${config.sops.placeholder.wifi_leiden_identity} + password=${config.sops.placeholder.wifi_leiden_password} + anonymous-identity=anonymous@leidenuniv.nl + phase2-auth=mschapv2 + ca-cert=${./leiden.crt} + ''; + }; + } + { + # Run nmcli reload after every switch + system.activationScripts.nm-reload = { + text = '' + ${pkgs.networkmanager}/bin/nmcli connection reload || true + ''; + deps = [ ]; + }; + } +] diff --git a/hosts/hetzner/configuration.nix b/hosts/hetzner/configuration.nix new file mode 100644 index 0000000..b02299e --- /dev/null +++ b/hosts/hetzner/configuration.nix @@ -0,0 +1,64 @@ +{ + pkgs, + lib, + domain, + isProd, + inputs, + config, + modulesPath, + ... +}: +{ + + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + (modulesPath + "/profiles/qemu-guest.nix") + + ./disko-config.nix + ./secret.nix + ./media.nix + ./nginx.nix + ./immich.nix + ./forego.nix + + # ./vm.nix + ]; + + system.stateVersion = "25.11"; + + environment.systemPackages = with pkgs; [ + vim + cifs-utils + btrfs-progs + forgejo + ]; + + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ]; + + boot.loader.grub = { + # enable = true; + efiSupport = true; + efiInstallAsRemovable = true; + }; + + services.openssh.enable = true; + users.users.root.openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDBUxBUar3CyZCZTet3s8s28Pu1d0viuDe6YoMQBVdFB louis@T14p" + ]; + + networking.hostName = "webserver"; + networking.firewall.allowedTCPPorts = [ + 80 + 443 + 22 + ]; + + + security.acme = lib.mkIf isProd { + acceptTerms = true; + defaults.email = "admin@${domain}"; + }; +} diff --git a/hosts/hetzner/disko-config.nix b/hosts/hetzner/disko-config.nix new file mode 100644 index 0000000..afc1fe2 --- /dev/null +++ b/hosts/hetzner/disko-config.nix @@ -0,0 +1,86 @@ +{ lib, ... }: +let + disk-id = "scsi-0QEMU_QEMU_HARDDISK_110162268"; + data-id = "scsi-0HC_Volume_104473479"; +in +{ + disko.devices = { + + # --- DISK 1: MAIN OS (38GB) --- + disk.main = { + # You were right! This IS the correct ID for the 38GB drive. + device = "/dev/disk/by-id/${disk-id}"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; + priority = 1; + }; + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + + disk.volume = { + # This is the ID for your volume (from your ls output) + device = "/dev/disk/by-id/${disk-id}"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + data = { + size = "100%"; + content = { + type = "btrfs"; + extraArgs = [ "-f" ]; + mountpoint = "/mnt/data"; + subvolumes = { + "@postgresql" = { + mountpoint = "/mnt/data/postgresql"; + mountOptions = [ + "nodatacow" + "noatime" + ]; + }; + "@forgejo" = { + mountpoint = "/mnt/data/forgejo"; + mountOptions = [ + "compress=zstd" + "noatime" + ]; + }; + "@immich" = { + mountpoint = "/mnt/data/immich"; + mountOptions = [ + "compress=zstd" + "noatime" + ]; + }; + }; + }; + }; + }; + }; + }; + + }; +} diff --git a/hosts/hetzner/filesystem.nix b/hosts/hetzner/filesystem.nix new file mode 100644 index 0000000..e20ff71 --- /dev/null +++ b/hosts/hetzner/filesystem.nix @@ -0,0 +1,71 @@ +{ lib, config, ... }: +let + volume-id = "scsi-0HC_Volume_104473479"; +in +{ + systemd.tmpfiles.rules = [ + "d /mnt/ 0755 root root -" + + "d /mnt/box 0770 root root -" + + "d /mnt/box/immich 0770 immich immich -" + "d /mnt/box/immich 0770 immich immich -" + "d /mnt/box/immich/library 0770 immich immich -" + "d /mnt/box/immich/upload 0770 immich immich -" + "d /mnt/box/immich/thumbs 0770 immich immich -" + "d /mnt/box/immich/encoded-video 0770 immich immich -" + "d /mnt/box/immich/profile 0770 immich immich -" + "d /mnt/box/immich/backups 0770 immich immich -" + + "d /mnt/volume 0777 root root -" + "d /mnt/volume/postgresql 0700 postgres postgres -" + "d /mnt/volume/forgejo 0750 forgejo forgejo -" + + "d /mnt/volume/immich 0750 immich immich -" + ]; + + fileSystems."/mnt/data/postgres" = { + device = "/dev/disk/by-id/${volume-id}"; + fsType = "btrfs"; + options = [ + "subvol=@postgres" + "nodatacow" # <--- Disables Copy-on-Write for performance + "noatime" + ]; + }; + + fileSystems."/mnt/data/immich" = { + device = "/dev/disk/by-id/${volume-id}"; + fsType = "btrfs"; + options = [ + "subvol=@immich" + "compress=zstd" + "noatime" + ]; + }; + + fileSystems."/mnt/data/forgejo" = { + device = "/dev/disk/by-id/${volume-id}"; + fsType = "btrfs"; + options = [ + "subvol=@forgejo" + "compress=zstd" + "noatime" + ]; + }; + + #fileSystems."/mnt/box" = { + # device = "//u536222.your-storagebox.de/backup"; + # fsType = "cifs"; + # options = [ + # "x-systemd.automount" + # "noauto" + # "rw" + # "credentials=${config.sops.secrets.storage_box_credentials.path}" + # "uid=900" + # "gid=100" + # "file_mode=0660" + # "dir_mode=0770" + # ]; + #}; +} diff --git a/hosts/hetzner/forego.nix b/hosts/hetzner/forego.nix new file mode 100644 index 0000000..34a870a --- /dev/null +++ b/hosts/hetzner/forego.nix @@ -0,0 +1,74 @@ +{ + domain, + isProd, + config, + pkgs, + ... +}: +{ + # 1. Access the Secret + sops.secrets.forgejo_db_password = { + owner = "forgejo"; + # Restart forgejo if the password changes + restartUnits = [ "forgejo.service" ]; + }; + + services.forgejo = { + enable = true; + + # 2. STORAGE (SSD) + stateDir = "/mnt/data/forgejo"; + + # 3. DATABASE (Shared Postgres) + database = { + type = "postgres"; + name = "forgejo"; + user = "forgejo"; + createDatabase = false; # We let NixOS manage this below + socket = "/run/postgresql"; # Ultra-fast socket connection + passwordFile = config.sops.secrets.forgejo_db_password.path; + }; + + # 4. SETTINGS + settings = { + server = { + DOMAIN = "git.${domain}"; + ROOT_URL = "https://git.${domain}/"; + HTTP_PORT = 3000; + # Run internal SSH on 2222 so it doesn't block your Admin SSH (22) + SSH_PORT = 2222; + START_SSH_SERVER = true; + }; + # Disable registration to prevent random internet people from joining + service.DISABLE_REGISTRATION = true; + + # Optional: Metrics for Grafana later + metrics.ENABLED = true; + }; + }; + + # 5. POSTGRESQL PROVISIONING + # This automatically creates the DB and User when you deploy + services.postgresql = { + ensureDatabases = [ "forgejo" ]; + ensureUsers = [ + { + name = "forgejo"; + ensureDBOwnership = true; + } + ]; + }; + + # 6. REVERSE PROXY + services.nginx.virtualHosts."git.${domain}" = { + forceSSL = isProd; + enableACME = isProd; + locations."/" = { + proxyPass = "http://127.0.0.1:3000"; + }; + }; + + # 7. FIREWALL + # Allow Git-over-SSH on the custom port + networking.firewall.allowedTCPPorts = [ 2222 ]; +} diff --git a/hosts/hetzner/immich.nix b/hosts/hetzner/immich.nix new file mode 100644 index 0000000..bb1ffa2 --- /dev/null +++ b/hosts/hetzner/immich.nix @@ -0,0 +1,79 @@ +{ + domain, + lib, + isProd, + config, + pkgs, + ... +}: +{ + sops.secrets.immich_db_password = { }; + sops.secrets.immich_jwt_secret = { }; + + sops.templates."immich.env".content = '' + DB_PASSWORD=${config.sops.placeholder.immich_db_password} + JWT_SECRET=${config.sops.placeholder.immich_jwt_secret} + ''; + + #users.users.immich.extraGroups = [ "users" ]; + users.users.immich.uid = 900; + users.groups.immich.gid = 900; + + services.immich = { + enable = true; + host = "127.0.0.1"; + port = 2283; + + mediaLocation = "/mnt/media/immich"; + + secretsFile = config.sops.templates."immich.env".path; + + redis.enable = true; + database = { + enable = true; + createDB = true; + user = "immich"; + name = "immich"; + host = "/run/postgresql"; + }; + + machine-learning.enable = true; + }; + + systemd.services.immich-server = { + requires = [ "mnt-media.mount" ]; + after = [ "mnt-media.mount" ]; + + serviceConfig = { + DynamicUser = lib.mkForce false; + ReadWritePaths = [ "/mnt/media/immich" ]; + BindPaths = [ "/mnt/media/immich" ]; + }; + }; + + services.postgresql = { + enable = true; + + dataDir = "/mnt/data/postgresql"; + + ensureDatabases = [ "immich" ]; + ensureUsers = [ + { + name = "immich"; + ensureDBOwnership = true; + } + ]; + }; + + services.nginx.virtualHosts."photo.${domain}" = { + forceSSL = isProd; + enableACME = isProd; + + locations."/" = { + proxyPass = "http://127.0.0.1:2283"; + proxyWebsockets = true; + extraConfig = "client_max_body_size 50G;"; + }; + + }; +} diff --git a/hosts/hetzner/media.nix b/hosts/hetzner/media.nix new file mode 100644 index 0000000..0d837cd --- /dev/null +++ b/hosts/hetzner/media.nix @@ -0,0 +1,34 @@ +{ config, ... }: +{ + systemd.tmpfiles.rules = [ + # 1. THE PARENT DIRS + # Change 0770 -> 0755 so users like 'postgres' can walk through the door. + "d /mnt/data 0755 root root -" + "d /mnt/media 0755 root root -" + + # 2. THE SERVICES (SSD / Data) + # IMPORTANT: These MUST match where your services.postgresql.dataDir points + "d /mnt/data/postgresql 0700 postgres postgres -" + "d /mnt/data/forgejo 0750 forgejo forgejo -" + + # 3. THE STORAGE (HDD / Media) + "d /mnt/media/immich 0750 immich immich -" + ]; + + fileSystems."/mnt/media" = { + device = "//u536222.your-storagebox.de/backup"; + fsType = "cifs"; + options = [ + "nofail" + "noperm" + "rw" + "credentials=${config.sops.secrets.storage_box_credentials.path}" + "uid=900" + "gid=900" + "forceuid" + "forcegid" + "file_mode=0660" + "dir_mode=0770" + ]; + }; +} diff --git a/hosts/hetzner/nginx.nix b/hosts/hetzner/nginx.nix new file mode 100644 index 0000000..61269cf --- /dev/null +++ b/hosts/hetzner/nginx.nix @@ -0,0 +1,18 @@ +{ domain, isProd, ... }: +{ + services.nginx = { + enable = true; + recommendedProxySettings = true; + recommendedTlsSettings = isProd; + + virtualHosts."${domain}" = { + forceSSL = isProd; + enableACME = isProd; + + locations."/test" = { + return = "200 'Hello! You are accessing: ${domain}/test'"; + extraConfig = "default_type text/plain;"; + }; + }; + }; +} diff --git a/hosts/hetzner/secret.nix b/hosts/hetzner/secret.nix new file mode 100644 index 0000000..6d272ae --- /dev/null +++ b/hosts/hetzner/secret.nix @@ -0,0 +1,10 @@ +{ ... }: +{ + sops = { + defaultSopsFile = ./../../secrets.yaml; + defaultSopsFormat = "yaml"; + age.keyFile = "/var/lib/sops-nix/key.txt"; + }; + + sops.secrets.storage_box_credentials = { }; +} diff --git a/hosts/hetzner/vm.nix b/hosts/hetzner/vm.nix new file mode 100644 index 0000000..c49cd8d --- /dev/null +++ b/hosts/hetzner/vm.nix @@ -0,0 +1,55 @@ +{ lib, ... }: +{ + virtualisation.vmVariant = { + virtualisation = { + memorySize = 4096; + cores = 2; + graphics = false; + sharedDirectories = { + sops-keys = { + source = "/home/louis/.config/sops/age"; + target = "/var/lib/sops-nix"; + }; + }; + forwardPorts = [ + { + from = "host"; + host.port = 8080; + guest.port = 80; + } + ]; + }; + + fileSystems."/mnt/volume" = lib.mkForce { + device = "none"; + fsType = "tmpfs"; + options = [ + "size=2G" + "mode=777" + ]; + }; + + fileSystems."/mnt/box" = lib.mkForce { + device = "none"; + fsType = "tmpfs"; + options = [ + "size=2G" + "mode=777" + ]; + }; + + sops.age.keyFile = lib.mkForce "/var/lib/sops-nix/keys.txt"; + + users.users.root.password = "root"; + services.openssh.settings.PermitRootLogin = "yes"; + services.openssh.settings.PasswordAuthentication = true; + + documentation.enable = false; + systemd.services.NetworkManager-wait-online.enable = false; + + networking.useDHCP = lib.mkDefault true; + + services.qemuGuest.enable = true; + networking.enableIPv6 = false; + }; +} diff --git a/languages/python.nix b/languages/python.nix new file mode 100644 index 0000000..bc8e16b --- /dev/null +++ b/languages/python.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: +pkgs.python3.withPackages (ps: with ps; [ + pynvim + cairosvg + pnglatex + plotly + pyperclip + ipython +]) diff --git a/languages/r.nix b/languages/r.nix new file mode 100644 index 0000000..676a2e9 --- /dev/null +++ b/languages/r.nix @@ -0,0 +1,52 @@ +{ pkgs, ... }: +let + r-packages = with pkgs.rPackages; [ + ggplot2 + BiocManager + ptmixed + reshape2 + opencv + dplyr + stringi + stringr + tidyverse + waffle + languageserver + knitr + rmarkdown + IRkernel + matlib + lme4 + emmeans + multcomp + lobstr + sloop + vioplot + corrplot + brolgar + mvtnorm + doBy + car + rbenchmark + testthat + MASS + faraway + EnvStats + leaps + ISLR + MuMIn + corrplot + caret + skimr + ]; +in +{ + packages = r-packages; + env = pkgs.rWrapper.override { + packages = r-packages; + }; + rstudio = pkgs.rstudioWrapper.override { + packages = r-packages; + }; +} + diff --git a/modules/model-downloads.nix b/modules/model-downloads.nix new file mode 100644 index 0000000..ce6f976 --- /dev/null +++ b/modules/model-downloads.nix @@ -0,0 +1,49 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.modelDownloads; + + script = pkgs.writeShellScript "download-models" '' + set -eu + mkdir -p "${cfg.dir}" + + for url in ${lib.concatStringsSep " " (map lib.escapeShellArg cfg.urls)}; do + name="$(basename "$url")" + out="${cfg.dir}/$name" + + if [ -f "$out" ]; then + echo "OK: $name already exists" + continue + fi + + echo "Downloading $name" + tmp="$out.part" + ${pkgs.curl}/bin/curl -L --fail --retry 3 --retry-delay 2 -o "$tmp" "$url" + mv -f "$tmp" "$out" + done + ''; +in +{ + options.modelDownloads = { + enable = lib.mkEnableOption "Download model files into a persistent directory"; + + dir = lib.mkOption { + type = lib.types.str; + default = "/data/ai/models"; + }; + + urls = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of direct download URLs (filenames are taken from the URL)."; + }; + }; + + config = lib.mkIf cfg.enable { + home.packages = [ pkgs.curl ]; + + home.activation.modelDownloads = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + ${script} + ''; + }; +} diff --git a/secrets.yaml b/secrets.yaml new file mode 100644 index 0000000..a704f9a --- /dev/null +++ b/secrets.yaml @@ -0,0 +1,33 @@ +storage_box_credentials: ENC[AES256_GCM,data:REgBpguvSUAQOrobf0ywPPKcegsclEvqMcplNPh8NrRUVXhviJa6aVeUpzw2L+jelcGM8A==,iv:8nHaRMKCE6YxX0wV85roucRZKjzX7ims1fyLUObNui4=,tag:Xq9hqI6l6v5e58urdTf4rA==,type:str] +immich_db_password: ENC[AES256_GCM,data:+5py95Kqa3RU+jitZbbe82xndsRJ5cZPHravO8xxwzFf,iv:4PJP8itiBFWJ9OsrIHFs3qBK/69LENEtjm73KsqECC8=,tag:W0BD/d3Ebls0LJ5dpYrKVA==,type:str] +immich_jwt_secret: ENC[AES256_GCM,data:ir2gXv515OSGaMmtDGwAbJ+SPVoKf07rBbGhBkmd2zI=,iv:YbFpuFBb2RQDUx0FUXUF+rjfLo0lA1eQAUGnMi0u784=,tag:tAyFUz0hPLf5UmkFkJSmZQ==,type:str] +forgejo_db_password: ENC[AES256_GCM,data:HeOaEGGxIx8ZabhzgzMttxt72gJ3fLaPU+zdXI77t5W81b7Tsbsm/SpelZnnyWTiyQU=,iv:mxg4m9am5c6J5hTcCe7LxLWFLHWJZBpsoN++0eudgXY=,tag:/cFNWRm0xNB/+0pE7Ga39A==,type:str] +wifi_leiden_identity: ENC[AES256_GCM,data:nj9LEiHNwubm2iCc4PnBk/Q79KXEsg==,iv:PLq235mWh0vKUutvZN4+vQu+7dUfrqq76LzuHvaWLg4=,tag:K9pMdUnqMR62QeFRHmNoog==,type:str] +wifi_leiden_password: ENC[AES256_GCM,data:p2eAI6Fe66fhqCS4iz17xkBIpc1i,iv:Yf4ZbmxGAw70aiG06sr3o7m0EtJyDfwKspIiyux9hrA=,tag:3VlUcreKWpTnbmzPJhdxOw==,type:str] +wifi_home: ENC[AES256_GCM,data:yXcc8VGG+WFFEByr,iv:yTAAYCJueAUajrLd9AThypIhTggtEIFJYl9uCpPpXQs=,tag:vJ6T5N0MB8sFrESJ/0Pwtw==,type:str] +git_name: ENC[AES256_GCM,data:iIlG0/JuyJgh8Z4zE8K9X1PGPQ==,iv:po4WCA3p5xEwQdNh5cYycCh1D6mWIxUmIDIUg13aQs0=,tag:SCED1jmBTaH/WRll0anpYg==,type:str] +git_email: ENC[AES256_GCM,data:pCrxo99MHEue1kat6tVFGsaXrA==,iv:itQgTc9HW/tweKylcMHzqwm2yNiNtxYjc0BmGM+b1mY=,tag:Ri+euCueyv+xwEOeswidpg==,type:str] +sops: + age: + - recipient: age1y38cnfvt42ar4seqx8n2cw0ynwls2rdl6jyzhv9wuv6jcfqrwckqwlh9p0 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsRTBPRjJIaHZtOGpDaWJY + SnhOSE5BeGkrOUQwQ3dQRG5SSDRVdXk2ZGdnCjg1Yk5NT01sOElyNyswR0ZzK0JG + cXhHVHhLR2cwRjRJb1A4OWQ4N3VTekkKLS0tIHcwWG9ORlR3TjVpbTZhcm5yYXpj + OG5xRVIvWFJFRjlTR2hRVFpVbFI2em8KdwnBU+SgjLoujtFT7m58JwWJBuRcL+cr + QLtDXAWRjrop1l+cjE1le5nwwGFUYVgUzHWRi5V6tMYrkUyazj76gg== + -----END AGE ENCRYPTED FILE----- + - recipient: age1sjq27nh4znq75fzzx6epwd6700wgg23v9xenzvmam4kmncvjdcesakv9e2 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2YmdRYVFOeGdWR01DbGxk + YUg3UXZXcVpPcXUzL0hQaUd4bW9BOXg2U3pjClB2R1M2UEpiSWVyR0ZTZk5yQXor + eXBKYWVIbzdEQ1dUZFlyWk5tZjZFa1UKLS0tIDkvN0FsaGZLMkNPa1ZxWkk0SlNM + emZCZDM4djFEdDIya3NISElqRU9QUFkKJG1REQS0/HmY1jbOUlt/ynhlLwmrVbkK + mNdhF8BdfP0rCwjWLvWnmikxdLIBHeeeodipZFr5QDpBZqzFHUxBaA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-01-26T00:05:27Z" + mac: ENC[AES256_GCM,data:A0M7dnSZlO3A+vahwjtbSSLwjzTMF/GZjnw385siHeTrT3ICX9nfnh9MzgmT3k7AOsaJkbUuq5dl67pfNEaDxbLdYc6k2rFfyniVFq3t5JVPUr7+F1poijdKMSGrFMv/8uLDc0NlKDDa6VOEX4le7akqBRhAKMCqTZ5atmskwhg=,iv:sPJT9watBgpusk3lGPXLxNWNrMTLITxv1vo11a92uIc=,tag:YPhTUghRjYDZZo3VsYwAFQ==,type:str] + unencrypted_suffix: _unencrypted + version: 3.11.0