chore: reset history
This commit is contained in:
commit
fe44657d8d
213 changed files with 14801 additions and 0 deletions
23
.claude/commands/openspec/apply.md
Normal file
23
.claude/commands/openspec/apply.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: OpenSpec: Apply
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
category: OpenSpec
|
||||
tags: [openspec, apply]
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
|
||||
**Steps**
|
||||
Track these steps as TODOs and complete them one by one.
|
||||
1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
|
||||
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
|
||||
3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
|
||||
4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality.
|
||||
5. Reference `openspec list` or `openspec show <item>` when additional context is required.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
|
||||
<!-- OPENSPEC:END -->
|
||||
27
.claude/commands/openspec/archive.md
Normal file
27
.claude/commands/openspec/archive.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: OpenSpec: Archive
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
category: OpenSpec
|
||||
tags: [openspec, archive]
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
|
||||
**Steps**
|
||||
1. Determine the change ID to archive:
|
||||
- If this prompt already includes a specific change ID (for example inside a `<ChangeId>` block populated by slash-command arguments), use that value after trimming whitespace.
|
||||
- If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
|
||||
- Otherwise, review the conversation, run `openspec list`, and ask the user which change to archive; wait for a confirmed change ID before proceeding.
|
||||
- If you still cannot identify a single change ID, stop and tell the user you cannot archive anything yet.
|
||||
2. Validate the change ID by running `openspec list` (or `openspec show <id>`) and stop if the change is missing, already archived, or otherwise not ready to archive.
|
||||
3. Run `openspec archive <id> --yes` so the CLI moves the change and applies spec updates without prompts (use `--skip-specs` only for tooling-only work).
|
||||
4. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`.
|
||||
5. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec list` to confirm change IDs before archiving.
|
||||
- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
|
||||
<!-- OPENSPEC:END -->
|
||||
27
.claude/commands/openspec/proposal.md
Normal file
27
.claude/commands/openspec/proposal.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: OpenSpec: Proposal
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
category: OpenSpec
|
||||
tags: [openspec, change]
|
||||
---
|
||||
<!-- OPENSPEC:START -->
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
|
||||
|
||||
**Steps**
|
||||
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
|
||||
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes/<id>/`.
|
||||
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
|
||||
4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
|
||||
5. Draft spec deltas in `changes/<id>/specs/<capability>/spec.md` (one folder per capability) using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference related capabilities when relevant.
|
||||
6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
|
||||
7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation fails.
|
||||
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
|
||||
- Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation realities.
|
||||
<!-- OPENSPEC:END -->
|
||||
16
.claude/settings.local.json
Normal file
16
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(openspec list:*)",
|
||||
"Bash(openspec validate:*)",
|
||||
"Bash(nmcli device:*)",
|
||||
"Bash(nmcli connection show:*)",
|
||||
"Bash(iw dev:*)",
|
||||
"Bash(pmtiles tile:*)",
|
||||
"Bash(nix-shell:*)"
|
||||
],
|
||||
"deny": [
|
||||
"Read(./secrets/**)"
|
||||
]
|
||||
}
|
||||
}
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.aider*
|
||||
.DS_Store
|
||||
.opencode
|
||||
.secrets.env
|
||||
.playwright-mcp/
|
||||
tmp
|
||||
scripts/
|
||||
docs/
|
||||
46
README.md
Normal file
46
README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<h1 align="center">dotfiles</h1>
|
||||
|
||||
## Why?
|
||||
|
||||
- A single repository that manages all my machines: Framework13, Steam Deck, other potential servers
|
||||
- Must be able to:
|
||||
- apply configuration to any machine from my main Framework13 laptop
|
||||
- encrypt and store secrets I might need here
|
||||
- define common service definition and decide which machine gets which service / config
|
||||
- Must be able to manage home directory, dotfiles and `~/.config` via this repository
|
||||
- dislike symlinking
|
||||
|
||||
## The current setup
|
||||
|
||||
System Management: [NixOS](https://nixos.org/)
|
||||
- replacing all dotfile management with NixOS Home Manager
|
||||
|
||||
Gaming System Management: [Jovian-NixOS](https://github.com/Jovian-Experiments/Jovian-NixOS)
|
||||
|
||||
Syncthing for syncing all my devices
|
||||
|
||||
Terminal Emulator: [Ghostty](https://ghostty.org/)
|
||||
- Zig-based terminal emulator
|
||||
- uses platform-native UI & GPU acceleration (e.g. MacOS secure input for passwords)
|
||||
|
||||
Shell: [fish](https://fishshell.com/)
|
||||
- completions
|
||||
- sane scripting
|
||||
- user friendly
|
||||
|
||||
Text Editor: [neovim](https://neovim.io/)
|
||||
- vi-movements
|
||||
- great community, loads of completions
|
||||
|
||||
Secrets Management: [sops-nix](https://github.com/Mic92/sops-nix)
|
||||
- easy to manage AGE keys
|
||||
- easy to integrate into my git repository (RIP me when we get quantum chips breaking encryption)
|
||||
|
||||
Code Completion & Language Servers: Neovim LSP & [Mason](https://github.com/williamboman/mason.nvim)
|
||||
Colorscheme: [Rosé Pine](https://rosepinetheme.com/)
|
||||
|
||||
## Rice 米
|
||||
|
||||

|
||||
|
||||

|
||||
BIN
assets/rice.png
Normal file
BIN
assets/rice.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
BIN
assets/rice_light.png
Normal file
BIN
assets/rice_light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
2223
flake.lock
generated
Normal file
2223
flake.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
152
flake.nix
Normal file
152
flake.nix
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
{
|
||||
description = "Framework 13 NixOS Configuration";
|
||||
|
||||
outputs = inputs:
|
||||
inputs.flake-parts.lib.mkFlake {inherit inputs;} {
|
||||
systems = ["x86_64-linux"];
|
||||
perSystem = { pkgs, system, ... }: {
|
||||
_module.args.pkgs = import inputs.nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ ];
|
||||
config = { };
|
||||
};
|
||||
|
||||
# Boox eReader and Android device management shell
|
||||
devShells.default = import ./shell.nix {
|
||||
inherit pkgs inputs;
|
||||
};
|
||||
};
|
||||
|
||||
imports = [
|
||||
./hosts
|
||||
];
|
||||
};
|
||||
|
||||
inputs = {
|
||||
devenv = {
|
||||
url = "github:cachix/devenv";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Declarative disk partitioning (for phantom USB)
|
||||
disko = {
|
||||
url = "github:nix-community/disko";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
elephant.url = "github:abenz1267/elephant";
|
||||
|
||||
flake-parts = {
|
||||
url = "github:hercules-ci/flake-parts";
|
||||
inputs.nixpkgs-lib.follows = "nixpkgs";
|
||||
};
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager/release-26.05";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Home Manager for Steam Deck (follows unstable for Jovian-NixOS compatibility)
|
||||
home-manager-unstable = {
|
||||
url = "github:nix-community/home-manager";
|
||||
inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||
};
|
||||
|
||||
hypridle = {
|
||||
url = "github:hyprwm/hypridle";
|
||||
inputs = {
|
||||
hyprlang.follows = "hyprland/hyprlang";
|
||||
hyprutils.follows = "hyprland/hyprutils";
|
||||
nixpkgs.follows = "hyprland/nixpkgs";
|
||||
systems.follows = "hyprland/systems";
|
||||
};
|
||||
};
|
||||
|
||||
# Hyprland
|
||||
hyprland.url = "github:hyprwm/hyprland";
|
||||
|
||||
hyprland-contrib = {
|
||||
url = "github:hyprwm/contrib";
|
||||
inputs.nixpkgs.follows = "hyprland/nixpkgs";
|
||||
};
|
||||
|
||||
hyprlock = {
|
||||
url = "github:hyprwm/hyprlock";
|
||||
inputs = {
|
||||
hyprgraphics.follows = "hyprland/hyprgraphics";
|
||||
hyprlang.follows = "hyprland/hyprlang";
|
||||
hyprutils.follows = "hyprland/hyprutils";
|
||||
nixpkgs.follows = "hyprland/nixpkgs";
|
||||
systems.follows = "hyprland/systems";
|
||||
};
|
||||
};
|
||||
|
||||
hyprpaper = {
|
||||
url = "github:hyprwm/hyprpaper";
|
||||
inputs = {
|
||||
hyprgraphics.follows = "hyprland/hyprgraphics";
|
||||
hyprlang.follows = "hyprland/hyprlang";
|
||||
hyprutils.follows = "hyprland/hyprutils";
|
||||
nixpkgs.follows = "hyprland/nixpkgs";
|
||||
systems.follows = "hyprland/systems";
|
||||
};
|
||||
};
|
||||
|
||||
# Ephemeral root with selective persistence (for phantom USB)
|
||||
impermanence.url = "github:nix-community/impermanence";
|
||||
|
||||
# Steam Deck support via Jovian-NixOS
|
||||
# Note: Don't override nixpkgs to ensure cache hits for pre-built binaries
|
||||
jovian-nixos.url = "github:Jovian-Experiments/Jovian-NixOS";
|
||||
|
||||
mcp-servers-nix = {
|
||||
url = "github:natsukium/mcp-servers-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Minecraft declarative mod management
|
||||
nix-minecraft = {
|
||||
url = "github:Infinidoge/nix-minecraft";
|
||||
inputs.nixpkgs.follows = "nixpkgs-unstable";
|
||||
};
|
||||
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
|
||||
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
|
||||
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
nur = {
|
||||
url = "github:nix-community/NUR";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# Path to your local nvim configuration folder
|
||||
nvchad-config = {
|
||||
url = "path:./home/editors/nvim/nvchad";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
nix4nvchad = {
|
||||
url = "github:nix-community/nix4nvchad";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.nvchad-starter.follows = "nvchad-config"; # Overrides the starter
|
||||
};
|
||||
|
||||
openapi-tui.url = "github:zaghaghi/openapi-tui";
|
||||
|
||||
secrets.url = "git+ssh://forgejo@git.oolongtechnologies.io/bogdan/secrets.git";
|
||||
|
||||
# Secrets management
|
||||
sops-nix = {
|
||||
url = "github:Mic92/sops-nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
stylix = {
|
||||
url = "github:nix-community/stylix/release-26.05";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
walker = {
|
||||
url = "github:abenz1267/walker";
|
||||
inputs.elephant.follows = "elephant";
|
||||
};
|
||||
yazi.url = "github:sxyazi/yazi"; };
|
||||
}
|
||||
29
home/default.nix
Normal file
29
home/default.nix
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./terminal
|
||||
./terminal/emulators/ghostty.nix
|
||||
];
|
||||
|
||||
home = {
|
||||
username = "bogdan";
|
||||
homeDirectory = "/home/bogdan";
|
||||
stateVersion = "25.11";
|
||||
|
||||
pointerCursor = {
|
||||
name = "BreezeX-RosePine-Linux";
|
||||
package = pkgs.rose-pine-cursor;
|
||||
size = 24;
|
||||
gtk.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Symlink hyprcursor theme into ~/.local/share/icons/ so hyprcursor
|
||||
# can discover it even when XDG_DATA_DIRS is empty (UWSM sessions).
|
||||
xdg.dataFile."icons/rose-pine-hyprcursor".source =
|
||||
"${pkgs.rose-pine-hyprcursor}/share/icons/rose-pine-hyprcursor";
|
||||
|
||||
# Let Home Manager install and manage itself
|
||||
programs.home-manager.enable = true;
|
||||
}
|
||||
66
home/editors/nvim/default.nix
Normal file
66
home/editors/nvim/default.nix
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
# vimAlias replacement: nix4nvchad only installs `nvim`, so map `vim` → nvim.
|
||||
home.shellAliases.vim = "nvim";
|
||||
|
||||
programs.nvchad = {
|
||||
enable = true;
|
||||
|
||||
# Pin plugin commits reproducibly. Without this, nix4nvchad installs an
|
||||
# EMPTY lazy-lock.json into the store config, which nvchad.sh's cleanup_lock
|
||||
# then deletes at launch — so lazy.nvim resolves plugin HEADs freshly on
|
||||
# every machine. Feeding the committed lockfile here lands it in
|
||||
# ~/.config/nvim/lazy-lock.json so `:Lazy restore` pins to known-good commits.
|
||||
# Refresh it with: cp ~/.local/share/nvim/lazy-lock.json ./nvchad/lazy-lock.json
|
||||
lazy-lock = builtins.readFile ./nvchad/lazy-lock.json;
|
||||
|
||||
# image.nvim uses processor = "magick_rock", which require()s the `magick`
|
||||
# LuaRock. nix4nvchad has no --cmd/initLua hook, but image.nvim is
|
||||
# ft = { "markdown" } (lazy), so injecting package.path here in extraConfig
|
||||
# (appended to init.lua at startup) runs before any markdown buffer opens.
|
||||
# The hard-coded store path makes this independent of neovim's LUA_PATH
|
||||
# wrapper, which was unreliable on 0.12.
|
||||
extraConfig = ''
|
||||
package.path = package.path
|
||||
.. ";${pkgs.luajitPackages.magick}/share/lua/5.1/?.lua"
|
||||
.. ";${pkgs.luajitPackages.magick}/share/lua/5.1/?/init.lua"
|
||||
'';
|
||||
|
||||
extraPackages = with pkgs; [
|
||||
clang
|
||||
|
||||
# formatters
|
||||
gofumpt
|
||||
goimports-reviser
|
||||
golines
|
||||
|
||||
# gopls
|
||||
gopls
|
||||
|
||||
# tools
|
||||
gh # GitHub CLI — octo.nvim's API backend (token → OS keyring)
|
||||
go
|
||||
|
||||
# image.nvim — inline images in markdown via Ghostty's kitty graphics
|
||||
imagemagick
|
||||
|
||||
# test runner
|
||||
python3Packages.pytest
|
||||
|
||||
# nix
|
||||
nixfmt # was nixfmt-rfc-style; aliased to pkgs.nixfmt in 26.05
|
||||
nil
|
||||
|
||||
# lua (lua_ls is enabled by NvChad defaults)
|
||||
lua-language-server
|
||||
stylua
|
||||
|
||||
# ts
|
||||
prettierd
|
||||
|
||||
# html template formatters
|
||||
djlint
|
||||
];
|
||||
};
|
||||
}
|
||||
24
home/editors/nvim/nvchad/LICENSE
Normal file
24
home/editors/nvim/nvchad/LICENSE
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org>
|
||||
43
home/editors/nvim/nvchad/init.lua
Normal file
43
home/editors/nvim/nvchad/init.lua
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
vim.g.base46_cache = vim.fn.stdpath("data") .. "/base46/"
|
||||
vim.g.mapleader = "\\"
|
||||
-- Distinct localleader for octo.nvim's <localleader>-prefixed review keys.
|
||||
-- Must be set before lazy.setup() so plugin keymaps register against it.
|
||||
vim.g.maplocalleader = ","
|
||||
|
||||
-- bootstrap lazy and all plugins
|
||||
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
|
||||
|
||||
if not vim.uv.fs_stat(lazypath) then
|
||||
local repo = "https://github.com/folke/lazy.nvim.git"
|
||||
vim.fn.system({ "git", "clone", "--filter=blob:none", repo, "--branch=stable", lazypath })
|
||||
end
|
||||
|
||||
vim.opt.rtp:prepend(lazypath)
|
||||
|
||||
local lazy_config = require("configs.lazy")
|
||||
|
||||
vim.opt.relativenumber = true
|
||||
|
||||
-- load plugins
|
||||
require("lazy").setup({
|
||||
{
|
||||
"NvChad/NvChad",
|
||||
lazy = false,
|
||||
branch = "v2.5",
|
||||
import = "nvchad.plugins",
|
||||
},
|
||||
|
||||
{ import = "plugins" },
|
||||
}, lazy_config)
|
||||
|
||||
-- load theme (recompile to pick up external theme changes from theme-switch)
|
||||
require("base46").compile()
|
||||
dofile(vim.g.base46_cache .. "defaults")
|
||||
dofile(vim.g.base46_cache .. "statusline")
|
||||
|
||||
require("options")
|
||||
require("nvchad.autocmds")
|
||||
|
||||
vim.schedule(function()
|
||||
require("mappings")
|
||||
end)
|
||||
50
home/editors/nvim/nvchad/lazy-lock.json
Normal file
50
home/editors/nvim/nvchad/lazy-lock.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"FixCursorHold.nvim": { "branch": "master", "commit": "1900f89dc17c603eec29960f57c00bd9ae696495" },
|
||||
"NvChad": { "branch": "v2.5", "commit": "d042cc975247c2aa55fcb228e5d146dc1dc6c648" },
|
||||
"base46": { "branch": "v3.0", "commit": "884b990dcdbe07520a0892da6ba3e8d202b46337" },
|
||||
"blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" },
|
||||
"conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" },
|
||||
"edgy.nvim": { "branch": "main", "commit": "8bfd2808994a988c975694122f68624b8a219f5f" },
|
||||
"friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" },
|
||||
"gitsigns.nvim": { "branch": "main", "commit": "2038c666bd9d8a0b7349a0b6ee00dc83104b9ecf" },
|
||||
"hardtime.nvim": { "branch": "main", "commit": "b4e431934af1fe224a3a801f632c008278cb7628" },
|
||||
"harpoon": { "branch": "harpoon2", "commit": "87b1a3506211538f460786c23f98ec63ad9af4e5" },
|
||||
"image.nvim": { "branch": "master", "commit": "88351f1f7d9dbae286e671ce3690a49660dd8a5c" },
|
||||
"indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" },
|
||||
"jj.nvim": { "branch": "main", "commit": "cffc74128a80398469085e01ff2e4001e330b800" },
|
||||
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
|
||||
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
|
||||
"menu": { "branch": "main", "commit": "7a0a4a2896b715c066cfbe320bdc048091874cc6" },
|
||||
"minty": { "branch": "main", "commit": "aafc9e8e0afe6bf57580858a2849578d8d8db9e0" },
|
||||
"neotest": { "branch": "master", "commit": "ad991822b7076b1d940b33a9d6d0d30416d5df81" },
|
||||
"neotest-go": { "branch": "main", "commit": "59b50505053f9c45a9febb79e11a56206c3e3901" },
|
||||
"neotest-python": { "branch": "master", "commit": "e6df4f1892f6137f58135917db24d1655937d831" },
|
||||
"neotest-vitest": { "branch": "main", "commit": "2f19d215d7d4d7f121826f99f6f35a81e2c18a6f" },
|
||||
"noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" },
|
||||
"nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" },
|
||||
"nvim-cmp": { "branch": "main", "commit": "a1d504892f2bc56c2e79b65c6faded2fd21f3eca" },
|
||||
"nvim-lightbulb": { "branch": "master", "commit": "b8c08c5f3b1586dfcdd9f34d7d54fe6982e01ac9" },
|
||||
"nvim-lspconfig": { "branch": "master", "commit": "bfcc0171a43f22afa61d927ffe9fcb6cb85dc99e" },
|
||||
"nvim-nio": { "branch": "master", "commit": "21f5324bfac14e22ba26553caf69ec76ae8a7662" },
|
||||
"nvim-notify": { "branch": "master", "commit": "8701bece920b38ea289b457f902e2ad184131a5d" },
|
||||
"nvim-tree.lua": { "branch": "master", "commit": "8e8be709c9a93e1ebefde7964884a8b564bfd3e0" },
|
||||
"nvim-treesitter": { "branch": "main", "commit": "4916d6592ede8c07973490d9322f187e07dfefac" },
|
||||
"nvim-treesitter-textobjects": { "branch": "main", "commit": "851e865342e5a4cb1ae23d31caf6e991e1c99f1e" },
|
||||
"nvim-web-devicons": { "branch": "master", "commit": "dfbfaa967a6f7ec50789bead7ef87e336c1fa63c" },
|
||||
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
|
||||
"render-markdown.nvim": { "branch": "main", "commit": "f422cb5c6855f150e2ddcfaf44e7157b98b34f6a" },
|
||||
"telescope-fzf-native.nvim": { "branch": "main", "commit": "b25b749b9db64d375d782094e2b9dce53ad53a40" },
|
||||
"telescope-undo.nvim": { "branch": "main", "commit": "928d0c2dc9606e01e2cc547196f48d2eaecf58e5" },
|
||||
"telescope.nvim": { "branch": "master", "commit": "427b576c16792edad01a92b89721d923c19ad60f" },
|
||||
"trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" },
|
||||
"twilight.nvim": { "branch": "main", "commit": "664e752f4a219801265cc3fc18782b457b58c1e1" },
|
||||
"ui": { "branch": "v3.0", "commit": "3e67e9d5325fd47fdbc90ca00a147db2f3525754" },
|
||||
"vim-fugitive": { "branch": "master", "commit": "3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0" },
|
||||
"vim-hugo": { "branch": "master", "commit": "324fb8c7371d31701349c1192e25a0bdcf9898f8" },
|
||||
"vim-rhubarb": { "branch": "master", "commit": "5496d7c94581c4c9ad7430357449bb57fc59f501" },
|
||||
"vim-xcode": { "branch": "main", "commit": "24dc91d2f0e4736231011231d93855ca28c3f921" },
|
||||
"volt": { "branch": "main", "commit": "620de1321f275ec9d80028c68d1b88b409c0c8b1" },
|
||||
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" },
|
||||
"yaml.nvim": { "branch": "main", "commit": "43ddedd0f3b060b4475e19e762b1796fce1b23be" },
|
||||
"zen-mode.nvim": { "branch": "main", "commit": "8564ce6d29ec7554eb9df578efa882d33b3c23a7" }
|
||||
}
|
||||
42
home/editors/nvim/nvchad/lua/chadrc.lua
Normal file
42
home/editors/nvim/nvchad/lua/chadrc.lua
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
-- This file needs to have same structure as nvconfig.lua
|
||||
-- https://github.com/NvChad/ui/blob/v3.0/lua/nvconfig.lua
|
||||
-- Please read that file to know all available options :(
|
||||
|
||||
-- Read theme from file (set by theme-switch script)
|
||||
local function get_theme()
|
||||
local f = io.open(os.getenv("HOME") .. "/.config/theme/nvim-theme", "r")
|
||||
if f then
|
||||
local name = f:read("*l")
|
||||
f:close()
|
||||
if name and name ~= "" then return name end
|
||||
end
|
||||
return "catppuccin"
|
||||
end
|
||||
|
||||
local function get_background()
|
||||
local f = io.open(os.getenv("HOME") .. "/.config/theme/nvim-bg", "r")
|
||||
if f then
|
||||
local bg = f:read("*l")
|
||||
f:close()
|
||||
if bg and bg ~= "" then
|
||||
vim.o.background = bg
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
get_background()
|
||||
|
||||
---@type ChadrcConfig
|
||||
local M = {}
|
||||
|
||||
M.base46 = {
|
||||
theme = get_theme(),
|
||||
transparency = true,
|
||||
|
||||
hl_override = {
|
||||
Comment = { italic = true },
|
||||
["@comment"] = { italic = true },
|
||||
},
|
||||
}
|
||||
|
||||
return M
|
||||
27
home/editors/nvim/nvchad/lua/configs/blink.lua
Normal file
27
home/editors/nvim/nvchad/lua/configs/blink.lua
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
return {
|
||||
enabled = function() return not vim.tbl_contains({ "typr" }, vim.bo.filetype)
|
||||
and vim.bo.buftype ~= "prompt"
|
||||
and vim.b.completion ~= false
|
||||
end,
|
||||
keymap = {
|
||||
["<C-n>"] = { "show", "hide" },
|
||||
["<C-p>"] = { "show_documentation", "hide_documentation" },
|
||||
|
||||
["<S-Tab>"] = { "select_prev", "fallback" },
|
||||
["<Tab>"] = { "select_next", "fallback" },
|
||||
["<CR>"] = { "select_and_accept", "fallback" },
|
||||
},
|
||||
completion = {
|
||||
menu = {
|
||||
draw = {
|
||||
treesitter = { "lsp" }
|
||||
}
|
||||
},
|
||||
ghost_text = {
|
||||
enabled = true,
|
||||
},
|
||||
},
|
||||
sources = {
|
||||
default = { "lsp", "path", "snippets", "buffer" },
|
||||
}
|
||||
}
|
||||
37
home/editors/nvim/nvchad/lua/configs/conform.lua
Normal file
37
home/editors/nvim/nvchad/lua/configs/conform.lua
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
local options = {
|
||||
formatters_by_ft = {
|
||||
lua = { "stylua" },
|
||||
css = { "prettier" },
|
||||
html = { "prettier" },
|
||||
htmlhugo = { "djlint" },
|
||||
python = { "ruff_format", "ruff_fix", "ruff_organize_imports" },
|
||||
tf = { "tflint", "terraform-ls" },
|
||||
go = { "goimports", "gofmt" },
|
||||
json = { "jq" },
|
||||
templ = { "templ" },
|
||||
javascript = { "prettierd" },
|
||||
typescript = { "prettierd" },
|
||||
typescriptReact = { "prettierd" },
|
||||
yaml = { "gh-actions-language-server" },
|
||||
zig = { "zls" },
|
||||
},
|
||||
formatters = {
|
||||
djlint = {
|
||||
prepend_args = { "--profile", "golang", "--indent", "2", "--preserve-blank-lines" },
|
||||
},
|
||||
},
|
||||
|
||||
format_on_save = {
|
||||
-- These options will be passed to conform.format()
|
||||
timeout_ms = 500,
|
||||
lsp_format = "fallback",
|
||||
},
|
||||
-- Set the log level. Use `:ConformInfo` to see the location of the log file.
|
||||
log_level = vim.log.levels.ERROR,
|
||||
-- Conform will notify you when a formatter errors
|
||||
notify_on_error = true,
|
||||
-- Conform will notify you when no formatters are available for the buffer
|
||||
notify_no_formatters = true,
|
||||
}
|
||||
|
||||
return options
|
||||
68
home/editors/nvim/nvchad/lua/configs/diffview.lua
Normal file
68
home/editors/nvim/nvchad/lua/configs/diffview.lua
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
-- diffview.nvim — whole-branch / PR diff review (file-tree panel + per-file diff).
|
||||
-- Keymaps live in the plugin spec (lua/plugins/init.lua) under the <leader>r group.
|
||||
--
|
||||
-- Navigation is all builtin: ]c/[c walk hunks in a file (native diff motions,
|
||||
-- restored buffer-locally in mappings.lua), <Tab>/<S-Tab> next/prev file,
|
||||
-- <leader>e focus the file panel, gf open the file for real edits.
|
||||
|
||||
-- Hunk-level stage/reset via gitsigns, which lives on the RIGHT pane because
|
||||
-- --imply-local makes it the live working-tree buffer. The left pane is a git
|
||||
-- blob with no gitsigns state — guard instead of erroring there.
|
||||
-- Note: gitsigns hunks are worktree-vs-index, matching the <leader>rw review;
|
||||
-- in a base...HEAD review already-committed hunks have nothing to stage.
|
||||
local function gs_hunk(fn_name, visual)
|
||||
return function()
|
||||
if not vim.b.gitsigns_status_dict then
|
||||
vim.notify("No gitsigns here — stage/reset from the local (right) pane", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
local gs = require("gitsigns")
|
||||
if visual then
|
||||
gs[fn_name]({ vim.fn.line("."), vim.fn.line("v") })
|
||||
else
|
||||
gs[fn_name]()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
enhanced_diff_hl = true,
|
||||
|
||||
-- --imply-local makes the RIGHT diff pane the live working-tree buffer, so
|
||||
-- LSP, gd, formatting — and gitsigns staging — work inside the review.
|
||||
-- Baked into the default so PR-range opens always get it.
|
||||
default_args = {
|
||||
DiffviewOpen = { "--imply-local" },
|
||||
},
|
||||
|
||||
-- Tree listing reads best for big multi-file PRs.
|
||||
file_panel = {
|
||||
listing_style = "tree",
|
||||
},
|
||||
|
||||
hooks = {
|
||||
-- gitsigns is lazy (NvChad: User FilePost) and attaches from a BufRead
|
||||
-- autocmd; diffview loads the local buffer in a hidden temp window, so
|
||||
-- when gitsigns isn't loaded yet that autocmd never ran for this buffer
|
||||
-- and hunk stage/reset silently has no state. Force load + attach here;
|
||||
-- attach is a no-op when already attached and rejects non-file buffers
|
||||
-- (left-pane blobs), so this is safe to fire for every diff buffer.
|
||||
diff_buf_read = function(bufnr)
|
||||
pcall(function()
|
||||
require("gitsigns").attach(bufnr)
|
||||
end)
|
||||
end,
|
||||
},
|
||||
|
||||
keymaps = {
|
||||
view = {
|
||||
-- `-` mirrors the file panel's `-` (stage entry) at hunk granularity;
|
||||
-- whole-file staging stays on the file panel's `-`.
|
||||
{ "n", "-", gs_hunk("stage_hunk"), { desc = "Stage/unstage hunk" } },
|
||||
{ "v", "-", gs_hunk("stage_hunk", true), { desc = "Stage selected lines" } },
|
||||
-- `X` mirrors the file panel's `X` (restore entry) at hunk granularity.
|
||||
{ "n", "X", gs_hunk("reset_hunk"), { desc = "Reset hunk" } },
|
||||
{ "v", "X", gs_hunk("reset_hunk", true), { desc = "Reset selected lines" } },
|
||||
},
|
||||
},
|
||||
}
|
||||
62
home/editors/nvim/nvchad/lua/configs/edgy.lua
Normal file
62
home/editors/nvim/nvchad/lua/configs/edgy.lua
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
-- edgy.nvim — three-rail layout contract:
|
||||
-- left = project navigation (file tree, diffview panels, git status)
|
||||
-- bottom = transient output (terminal, quickfix, diagnostics, test output)
|
||||
-- right = code context while reading (symbols outline, LSP refs/defs, test summary)
|
||||
--
|
||||
-- Trouble placement: trouble's mode config (plugins/init.lua) is the single
|
||||
-- source of truth for which edge a mode opens at; the filters below make edgy
|
||||
-- adopt each trouble window at that edge. A bare ft=trouble entry instead
|
||||
-- RELOCATES every trouble window to one hardcoded rail while trouble itself
|
||||
-- keeps opening at its configured position — that tug-of-war is what made
|
||||
-- references land on different rails depending on which edgebars were open.
|
||||
local function trouble_at(pos)
|
||||
return {
|
||||
ft = "trouble",
|
||||
filter = function(_, win)
|
||||
local t = vim.w[win].trouble
|
||||
return t ~= nil and t.position == pos and t.type == "split" and t.relative == "editor"
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Edgy should only manage real splits; floats (NvTerm_float, pickers) pass through.
|
||||
local function is_split(_, win)
|
||||
return vim.api.nvim_win_get_config(win).relative == ""
|
||||
end
|
||||
|
||||
return {
|
||||
animate = { enabled = false },
|
||||
exit_when_last = false,
|
||||
|
||||
left = {
|
||||
-- height 0.5 keeps the tree at half the rail when a diffview/fugitive
|
||||
-- panel stacks below it during review
|
||||
{ ft = "NvimTree", size = { height = 0.5 } },
|
||||
{ ft = "DiffviewFiles", title = "Diff Files" },
|
||||
{ ft = "DiffviewFileHistory", title = "Diff History" },
|
||||
{ ft = "fugitive" },
|
||||
{ ft = "gitcommit" },
|
||||
},
|
||||
|
||||
bottom = {
|
||||
-- NvChad's term module tags horizontal terminals NvTerm_sp (there is no
|
||||
-- toggleterm in this config); vsp/float terminals are left alone
|
||||
{ ft = "NvTerm_sp", title = "Terminal" },
|
||||
trouble_at("bottom"), -- diagnostics / qflist / loclist modes
|
||||
{ ft = "qf", title = "Quickfix" },
|
||||
{ ft = "neotest-output-panel", title = "Test Output" },
|
||||
-- noice's long-message splits (presets.long_message_to_split)
|
||||
{ ft = "noice", filter = is_split },
|
||||
},
|
||||
|
||||
right = {
|
||||
trouble_at("right"), -- symbols outline, lsp defs/refs panels
|
||||
{ ft = "neotest-summary", title = "Tests" },
|
||||
},
|
||||
|
||||
options = {
|
||||
left = { size = 35 },
|
||||
bottom = { size = 0.3 },
|
||||
right = { size = 0.3 },
|
||||
},
|
||||
}
|
||||
99
home/editors/nvim/nvchad/lua/configs/gitsigns.lua
Normal file
99
home/editors/nvim/nvchad/lua/configs/gitsigns.lua
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
return {
|
||||
signs = {
|
||||
add = { text = "│" },
|
||||
change = { text = "│" },
|
||||
delete = { text = "▁" },
|
||||
topdelete = { text = "▔" },
|
||||
changedelete = { text = "~" },
|
||||
untracked = { text = "┆" },
|
||||
},
|
||||
signcolumn = true,
|
||||
-- Open hunk qflist in the native quickfix (ft=qf → edgy bottom panel),
|
||||
-- not Trouble. Gitsigns auto-enables this whenever trouble.nvim is installed
|
||||
-- (defaults to pcall(require,'trouble')), which routed setqflist to the
|
||||
-- right-side Trouble panel instead of the bottom qf rule in edgy.lua.
|
||||
trouble = false,
|
||||
numhl = true,
|
||||
linehl = false,
|
||||
word_diff = false,
|
||||
watch_gitdir = {
|
||||
follow_files = true,
|
||||
},
|
||||
auto_attach = true,
|
||||
attach_to_untracked = true,
|
||||
current_line_blame = true,
|
||||
current_line_blame_opts = {
|
||||
virt_text = true,
|
||||
virt_text_pos = "eol",
|
||||
delay = 1000,
|
||||
ignore_whitespace = false,
|
||||
virt_text_priority = 100,
|
||||
},
|
||||
current_line_blame_formatter = "<author>, <author_time:%Y-%m-%d> - <summary>",
|
||||
sign_priority = 6,
|
||||
update_debounce = 100,
|
||||
status_formatter = nil,
|
||||
max_file_length = 40000,
|
||||
preview_config = {
|
||||
border = "rounded",
|
||||
style = "minimal",
|
||||
relative = "cursor",
|
||||
row = 0,
|
||||
col = 1,
|
||||
},
|
||||
on_attach = function(bufnr)
|
||||
local gs = package.loaded.gitsigns
|
||||
|
||||
local function map(mode, l, r, opts)
|
||||
opts = opts or {}
|
||||
opts.buffer = bufnr
|
||||
vim.keymap.set(mode, l, r, opts)
|
||||
end
|
||||
|
||||
-- Navigation (use ]g/[g to avoid conflict with treesitter ]c/[c).
|
||||
-- In diff windows gitsigns hunks (vs index) don't match what the diff
|
||||
-- shows, so delegate to the native ]c/[c change motion there. (]g has no
|
||||
-- native meaning — the old `normal! ]g` was a no-op.)
|
||||
map("n", "]g", function()
|
||||
if vim.wo.diff then
|
||||
vim.cmd.normal({ "]c", bang = true })
|
||||
else
|
||||
gs.nav_hunk("next")
|
||||
end
|
||||
end, { desc = "Next hunk" })
|
||||
|
||||
map("n", "[g", function()
|
||||
if vim.wo.diff then
|
||||
vim.cmd.normal({ "[c", bang = true })
|
||||
else
|
||||
gs.nav_hunk("prev")
|
||||
end
|
||||
end, { desc = "Previous hunk" })
|
||||
|
||||
-- Jump to first/last hunk in buffer (review sweeps)
|
||||
map("n", "]G", function() gs.nav_hunk("last") end, { desc = "Last hunk" })
|
||||
map("n", "[G", function() gs.nav_hunk("first") end, { desc = "First hunk" })
|
||||
|
||||
-- Actions
|
||||
map("n", "<leader>gb", gs.blame, { desc = "Blame" })
|
||||
map("n", "<leader>hs", gs.stage_hunk, { desc = "Stage hunk" })
|
||||
map("n", "<leader>hr", gs.reset_hunk, { desc = "Reset hunk" })
|
||||
map("v", "<leader>hs", function() gs.stage_hunk({ vim.fn.line("."), vim.fn.line("v") }) end, { desc = "Stage selected hunk" })
|
||||
map("v", "<leader>hr", function() gs.reset_hunk({ vim.fn.line("."), vim.fn.line("v") }) end, { desc = "Reset selected hunk" })
|
||||
map("n", "<leader>hS", gs.stage_buffer, { desc = "Stage buffer" })
|
||||
map("n", "<leader>hR", gs.reset_buffer, { desc = "Reset buffer" })
|
||||
map("n", "<leader>hp", gs.preview_hunk, { desc = "Preview hunk" })
|
||||
map("n", "<leader>hi", gs.preview_hunk_inline, { desc = "Preview hunk inline" })
|
||||
map("n", "<leader>hb", function() gs.blame_line({ full = true }) end, { desc = "Blame line" })
|
||||
map("n", "<leader>tb", gs.toggle_current_line_blame, { desc = "Toggle line blame" })
|
||||
map("n", "<leader>tw", gs.toggle_word_diff, { desc = "Toggle word diff" })
|
||||
map("n", "<leader>hd", gs.diffthis, { desc = "Diff this" })
|
||||
map("n", "<leader>hD", function() gs.diffthis("~") end, { desc = "Diff against HEAD~" })
|
||||
map("n", "<leader>hQ", function() gs.setqflist("all") end, { desc = "Hunks as qflist (all)" })
|
||||
map("n", "<leader>hq", gs.setqflist, { desc = "Hunks as qflist (buffer)" })
|
||||
map("n", "<leader>hu", gs.toggle_deleted, { desc = "Toggle deleted" })
|
||||
|
||||
-- Text object
|
||||
map({ "o", "x" }, "ih", gs.select_hunk, { desc = "Select hunk inner" })
|
||||
end,
|
||||
}
|
||||
57
home/editors/nvim/nvchad/lua/configs/lazy.lua
Normal file
57
home/editors/nvim/nvchad/lua/configs/lazy.lua
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
return {
|
||||
defaults = { lazy = true },
|
||||
install = { colorscheme = { "nvchad" } },
|
||||
|
||||
-- Read the lockfile nix4nvchad places in the config dir (= stdpath("config")),
|
||||
-- so the Nix-pinned commits are what lazy.nvim restores. The old stdpath("data")
|
||||
-- path was a workaround for the readonly /nix/store symlink era; under nvchad the
|
||||
-- config dir is a writable copy, so point back here.
|
||||
lockfile = vim.fn.stdpath("config") .. "/lazy-lock.json",
|
||||
|
||||
-- NixOS can't run lazy's hererocks/luarocks installer (sandboxed store);
|
||||
-- no plugins here need it, so disable to silence the healthcheck error.
|
||||
rocks = { enabled = false },
|
||||
|
||||
ui = {
|
||||
icons = {
|
||||
ft = "",
|
||||
lazy = " ",
|
||||
loaded = "",
|
||||
not_loaded = "",
|
||||
},
|
||||
},
|
||||
|
||||
performance = {
|
||||
rtp = {
|
||||
disabled_plugins = {
|
||||
"2html_plugin",
|
||||
"tohtml",
|
||||
"getscript",
|
||||
"getscriptPlugin",
|
||||
"gzip",
|
||||
"logipat",
|
||||
"netrw",
|
||||
"netrwPlugin",
|
||||
"netrwSettings",
|
||||
"netrwFileHandlers",
|
||||
"matchit",
|
||||
"tar",
|
||||
"tarPlugin",
|
||||
"rrhelper",
|
||||
"spellfile_plugin",
|
||||
"vimball",
|
||||
"vimballPlugin",
|
||||
"zip",
|
||||
"zipPlugin",
|
||||
"tutor",
|
||||
"rplugin",
|
||||
"syntax",
|
||||
"synmenu",
|
||||
"optwin",
|
||||
"compiler",
|
||||
"bugreport",
|
||||
"ftplugin",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
30
home/editors/nvim/nvchad/lua/configs/lightbulb.lua
Normal file
30
home/editors/nvim/nvchad/lua/configs/lightbulb.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
return {
|
||||
sign = {
|
||||
enabled = true,
|
||||
text = "",
|
||||
texthl = "DiagnosticSignHint",
|
||||
linehl = "",
|
||||
numhl = "",
|
||||
priority = 10,
|
||||
},
|
||||
float = {
|
||||
enabled = false,
|
||||
},
|
||||
virtual_text = {
|
||||
enabled = false,
|
||||
},
|
||||
status_text = {
|
||||
enabled = false,
|
||||
},
|
||||
autocmd = {
|
||||
enabled = true,
|
||||
updatetime = 100,
|
||||
events = { "CursorHold", "CursorHoldI", "InsertLeave" },
|
||||
pattern = { "*" },
|
||||
},
|
||||
ignore = {
|
||||
clients = {},
|
||||
ft = {},
|
||||
actions = {},
|
||||
},
|
||||
}
|
||||
74
home/editors/nvim/nvchad/lua/configs/lspconfig.lua
Normal file
74
home/editors/nvim/nvchad/lua/configs/lspconfig.lua
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
local configs = require("nvchad.configs.lspconfig")
|
||||
configs.defaults()
|
||||
|
||||
-- Noice has lsp.signature.enabled = false, so signature_help uses the default
|
||||
-- vim.lsp handler. focusable = false is REQUIRED: without it the cursor jumps
|
||||
-- into the floating signature window on trigger. The :checkhealth noice warning
|
||||
-- "signature_help is not configured to be handled by Noice" is expected here.
|
||||
-- vim.lsp.with() was removed in nvim 0.12; wrap the handler manually (same effect).
|
||||
vim.lsp.handlers["textDocument/signatureHelp"] = function(err, result, ctx, config)
|
||||
return vim.lsp.handlers.signature_help(err, result, ctx,
|
||||
vim.tbl_deep_extend("force", { focusable = false, silent = true }, config or {}))
|
||||
end
|
||||
|
||||
local servers = {
|
||||
html = {},
|
||||
cssls = {},
|
||||
gopls = {
|
||||
analyses = {
|
||||
unusedparams = true,
|
||||
},
|
||||
staticcheck = true,
|
||||
gofumpt = true,
|
||||
},
|
||||
ruff = {
|
||||
lint = {
|
||||
run = "onSave",
|
||||
},
|
||||
},
|
||||
zls = {},
|
||||
terraformls = {},
|
||||
templ = {},
|
||||
gh_actions_ls = {},
|
||||
|
||||
pyright = {
|
||||
settings = {
|
||||
python = {
|
||||
analysis = {
|
||||
autoSearchPaths = true,
|
||||
typeCheckingMode = "basic",
|
||||
},
|
||||
pythonPath = vim.fn.exepath("python3"),
|
||||
},
|
||||
},
|
||||
},
|
||||
ts_ls = {
|
||||
settings = {
|
||||
completions = {
|
||||
completeFunctionCalls = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
local on_attach = function(client, bufnr)
|
||||
configs.on_attach(client, bufnr)
|
||||
client.server_capabilities.documentFormattingProvider = true
|
||||
-- Use Trouble for references instead of Telescope (persistent split for browsing)
|
||||
vim.keymap.set("n", "gr", "<cmd>Trouble lsp_references toggle<cr>",
|
||||
{ buffer = bufnr, desc = "References (Trouble)" })
|
||||
end
|
||||
|
||||
for name, opts in pairs(servers) do
|
||||
local ok = pcall(function()
|
||||
opts.on_init = configs.on_init
|
||||
opts.on_attach = on_attach
|
||||
opts.capabilities = configs.capabilities
|
||||
vim.lsp.config(name, opts)
|
||||
vim.lsp.enable({name})
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
vim.notify("Failed to set up LSP: " .. name, vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
27
home/editors/nvim/nvchad/lua/configs/neotest.lua
Normal file
27
home/editors/nvim/nvchad/lua/configs/neotest.lua
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
return function()
|
||||
local neotest_ns = vim.api.nvim_create_namespace("neotest")
|
||||
vim.diagnostic.config({
|
||||
virtual_text = {
|
||||
format = function(diagnostic)
|
||||
local message = diagnostic.message:gsub("\n", " "):gsub("\t", " "):gsub("%s+", " "):gsub("^%s+", "")
|
||||
return message
|
||||
end,
|
||||
},
|
||||
}, neotest_ns)
|
||||
|
||||
require("neotest").setup({
|
||||
adapters = {
|
||||
require("neotest-go"),
|
||||
require("neotest-python")({
|
||||
dap = { justMyCode = false },
|
||||
args = { "-s", "-v", "--tb=short" },
|
||||
}),
|
||||
require("neotest-vitest"),
|
||||
},
|
||||
-- summary opens with neotest's default vsplit; edgy adopts it into the
|
||||
-- right rail by ft (the old "botright split" override opened it bottom
|
||||
-- full-width first, then edgy yanked it right — needless flicker).
|
||||
-- Keymaps live in the plugin spec (lua/plugins/init.lua) so they can
|
||||
-- lazy-load the plugin.
|
||||
})
|
||||
end
|
||||
19
home/editors/nvim/nvchad/lua/configs/octo.lua
Normal file
19
home/editors/nvim/nvchad/lua/configs/octo.lua
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- octo.nvim — in-editor GitHub PR lifecycle (browse, comment, review, approve,
|
||||
-- merge). Requires `gh` on PATH (added in default.nix) and `gh auth login` once.
|
||||
--
|
||||
-- Entry points live on <leader>r in the plugin spec. Inside octo buffers the
|
||||
-- builtin <localleader>-maps (localleader = "," set in init.lua) cover the rest:
|
||||
--
|
||||
-- PR buffer: ,po checkout ,pm merge ,psm squash-merge
|
||||
-- ,prm rebase-merge ,ca add comment <C-b> browser
|
||||
-- Review diff: ]q/[q next/prev file ]t/[t next/prev thread
|
||||
-- ,ca comment ,sa suggestion ,rt resolve thread
|
||||
-- ,vs submit review (then <C-a> approve / <C-m> comment /
|
||||
-- <C-r> request changes in the submit window)
|
||||
--
|
||||
-- `enable_builtin` gives the bare :Octo command a picker over all subcommands —
|
||||
-- the discoverable fallback for anything unbound.
|
||||
return {
|
||||
picker = "telescope", -- reuse the existing telescope + fzf-native install
|
||||
enable_builtin = true,
|
||||
}
|
||||
220
home/editors/nvim/nvchad/lua/configs/telescope.lua
Normal file
220
home/editors/nvim/nvchad/lua/configs/telescope.lua
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
local ts = require("telescope")
|
||||
local actions = require("telescope.actions")
|
||||
local layout_actions = require("telescope.actions.layout")
|
||||
local undo_actions = require("telescope-undo.actions")
|
||||
|
||||
local h_pct = 0.90
|
||||
local w_pct = 0.80
|
||||
local w_limit = 100
|
||||
|
||||
-- Compact: vertical, preview hidden — for quick picks (files, buffers, branches)
|
||||
local compact = {
|
||||
preview = { hide_on_startup = true },
|
||||
layout_strategy = "vertical",
|
||||
layout_config = {
|
||||
vertical = {
|
||||
mirror = true,
|
||||
prompt_position = "top",
|
||||
width = function(_, cols, _)
|
||||
return math.min(math.floor(w_pct * cols), w_limit)
|
||||
end,
|
||||
height = function(_, _, rows)
|
||||
return math.floor(rows * h_pct)
|
||||
end,
|
||||
preview_cutoff = 10,
|
||||
preview_height = 0.4,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- Fullscreen: flex (adapts to terminal width), preview always visible — for grep, LSP, undo
|
||||
local fullscreen = {
|
||||
preview = { hide_on_startup = false },
|
||||
layout_strategy = "flex",
|
||||
layout_config = {
|
||||
flex = { flip_columns = 120 },
|
||||
horizontal = {
|
||||
mirror = false,
|
||||
prompt_position = "top",
|
||||
width = function(_, cols, _)
|
||||
return math.floor(cols * w_pct)
|
||||
end,
|
||||
height = function(_, _, rows)
|
||||
return math.floor(rows * h_pct)
|
||||
end,
|
||||
preview_cutoff = 10,
|
||||
preview_width = 0.5,
|
||||
},
|
||||
vertical = {
|
||||
mirror = true,
|
||||
prompt_position = "top",
|
||||
width = function(_, cols, _)
|
||||
return math.floor(cols * w_pct)
|
||||
end,
|
||||
height = function(_, _, rows)
|
||||
return math.floor(rows * h_pct)
|
||||
end,
|
||||
preview_cutoff = 10,
|
||||
preview_height = 0.5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ts.setup({
|
||||
defaults = vim.tbl_extend("error", compact, {
|
||||
sorting_strategy = "ascending",
|
||||
path_display = { "filename_first" },
|
||||
file_ignore_patterns = { "node_modules", ".venv/", ".git/", "%.lock", "CLAUDE.md", "%AGENTS.md" },
|
||||
mappings = {
|
||||
i = {
|
||||
["<C-S>"] = actions.to_fuzzy_refine,
|
||||
["<C-o>"] = layout_actions.toggle_preview,
|
||||
},
|
||||
n = {
|
||||
["o"] = layout_actions.toggle_preview,
|
||||
["<C-c>"] = actions.close,
|
||||
},
|
||||
},
|
||||
}),
|
||||
pickers = {
|
||||
find_files = vim.tbl_extend("force", compact, {
|
||||
find_command = { "fd", "--type", "f", "-H", "--strip-cwd-prefix" },
|
||||
}),
|
||||
live_grep = fullscreen,
|
||||
grep_string = fullscreen,
|
||||
lsp_dynamic_workspace_symbols = fullscreen,
|
||||
help_tags = fullscreen,
|
||||
buffers = {
|
||||
sort_lastused = true,
|
||||
mappings = {
|
||||
n = { ["dd"] = actions.delete_buffer },
|
||||
},
|
||||
},
|
||||
git_branches = compact,
|
||||
-- Big preview for reviewing changed files, but start hidden ('o' toggles it)
|
||||
git_status = vim.tbl_extend("force", fullscreen, {
|
||||
preview = { hide_on_startup = true },
|
||||
}),
|
||||
},
|
||||
extensions = {
|
||||
undo = vim.tbl_extend("force", fullscreen, {
|
||||
vim_diff_opts = { ctxlen = 4 },
|
||||
mappings = {
|
||||
i = {
|
||||
["<cr>"] = undo_actions.restore,
|
||||
["<C-y>d"] = undo_actions.yank_deletions,
|
||||
["<C-y>a"] = undo_actions.yank_additions,
|
||||
},
|
||||
n = {
|
||||
["<cr>"] = undo_actions.restore,
|
||||
["ya"] = undo_actions.yank_additions,
|
||||
["yd"] = undo_actions.yank_deletions,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
ts.load_extension("fzf")
|
||||
ts.load_extension("undo")
|
||||
|
||||
-- ── Cross-file hunk picker ────────────────────────────────────────────────
|
||||
-- Flat, fuzzy-searchable list of every hunk dirty vs a base (default HEAD),
|
||||
-- with a diff previewer. Built by parsing `git diff` directly rather than
|
||||
-- gitsigns.get_hunks(), so it never races the async buffer attach and works
|
||||
-- on files that aren't open yet.
|
||||
local function parse_git_hunks(base)
|
||||
base = base or "HEAD"
|
||||
-- List form → no shell, so fish quoting can't interfere.
|
||||
local lines = vim.fn.systemlist({ "git", "--no-pager", "diff", "--no-color", "-U3", base })
|
||||
if vim.v.shell_error ~= 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
local results, cur_file, cur = {}, nil, nil
|
||||
local function flush()
|
||||
if cur then
|
||||
table.insert(results, cur)
|
||||
cur = nil
|
||||
end
|
||||
end
|
||||
|
||||
for _, line in ipairs(lines) do
|
||||
if line:sub(1, 11) == "diff --git " then
|
||||
flush() -- new file boundary: close out the previous file's last hunk
|
||||
elseif line:sub(1, 6) == "+++ b/" then
|
||||
local p = line:sub(7)
|
||||
if p ~= "/dev/null" then
|
||||
cur_file = p
|
||||
end
|
||||
elseif line:sub(1, 2) == "@@" then
|
||||
flush()
|
||||
local nstart = tonumber(line:match("^@@ %-%d+,?%d* %+(%d+)"))
|
||||
cur = {
|
||||
file = cur_file,
|
||||
lnum = nstart or 1,
|
||||
section = line:match("@@.-@@%s?(.*)") or "",
|
||||
lines = { line },
|
||||
adds = 0,
|
||||
dels = 0,
|
||||
}
|
||||
elseif cur then
|
||||
local c = line:sub(1, 1)
|
||||
if c == "+" then
|
||||
cur.adds = cur.adds + 1
|
||||
elseif c == "-" then
|
||||
cur.dels = cur.dels + 1
|
||||
end
|
||||
if c ~= "\\" then -- skip "\ No newline at end of file"
|
||||
table.insert(cur.lines, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
flush()
|
||||
return results
|
||||
end
|
||||
|
||||
local function hunks_picker(opts)
|
||||
opts = opts or {}
|
||||
local pickers = require("telescope.pickers")
|
||||
local finders = require("telescope.finders")
|
||||
local previewers = require("telescope.previewers")
|
||||
local conf = require("telescope.config").values
|
||||
|
||||
local hunks = parse_git_hunks(opts.base)
|
||||
if vim.tbl_isempty(hunks) then
|
||||
vim.notify("No dirty hunks vs " .. (opts.base or "HEAD"), vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
pickers
|
||||
.new(fullscreen, {
|
||||
prompt_title = "Git Hunks (" .. (opts.base or "HEAD") .. ")",
|
||||
finder = finders.new_table({
|
||||
results = hunks,
|
||||
entry_maker = function(e)
|
||||
return {
|
||||
value = e,
|
||||
-- filename + lnum make the default select action jump straight to the hunk
|
||||
filename = e.file,
|
||||
lnum = e.lnum,
|
||||
ordinal = e.file .. " " .. e.section,
|
||||
display = string.format("%s:%d +%d -%d %s", e.file, e.lnum, e.adds, e.dels, e.section),
|
||||
}
|
||||
end,
|
||||
}),
|
||||
sorter = conf.generic_sorter({}),
|
||||
previewer = previewers.new_buffer_previewer({
|
||||
title = "Hunk Diff",
|
||||
define_preview = function(self, entry)
|
||||
vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, entry.value.lines)
|
||||
vim.bo[self.state.bufnr].filetype = "diff"
|
||||
end,
|
||||
}),
|
||||
})
|
||||
:find()
|
||||
end
|
||||
|
||||
return {
|
||||
hunks_picker = hunks_picker,
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
local select = require("nvim-treesitter-textobjects.select").select_textobject
|
||||
local move = require("nvim-treesitter-textobjects.move")
|
||||
local swap = require("nvim-treesitter-textobjects.swap")
|
||||
|
||||
require("nvim-treesitter-textobjects").setup({
|
||||
select = {
|
||||
lookahead = true,
|
||||
selection_modes = {
|
||||
["@parameter.outer"] = "v",
|
||||
["@function.outer"] = "V",
|
||||
["@class.outer"] = "V",
|
||||
},
|
||||
include_surrounding_whitespace = false,
|
||||
},
|
||||
})
|
||||
|
||||
-- Select keymaps (visual + operator-pending)
|
||||
local so = { "x", "o" }
|
||||
vim.keymap.set(so, "af", function() select("@function.outer", "textobjects") end, { desc = "Around function" })
|
||||
vim.keymap.set(so, "if", function() select("@function.inner", "textobjects") end, { desc = "Inside function" })
|
||||
vim.keymap.set(so, "ac", function() select("@class.outer", "textobjects") end, { desc = "Around class" })
|
||||
vim.keymap.set(so, "ic", function() select("@class.inner", "textobjects") end, { desc = "Inside class" })
|
||||
vim.keymap.set(so, "aa", function() select("@parameter.outer", "textobjects") end, { desc = "Around parameter" })
|
||||
vim.keymap.set(so, "ia", function() select("@parameter.inner", "textobjects") end, { desc = "Inside parameter" })
|
||||
|
||||
-- Move keymaps (normal + visual + operator-pending)
|
||||
local nso = { "n", "x", "o" }
|
||||
vim.keymap.set(nso, "]f", function() move.goto_next_start("@function.outer", "textobjects") end, { desc = "Next function" })
|
||||
vim.keymap.set(nso, "[f", function() move.goto_previous_start("@function.outer", "textobjects") end, { desc = "Prev function" })
|
||||
vim.keymap.set(nso, "]c", function() move.goto_next_start("@class.outer", "textobjects") end, { desc = "Next class" })
|
||||
vim.keymap.set(nso, "[c", function() move.goto_previous_start("@class.outer", "textobjects") end, { desc = "Prev class" })
|
||||
vim.keymap.set(nso, "]a", function() move.goto_next_start("@parameter.inner", "textobjects") end, { desc = "Next parameter" })
|
||||
vim.keymap.set(nso, "[a", function() move.goto_previous_start("@parameter.inner", "textobjects") end, { desc = "Prev parameter" })
|
||||
|
||||
-- Swap keymaps (normal only)
|
||||
vim.keymap.set("n", "<leader>sa", function() swap.swap_next("@parameter.inner") end, { desc = "Swap parameter forward" })
|
||||
vim.keymap.set("n", "<leader>sA", function() swap.swap_previous("@parameter.inner") end, { desc = "Swap parameter backward" })
|
||||
48
home/editors/nvim/nvchad/lua/mappings.lua
Normal file
48
home/editors/nvim/nvchad/lua/mappings.lua
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
require("nvchad.mappings")
|
||||
|
||||
local map = vim.keymap.set
|
||||
|
||||
map("n", ";", ":", { desc = "CMD enter command mode" })
|
||||
map("i", "jk", "<ESC>")
|
||||
|
||||
-- Telescope pickers
|
||||
local builtin = require("telescope.builtin")
|
||||
map("n", "<C-P>", builtin.find_files, { desc = "Find files" })
|
||||
map("n", "<C-f>", builtin.live_grep, { desc = "Live grep" })
|
||||
map("n", "<leader>fs", builtin.grep_string, { desc = "Grep string under cursor" })
|
||||
map("n", "<leader>fS", builtin.lsp_dynamic_workspace_symbols, { desc = "Workspace symbols" })
|
||||
map("n", "<leader>fb", builtin.buffers, { desc = "Buffers" })
|
||||
map("n", "<leader>fh", builtin.help_tags, { desc = "Help tags" })
|
||||
map("n", "<leader>co", builtin.git_branches, { desc = "Git branches" })
|
||||
map("n", "<leader>fu", "<cmd>Telescope undo<cr>", { desc = "Undo tree" })
|
||||
map("n", "<leader>fr", builtin.resume, { desc = "Resume last picker" })
|
||||
map("n", "<leader>fo", builtin.oldfiles, { desc = "Recent files" })
|
||||
|
||||
-- Code review: fuzzy-jump between dirty hunks across files (custom picker).
|
||||
-- Rebound into the <leader>r "review" group alongside diffview/octo. Unique vs
|
||||
-- diffview: fuzzy-typeahead over hunk section names, which diffview can't do.
|
||||
local hunks_picker = require("configs.telescope").hunks_picker
|
||||
map("n", "<leader>rh", hunks_picker, { desc = "Fuzzy hunks vs HEAD" })
|
||||
map("n", "<leader>rH", function()
|
||||
vim.ui.input({ prompt = "Review against base: ", default = "origin/HEAD" }, function(base)
|
||||
if base and base ~= "" then
|
||||
hunks_picker({ base = base })
|
||||
end
|
||||
end)
|
||||
end, { desc = "Fuzzy hunks vs base…" })
|
||||
map("n", "<leader>gf", builtin.git_status, { desc = "Changed files (git status)" })
|
||||
|
||||
-- treesitter-textobjects remaps ]c/[c to next/prev class globally, which shadows
|
||||
-- Vim's native ]c/[c "next/prev diff change" motion. In diff windows (diffview
|
||||
-- review, :Gdiffsplit) restore native change-stepping buffer-locally so PR review
|
||||
-- can walk changes the GitHub way. bang=true bypasses the global class-nav remap.
|
||||
vim.api.nvim_create_autocmd("OptionSet", {
|
||||
pattern = "diff",
|
||||
callback = function()
|
||||
if not vim.wo.diff then
|
||||
return
|
||||
end
|
||||
map("n", "]c", function() vim.cmd.normal({ "]c", bang = true }) end, { buffer = 0, desc = "Next diff change" })
|
||||
map("n", "[c", function() vim.cmd.normal({ "[c", bang = true }) end, { buffer = 0, desc = "Prev diff change" })
|
||||
end,
|
||||
})
|
||||
15
home/editors/nvim/nvchad/lua/options.lua
Normal file
15
home/editors/nvim/nvchad/lua/options.lua
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
require("nvchad.options")
|
||||
|
||||
-- add yours here!
|
||||
|
||||
local o = vim.o
|
||||
o.cursorlineopt = "both" -- to enable cursorline!
|
||||
|
||||
-- Hugo template filetype detection
|
||||
-- Detect .html files in Hugo layouts/templates directories as htmlhugo (vim-hugo filetype)
|
||||
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
|
||||
pattern = { "*/layouts/*.html", "*/layouts/**/*.html" },
|
||||
callback = function()
|
||||
vim.bo.filetype = "htmlhugo"
|
||||
end,
|
||||
})
|
||||
482
home/editors/nvim/nvchad/lua/plugins/init.lua
Normal file
482
home/editors/nvim/nvchad/lua/plugins/init.lua
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
local function review_base()
|
||||
local head = vim.fn.systemlist({ "git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD" })
|
||||
if vim.v.shell_error == 0 and head[1] and head[1] ~= "" then
|
||||
return head[1]
|
||||
end
|
||||
for _, b in ipairs({ "origin/main", "origin/master", "main", "master" }) do
|
||||
vim.fn.system({ "git", "rev-parse", "--verify", "--quiet", b })
|
||||
if vim.v.shell_error == 0 then
|
||||
return b
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function review_pr_diff()
|
||||
local base = review_base()
|
||||
if not base then
|
||||
vim.notify("diffview: no base branch found (origin/HEAD unset; no main/master)", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.cmd("DiffviewOpen " .. base .. "...HEAD")
|
||||
end
|
||||
|
||||
return {
|
||||
{ "hrsh7th/nvim-cmp", enabled = false },
|
||||
{
|
||||
"stevearc/conform.nvim",
|
||||
event = "BufWritePre", -- uncomment for format on save
|
||||
opts = require("configs.conform"),
|
||||
},
|
||||
{
|
||||
"nvim-telescope/telescope.nvim",
|
||||
dependencies = {
|
||||
{ "nvim-telescope/telescope-fzf-native.nvim", build = "make" },
|
||||
"debugloop/telescope-undo.nvim",
|
||||
},
|
||||
config = function()
|
||||
dofile(vim.g.base46_cache .. "telescope")
|
||||
require("configs.telescope")
|
||||
end,
|
||||
},
|
||||
|
||||
-- These are some examples, uncomment them if you want to see them work!
|
||||
{
|
||||
"neovim/nvim-lspconfig",
|
||||
config = function()
|
||||
require("configs.lspconfig")
|
||||
end,
|
||||
},
|
||||
|
||||
{
|
||||
"nvim-treesitter/nvim-treesitter",
|
||||
branch = "main",
|
||||
opts = {
|
||||
ensure_installed = {
|
||||
"vim",
|
||||
"lua",
|
||||
"vimdoc",
|
||||
"html",
|
||||
"css",
|
||||
"python",
|
||||
"typescript",
|
||||
"go",
|
||||
"zig",
|
||||
"nix",
|
||||
"terraform",
|
||||
"markdown",
|
||||
"markdown_inline",
|
||||
"regex",
|
||||
"bash",
|
||||
"json",
|
||||
"yaml",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nvim-treesitter/nvim-treesitter-textobjects",
|
||||
branch = "main",
|
||||
lazy = false,
|
||||
dependencies = { "nvim-treesitter/nvim-treesitter" },
|
||||
config = function()
|
||||
require("configs.treesitter-textobjects")
|
||||
end,
|
||||
},
|
||||
{
|
||||
"tpope/vim-fugitive",
|
||||
cmd = {
|
||||
"Git",
|
||||
"G",
|
||||
"Gdiffsplit",
|
||||
"Gvdiffsplit",
|
||||
"Gwrite",
|
||||
"Gread",
|
||||
"Ggrep",
|
||||
"GMove",
|
||||
"GDelete",
|
||||
"GBrowse",
|
||||
"Gstatus",
|
||||
"Gedit",
|
||||
"Glog",
|
||||
},
|
||||
keys = {
|
||||
{ "<leader>gs", "<cmd>vertical Git<cr>", desc = "Open Fugitive" },
|
||||
},
|
||||
dependencies = {
|
||||
"tpope/vim-rhubarb",
|
||||
},
|
||||
},
|
||||
{
|
||||
"nicolasgb/jj.nvim",
|
||||
version = "*",
|
||||
cmd = { "J" },
|
||||
keys = {
|
||||
{ "<leader>jj", "<cmd>J status<cr>", desc = "Jujutsu status" },
|
||||
{ "<leader>jl", "<cmd>J log<cr>", desc = "Jujutsu log" },
|
||||
{ "<leader>jd", "<cmd>J describe<cr>", desc = "Jujutsu describe" },
|
||||
{ "<leader>jn", "<cmd>J new<cr>", desc = "Jujutsu new change" },
|
||||
{ "<leader>je", "<cmd>J edit<cr>", desc = "Jujutsu edit" },
|
||||
{ "<leader>jp", "<cmd>J git push<cr>", desc = "Jujutsu push" },
|
||||
},
|
||||
config = function()
|
||||
require("jj").setup({})
|
||||
end,
|
||||
},
|
||||
{
|
||||
"folke/zen-mode.nvim",
|
||||
cmd = "ZenMode",
|
||||
keys = {
|
||||
{ "<leader>z", "<cmd>ZenMode<cr>", desc = "Toggle ZenMode" },
|
||||
},
|
||||
opts = {
|
||||
plugins = {
|
||||
twilight = { enabled = false },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"folke/twilight.nvim",
|
||||
keys = {
|
||||
{ "<leader>tz", "<cmd>Twilight<cr>", desc = "Toggle Twilight" },
|
||||
},
|
||||
opts = {},
|
||||
},
|
||||
{
|
||||
"ThePrimeagen/harpoon",
|
||||
branch = "harpoon2",
|
||||
dependencies = { "nvim-lua/plenary.nvim" },
|
||||
config = function()
|
||||
require("harpoon"):setup()
|
||||
end,
|
||||
keys = {
|
||||
{
|
||||
"<leader>ha",
|
||||
function()
|
||||
require("harpoon"):list():add()
|
||||
end,
|
||||
desc = "Harpoon add",
|
||||
},
|
||||
{
|
||||
"<leader>hm",
|
||||
function()
|
||||
require("harpoon").ui:toggle_quick_menu(require("harpoon"):list())
|
||||
end,
|
||||
desc = "Harpoon menu",
|
||||
},
|
||||
{
|
||||
"<leader>hH",
|
||||
function()
|
||||
require("harpoon"):list():clear()
|
||||
end,
|
||||
desc = "Harpoon clear all",
|
||||
},
|
||||
{
|
||||
"<leader>1",
|
||||
function()
|
||||
require("harpoon"):list():select(1)
|
||||
end,
|
||||
desc = "Harpoon 1",
|
||||
},
|
||||
{
|
||||
"<leader>2",
|
||||
function()
|
||||
require("harpoon"):list():select(2)
|
||||
end,
|
||||
desc = "Harpoon 2",
|
||||
},
|
||||
{
|
||||
"<leader>3",
|
||||
function()
|
||||
require("harpoon"):list():select(3)
|
||||
end,
|
||||
desc = "Harpoon 3",
|
||||
},
|
||||
{
|
||||
"<leader>4",
|
||||
function()
|
||||
require("harpoon"):list():select(4)
|
||||
end,
|
||||
desc = "Harpoon 4",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"folke/trouble.nvim",
|
||||
opts = {
|
||||
auto_close = false,
|
||||
auto_refresh = true,
|
||||
auto_preview = false,
|
||||
warn_no_results = false,
|
||||
-- win.position here is the single source of truth for placement;
|
||||
-- edgy adopts each window at that edge (see configs/edgy.lua).
|
||||
-- Bottom (trouble's default) = transient lists: diagnostics/qf/loclist.
|
||||
-- Right = code-context panels you read alongside the buffer.
|
||||
modes = {
|
||||
diagnostics = {
|
||||
auto_close = true, -- bottom panel closes when no diagnostics
|
||||
},
|
||||
lsp = { win = { position = "right" } },
|
||||
-- focus=true: gr means "show me the list, let me pick one"
|
||||
lsp_references = { win = { position = "right" }, focus = true },
|
||||
},
|
||||
},
|
||||
cmd = "Trouble",
|
||||
keys = {
|
||||
{ "<leader>xx", "<cmd>Trouble diagnostics toggle<cr>", desc = "Workspace diagnostics" },
|
||||
{ "<leader>xX", "<cmd>Trouble diagnostics toggle filter.buf=0<cr>", desc = "Buffer diagnostics" },
|
||||
-- builtin symbols mode = lsp_document_symbols + right rail + the
|
||||
-- curated kind filter (the old copy of that filter on `lsp` mode
|
||||
-- dropped refs/defs items, which carry no symbol kind)
|
||||
{ "<leader>cs", "<cmd>Trouble symbols toggle<cr>", desc = "Symbols outline" },
|
||||
{ "<leader>cl", "<cmd>Trouble lsp toggle<cr>", desc = "LSP defs/refs/impls" },
|
||||
{ "<leader>xq", "<cmd>Trouble qflist toggle<cr>", desc = "Quickfix" },
|
||||
{ "<leader>xl", "<cmd>Trouble loclist toggle<cr>", desc = "Location list" },
|
||||
},
|
||||
},
|
||||
{
|
||||
-- Label the <leader> umbrellas so the which-key popup reads as a menu.
|
||||
-- NvChad's whichkey opts is a function, so a plain table override can be
|
||||
-- dropped during lazy's merge — extend with a function fragment instead
|
||||
-- (lazy feeds it the accumulated opts and keeps the mutation).
|
||||
"folke/which-key.nvim",
|
||||
opts = function(_, opts)
|
||||
opts.spec = opts.spec or {}
|
||||
vim.list_extend(opts.spec, {
|
||||
{ "<leader>c", group = "code" },
|
||||
{ "<leader>f", group = "find" },
|
||||
{ "<leader>g", group = "git" },
|
||||
{ "<leader>h", group = "hunks · harpoon" },
|
||||
{ "<leader>j", group = "jujutsu" },
|
||||
{ "<leader>r", group = "review" },
|
||||
{ "<leader>s", group = "swap" },
|
||||
{ "<leader>t", group = "test · toggle" },
|
||||
{ "<leader>x", group = "diagnostics" },
|
||||
})
|
||||
return opts
|
||||
end,
|
||||
},
|
||||
{
|
||||
"folke/noice.nvim",
|
||||
event = "VeryLazy",
|
||||
opts = {
|
||||
lsp = {
|
||||
signature = {
|
||||
enabled = false,
|
||||
},
|
||||
-- override markdown rendering so LSP docs render through Treesitter
|
||||
-- (cmp override dropped: nvim-cmp is disabled, blink renders its own docs)
|
||||
override = {
|
||||
["vim.lsp.util.convert_input_to_markdown_lines"] = true,
|
||||
["vim.lsp.util.stylize_markdown"] = true,
|
||||
},
|
||||
},
|
||||
-- you can enable a preset for easier configuration
|
||||
presets = {
|
||||
bottom_search = true, -- use a classic bottom cmdline for search
|
||||
command_palette = true, -- position the cmdline and popupmenu together
|
||||
long_message_to_split = true, -- long messages will be sent to a split
|
||||
inc_rename = false, -- enables an input dialog for inc-rename.nvim
|
||||
lsp_doc_border = true, -- add a border to hover docs and signature help
|
||||
},
|
||||
},
|
||||
dependencies = {
|
||||
"MunifTanjim/nui.nvim",
|
||||
{ "rcarriga/nvim-notify", opts = { background_colour = "#000000" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
"m4xshen/hardtime.nvim",
|
||||
lazy = false,
|
||||
dependencies = { "MunifTanjim/nui.nvim" },
|
||||
opts = {},
|
||||
},
|
||||
|
||||
{
|
||||
"MeanderingProgrammer/render-markdown.nvim",
|
||||
opts = {
|
||||
file_types = { "markdown" },
|
||||
},
|
||||
ft = { "markdown" },
|
||||
},
|
||||
|
||||
{
|
||||
-- inline images in markdown buffers (e.g. newsboat ,f full-text view).
|
||||
-- Ghostty implements the kitty graphics protocol, so use that backend.
|
||||
"3rd/image.nvim",
|
||||
ft = { "markdown" },
|
||||
opts = {
|
||||
backend = "kitty",
|
||||
processor = "magick_rock",
|
||||
integrations = {
|
||||
markdown = {
|
||||
enabled = true,
|
||||
-- render only the image on the cursor's line — one at a
|
||||
-- time, so a stack of images never overlaps the text
|
||||
only_render_image_at_cursor = true,
|
||||
download_remote_images = true,
|
||||
},
|
||||
},
|
||||
-- bound height so image.nvim reserves the same number of rows it
|
||||
-- renders (mismatch is what makes images overlap the alt text)
|
||||
max_width_window_percentage = 80,
|
||||
max_height_window_percentage = 50,
|
||||
-- repaint when other windows overlap, so scroll/wrap don't leave
|
||||
-- the image stamped over text
|
||||
window_overlap_clear_enabled = true,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
"folke/edgy.nvim",
|
||||
event = "VeryLazy",
|
||||
enabled = true,
|
||||
init = function()
|
||||
vim.opt.laststatus = 3
|
||||
vim.opt.splitkeep = "screen"
|
||||
end,
|
||||
opts = require("configs.edgy"),
|
||||
},
|
||||
{
|
||||
"saghen/blink.cmp",
|
||||
dependencies = { "rafamadriz/friendly-snippets" },
|
||||
event = "VeryLazy",
|
||||
version = "*",
|
||||
opts = require("configs.blink"),
|
||||
},
|
||||
{
|
||||
"phelipetls/vim-hugo",
|
||||
-- Load early to enable filetype detection for Hugo templates
|
||||
lazy = false,
|
||||
},
|
||||
{
|
||||
"lewis6991/gitsigns.nvim",
|
||||
-- NvChad includes gitsigns by default; override its opts
|
||||
opts = require("configs.gitsigns"),
|
||||
},
|
||||
{
|
||||
"nvim-neotest/neotest",
|
||||
dependencies = {
|
||||
"nvim-neotest/nvim-nio",
|
||||
"nvim-lua/plenary.nvim",
|
||||
"antoinemadec/FixCursorHold.nvim",
|
||||
"nvim-treesitter/nvim-treesitter",
|
||||
"nvim-neotest/neotest-go",
|
||||
"nvim-neotest/neotest-python",
|
||||
"marilari88/neotest-vitest",
|
||||
},
|
||||
-- keys-gated (was lazy=false): nothing needs neotest before the first
|
||||
-- test action, and it pulls in nio + 3 adapters at startup otherwise
|
||||
keys = {
|
||||
-- stylua: ignore start
|
||||
{ "<leader>tr", function() require("neotest").run.run() end, desc = "Run nearest test" },
|
||||
{ "<leader>tR", function() require("neotest").run.run(vim.fn.expand("%")) end, desc = "Run file tests" },
|
||||
{ "<leader>tl", function() require("neotest").run.run_last() end, desc = "Run last test" },
|
||||
{ "<leader>ts", function() require("neotest").run.stop() end, desc = "Stop test run" },
|
||||
{ "<leader>tt", function() require("neotest").summary.toggle() end, desc = "Toggle test summary" },
|
||||
{ "<leader>to", function() require("neotest").output.open({ enter = true, short = true }) end, desc = "Show test output" },
|
||||
{ "<leader>tO", function() require("neotest").output_panel.toggle() end, desc = "Toggle output panel" },
|
||||
{ "<leader>tW", function() require("neotest").watch.toggle(vim.fn.expand("%")) end, desc = "Watch file tests" },
|
||||
{ "]t", function() require("neotest").jump.next({ status = "failed" }) end, desc = "Next failed test" },
|
||||
{ "[t", function() require("neotest").jump.prev({ status = "failed" }) end, desc = "Prev failed test" },
|
||||
-- stylua: ignore end
|
||||
},
|
||||
config = function()
|
||||
require("configs.neotest")()
|
||||
end,
|
||||
},
|
||||
{
|
||||
"kosayoda/nvim-lightbulb",
|
||||
event = "LspAttach",
|
||||
opts = require("configs.lightbulb"),
|
||||
},
|
||||
|
||||
-- ── PR review surface ─────────────────────────────────────────────────────
|
||||
{
|
||||
"sindrets/diffview.nvim",
|
||||
cmd = {
|
||||
"DiffviewOpen",
|
||||
"DiffviewFileHistory",
|
||||
"DiffviewClose",
|
||||
"DiffviewToggleFiles",
|
||||
"DiffviewFocusFiles",
|
||||
"DiffviewRefresh",
|
||||
},
|
||||
keys = {
|
||||
-- Triple-dot = merge-base diff = exact GitHub "Files changed" semantics.
|
||||
-- Base resolved locally (review_base) so it works without origin/HEAD set.
|
||||
{ "<leader>rd", review_pr_diff, desc = "Review: PR diff vs base" },
|
||||
{ "<leader>rw", "<cmd>DiffviewOpen<cr>", desc = "Review: working-tree diff" },
|
||||
{
|
||||
"<leader>rB",
|
||||
function()
|
||||
vim.ui.input({ prompt = "Diff vs base: ", default = review_base() or "origin/main" }, function(b)
|
||||
if b and b ~= "" then
|
||||
vim.cmd("DiffviewOpen " .. b .. "...HEAD")
|
||||
end
|
||||
end)
|
||||
end,
|
||||
desc = "Review: diff vs base…",
|
||||
},
|
||||
{ "<leader>rf", "<cmd>DiffviewFileHistory %<cr>", desc = "Review: file history" },
|
||||
{ "<leader>rl", "<cmd>DiffviewFileHistory<cr>", desc = "Review: branch history" },
|
||||
{ "<leader>rq", "<cmd>DiffviewClose<cr>", desc = "Review: close diffview" },
|
||||
},
|
||||
opts = require("configs.diffview"),
|
||||
-- <leader>r group label lives in the which-key spec above, with the rest.
|
||||
},
|
||||
{
|
||||
"pwntester/octo.nvim",
|
||||
cmd = "Octo",
|
||||
dependencies = {
|
||||
"nvim-lua/plenary.nvim",
|
||||
"nvim-telescope/telescope.nvim",
|
||||
"nvim-tree/nvim-web-devicons",
|
||||
},
|
||||
keys = {
|
||||
-- Browse: pickers over PRs/issues; <C-o> checkout, <C-r> merge, <C-b>
|
||||
-- browser directly from the picker.
|
||||
{ "<leader>ro", "<cmd>Octo pr list<cr>", desc = "Review: list PRs" },
|
||||
{ "<leader>ri", "<cmd>Octo issue list<cr>", desc = "Review: list issues" },
|
||||
{ "<leader>rn", "<cmd>Octo pr create<cr>", desc = "Review: create PR" },
|
||||
-- Review flow on the checked-out / opened PR.
|
||||
{ "<leader>rc", "<cmd>Octo pr checkout<cr>", desc = "Review: checkout PR" },
|
||||
{ "<leader>rs", "<cmd>Octo review start<cr>", desc = "Review: start review" },
|
||||
{ "<leader>rr", "<cmd>Octo review resume<cr>", desc = "Review: resume review" },
|
||||
-- Submit opens the review window: <C-a> approve, <C-m> comment,
|
||||
-- <C-r> request changes.
|
||||
{ "<leader>rS", "<cmd>Octo review submit<cr>", desc = "Review: submit review" },
|
||||
-- Merge uses default_merge_method; ,pm/,psm/,prm in the PR buffer
|
||||
-- pick the method explicitly.
|
||||
{ "<leader>rm", "<cmd>Octo pr merge<cr>", desc = "Review: merge PR" },
|
||||
{ "<leader>rM", "<cmd>Octo pr merge squash<cr>", desc = "Review: squash-merge PR" },
|
||||
},
|
||||
opts = require("configs.octo"),
|
||||
},
|
||||
{
|
||||
"nvim-treesitter/nvim-treesitter-context",
|
||||
event = { "BufReadPost", "BufNewFile" },
|
||||
dependencies = { "nvim-treesitter/nvim-treesitter" },
|
||||
-- Safe on treesitter branch="main": uses core vim.treesitter + its own
|
||||
-- bundled queries, not the rewritten nvim-treesitter module API.
|
||||
opts = {
|
||||
max_lines = 4, -- cap sticky-header height on deep nesting
|
||||
multiline_threshold = 1, -- collapse long signatures to one line
|
||||
trim_scope = "outer", -- keep innermost scope when over max_lines
|
||||
mode = "cursor",
|
||||
multiwindow = true, -- show context in BOTH diffview panes
|
||||
},
|
||||
keys = {
|
||||
-- [x, not [c: this config remaps ]c/[c to class-nav (via
|
||||
-- treesitter-textobjects), so [x is the free key for scope-jump.
|
||||
-- Native ]c/[c diff-change motion is restored in diff windows in mappings.lua.
|
||||
{
|
||||
"[x",
|
||||
function()
|
||||
require("treesitter-context").go_to_context(vim.v.count1)
|
||||
end,
|
||||
desc = "Jump to context",
|
||||
},
|
||||
{ "<leader>tC", "<cmd>TSContext toggle<cr>", desc = "Toggle sticky context" },
|
||||
},
|
||||
},
|
||||
}
|
||||
20
home/profiles/default.nix
Normal file
20
home/profiles/default.nix
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"bogdan@framework13" = [
|
||||
../.
|
||||
./framework13
|
||||
];
|
||||
|
||||
"deck@steamdeck" = [
|
||||
./steamdeck
|
||||
];
|
||||
|
||||
"nixos@iso" = [
|
||||
./iso/base.nix
|
||||
./iso
|
||||
];
|
||||
|
||||
"bogdan@phantom" = [
|
||||
../.
|
||||
./phantom
|
||||
];
|
||||
}
|
||||
71
home/profiles/framework13/default.nix
Normal file
71
home/profiles/framework13/default.nix
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
{ config, inputs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# theme
|
||||
../../theme
|
||||
|
||||
# editors
|
||||
../../editors/nvim
|
||||
|
||||
# programs
|
||||
../../programs
|
||||
../../programs/keepassxc.nix
|
||||
../../programs/wayland
|
||||
# HM-generated Hyprland lua config (framework13-first; replaces the
|
||||
# hand-authored system-level lua.nix once verified). See Change 2 plan.
|
||||
../../programs/wayland/hyprland.nix
|
||||
../../programs/gtk.nix
|
||||
../../programs/yt-dlp.nix
|
||||
../../programs/gpg.nix
|
||||
|
||||
# terminal emulators
|
||||
../../terminal/emulators/ghostty.nix
|
||||
|
||||
../../services/wayland/hyprpaper.nix
|
||||
../../services/mako.nix
|
||||
../../services/meeting-guard.nix
|
||||
../../services/podman.nix
|
||||
../../services/gpg.nix
|
||||
../../services/syncthing.nix
|
||||
../../services/trayscale.nix
|
||||
../../services/media/playerctl.nix
|
||||
|
||||
# Private home manager configuration
|
||||
inputs.secrets.homeModules.default
|
||||
];
|
||||
|
||||
# SOPS secret for OpenRouter API key (used by nvim 99 plugin)
|
||||
sops.secrets.openrouter_api_key = {
|
||||
sopsFile = "${inputs.secrets}/secrets/common/api-keys.yaml";
|
||||
};
|
||||
|
||||
sops.secrets.deepseek_api_key = {
|
||||
sopsFile = "${inputs.secrets}/secrets/common/api-keys.yaml";
|
||||
};
|
||||
|
||||
# Z.AI Coding Plan key (used by opencode zai-coding-plan provider)
|
||||
sops.secrets.zai_api_key = {
|
||||
sopsFile = "${inputs.secrets}/secrets/common/api-keys.yaml";
|
||||
};
|
||||
|
||||
# Alpha Vantage key (used by portfolio-update-prices)
|
||||
sops.secrets.alphavantage_api_key = {
|
||||
sopsFile = "${inputs.secrets}/secrets/common/api-keys.yaml";
|
||||
};
|
||||
|
||||
# Datadog API keys (used by opencode dd-log tool)
|
||||
sops.secrets.dd_api_key = {
|
||||
sopsFile = "${inputs.secrets}/secrets/common/datadog.yaml";
|
||||
};
|
||||
sops.secrets.dd_app_key = {
|
||||
sopsFile = "${inputs.secrets}/secrets/common/datadog.yaml";
|
||||
};
|
||||
|
||||
# dd-log reads *_FILE variants so key material stays on disk (sops
|
||||
# runtime paths), not in every process environment.
|
||||
home.sessionVariables = {
|
||||
DD_API_KEY_FILE = config.sops.secrets.dd_api_key.path;
|
||||
DD_APP_KEY_FILE = config.sops.secrets.dd_app_key.path;
|
||||
};
|
||||
}
|
||||
12
home/profiles/iso/base.nix
Normal file
12
home/profiles/iso/base.nix
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
home = {
|
||||
username = "nixos";
|
||||
homeDirectory = "/home/nixos";
|
||||
stateVersion = "25.11";
|
||||
};
|
||||
|
||||
# Let Home Manager install and manage itself
|
||||
programs.home-manager.enable = true;
|
||||
}
|
||||
41
home/profiles/iso/default.nix
Normal file
41
home/profiles/iso/default.nix
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
imports = [
|
||||
# Editors - vital!
|
||||
../../editors/nvim
|
||||
|
||||
# Terminal configuration
|
||||
../../terminal
|
||||
../../terminal/emulators/ghostty.nix
|
||||
|
||||
# Wayland/Hyprland essentials
|
||||
../../programs/wayland
|
||||
../../programs/gtk.nix
|
||||
../../programs/gpg.nix
|
||||
|
||||
# Desktop services
|
||||
../../services/wayland/hyprpaper.nix
|
||||
../../services/mako.nix
|
||||
../../services/gpg.nix
|
||||
../../services/media/playerctl.nix
|
||||
../../services/trayscale.nix
|
||||
|
||||
# Tailscale auto-auth (prompts for YubiKey PIN after login)
|
||||
./tailscale.nix
|
||||
];
|
||||
|
||||
# Generic Hyprland settings for any display
|
||||
wayland.windowManager.hyprland.settings = {
|
||||
# Auto-detect monitors
|
||||
monitor = [
|
||||
", preferred, auto, 1"
|
||||
];
|
||||
|
||||
# Generic touchpad settings
|
||||
input = {
|
||||
touchpad = {
|
||||
natural_scroll = true;
|
||||
tap-to-click = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
76
home/profiles/iso/tailscale.nix
Normal file
76
home/profiles/iso/tailscale.nix
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# Tailscale auto-authentication for live ISO (user session)
|
||||
# Runs after graphical login so pinentry-qt can prompt for YubiKey PIN
|
||||
{ config, pkgs, inputs, ... }:
|
||||
|
||||
let
|
||||
secretsFile = "${inputs.secrets}/secrets/iso/tailscale.yaml";
|
||||
in {
|
||||
# Systemd user service to authenticate Tailscale after graphical login
|
||||
systemd.user.services.tailscale-autoconnect = {
|
||||
Unit = {
|
||||
Description = "Authenticate to Tailscale with YubiKey-decrypted auth key";
|
||||
# Run after graphical session is ready (pinentry-qt needs display)
|
||||
After = [ "graphical-session.target" ];
|
||||
Wants = [ "graphical-session.target" ];
|
||||
};
|
||||
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
|
||||
# Environment for pinentry and age-plugin-yubikey
|
||||
Environment = [
|
||||
"PINENTRY_PROGRAM=${pkgs.pinentry-qt}/bin/pinentry-qt"
|
||||
];
|
||||
|
||||
ExecStart = pkgs.writeShellScript "tailscale-autoconnect" ''
|
||||
set -euo pipefail
|
||||
|
||||
echo "Tailscale auto-connect: checking status..."
|
||||
|
||||
# Check if already authenticated
|
||||
status="$(${pkgs.tailscale}/bin/tailscale status -json 2>/dev/null | ${pkgs.jq}/bin/jq -r '.BackendState' 2>/dev/null || echo 'Unknown')"
|
||||
|
||||
if [ "$status" = "Running" ]; then
|
||||
echo "Tailscale already running, skipping authentication"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Tailscale auto-connect: decrypting auth key..."
|
||||
echo "You may be prompted for your YubiKey PIN..."
|
||||
|
||||
# Decrypt the auth key using sops
|
||||
# This will trigger pinentry-qt for the YubiKey PIV PIN
|
||||
AUTH_KEY="$(${pkgs.sops}/bin/sops -d --extract '["tailscale_auth_key"]' ${secretsFile} 2>/dev/null)" || {
|
||||
echo "ERROR: Failed to decrypt Tailscale auth key"
|
||||
echo "Make sure your YubiKey is connected and try: tailscale up"
|
||||
# Send desktop notification
|
||||
${pkgs.libnotify}/bin/notify-send -u critical "Tailscale" "Failed to decrypt auth key. Run 'tailscale up' manually."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -z "$AUTH_KEY" ]; then
|
||||
echo "ERROR: Auth key is empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Tailscale auto-connect: authenticating..."
|
||||
${pkgs.tailscale}/bin/tailscale up --authkey="$AUTH_KEY" --operator=nixos
|
||||
|
||||
echo "Tailscale authenticated successfully!"
|
||||
${pkgs.libnotify}/bin/notify-send -u normal "Tailscale" "Connected to Tailscale network"
|
||||
'';
|
||||
};
|
||||
|
||||
Install = {
|
||||
WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
};
|
||||
|
||||
# Ensure required packages are available
|
||||
home.packages = with pkgs; [
|
||||
age-plugin-yubikey
|
||||
sops
|
||||
libnotify
|
||||
];
|
||||
}
|
||||
15
home/profiles/phantom/default.nix
Normal file
15
home/profiles/phantom/default.nix
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Phantom USB - Home Manager configuration
|
||||
# Inherits from framework13 profile for familiar environment
|
||||
# Adds phantom-specific overrides for ephemeral usage
|
||||
{
|
||||
imports = [
|
||||
# Inherit the full framework13 experience
|
||||
../framework13
|
||||
|
||||
# Override monitor config for unknown displays
|
||||
./hyprland.nix
|
||||
];
|
||||
|
||||
# Phantom-specific home settings can go here
|
||||
# Most config comes from framework13 profile
|
||||
}
|
||||
27
home/profiles/phantom/hyprland.nix
Normal file
27
home/profiles/phantom/hyprland.nix
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Phantom USB - Hyprland monitor configuration
|
||||
# Auto-detect displays since we don't know what machine we'll boot on
|
||||
{
|
||||
wayland.windowManager.hyprland.settings = {
|
||||
# Auto-configure any connected monitor
|
||||
# This overrides the framework13 specific monitor config
|
||||
monitor = [
|
||||
# Prefer high resolution, auto-position, with scaling
|
||||
",preferred,auto,1"
|
||||
];
|
||||
|
||||
# Generic touchpad settings (works on most laptops)
|
||||
device = {
|
||||
name = "generic-touchpad";
|
||||
natural_scroll = true;
|
||||
};
|
||||
|
||||
# Input configuration for unknown hardware
|
||||
input = {
|
||||
# Enable tap-to-click for any touchpad
|
||||
touchpad = {
|
||||
tap-to-click = true;
|
||||
natural_scroll = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
35
home/profiles/steamdeck/default.nix
Normal file
35
home/profiles/steamdeck/default.nix
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{ config, pkgs, inputs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./syncthing.nix
|
||||
../../programs/keepassxc.nix
|
||||
../../programs/games/minecraft
|
||||
../../programs/games/openrct2
|
||||
# Configured Claude Code (settings/agents/MCP/plugins under ~/.claude). Required
|
||||
# by the openrct2 coding-agent fork, whose in-game terminal launches `claude`.
|
||||
../../terminal/programs/claude
|
||||
./media.nix
|
||||
];
|
||||
|
||||
home = {
|
||||
username = "deck";
|
||||
homeDirectory = "/home/deck";
|
||||
stateVersion = "25.11";
|
||||
};
|
||||
|
||||
# Let Home Manager install and manage itself
|
||||
programs.home-manager.enable = true;
|
||||
|
||||
# claude-code (pulled in by programs.claude-code and baked into the openrct2
|
||||
# coding-agent wrapper) is unfree. This host's Home Manager builds its OWN pkgs
|
||||
# (useGlobalPkgs is not set for steamdeck — see system/default.nix), so the
|
||||
# system-level allowUnfree in hosts/steamdeck/default.nix does not reach it.
|
||||
# Opt in here too.
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
# keepassxc.nix sets `programs.keepassxc.autostart = true`, which (on
|
||||
# home-manager-unstable, used by this host) asserts xdg.autostart must be on.
|
||||
# Framework13's stable HM lacks that assertion, so this is only needed here.
|
||||
xdg.autostart.enable = true;
|
||||
}
|
||||
98
home/profiles/steamdeck/media.nix
Normal file
98
home/profiles/steamdeck/media.nix
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
mediaPaths = {
|
||||
# Transmission lands completed files here (hosts/steamdeck/transmission.nix).
|
||||
# The deck user's primary group is `users`, which owns this 0775 dir, so
|
||||
# Kodi can read it. Previously pointed at ~/torrents — nothing was there.
|
||||
torrents = "/srv/torrents/downloads";
|
||||
};
|
||||
|
||||
# Pre-configure Kodi media sources so it's ready on first launch
|
||||
kodiSources = pkgs.writeText "sources.xml" ''
|
||||
<sources>
|
||||
<video>
|
||||
<default pathversion="1"></default>
|
||||
<source>
|
||||
<name>Torrents</name>
|
||||
<path pathversion="1">${mediaPaths.torrents}/</path>
|
||||
<allowsharing>true</allowsharing>
|
||||
</source>
|
||||
</video>
|
||||
<music>
|
||||
<default pathversion="1"></default>
|
||||
<source>
|
||||
<name>Torrents</name>
|
||||
<path pathversion="1">${mediaPaths.torrents}/</path>
|
||||
<allowsharing>true</allowsharing>
|
||||
</source>
|
||||
</music>
|
||||
</sources>
|
||||
'';
|
||||
|
||||
# Hardware-friendly defaults plus a large read-ahead cache. buffermode=1
|
||||
# buffers all local + network sources, so playing a file that Transmission is
|
||||
# still writing to (or off a NAS later) doesn't stutter on the Deck's storage.
|
||||
kodiAdvancedSettings = pkgs.writeText "advancedsettings.xml" ''
|
||||
<advancedsettings version="1.0">
|
||||
<videoplayer>
|
||||
<usestagingbuffer>true</usestagingbuffer>
|
||||
</videoplayer>
|
||||
<cache>
|
||||
<buffermode>1</buffermode>
|
||||
<memorysize>209715200</memorysize>
|
||||
<readfactor>4</readfactor>
|
||||
</cache>
|
||||
</advancedsettings>
|
||||
'';
|
||||
|
||||
# Seed for the web remote (Kore/Yatse phone apps, browser UI on :8080).
|
||||
# NOT symlinked into place: Kodi rewrites guisettings.xml on every exit, so a
|
||||
# read-only store symlink would break it. The activation script below copies
|
||||
# this once, only if no guisettings exists yet, then Kodi owns the file.
|
||||
kodiGuiSettings = pkgs.writeText "guisettings.xml" ''
|
||||
<settings version="2">
|
||||
<setting id="services.webserver" default="false">true</setting>
|
||||
<setting id="services.webserverport" default="false">8080</setting>
|
||||
<setting id="services.webserverusername" default="false">kodi</setting>
|
||||
<setting id="services.esallinterfaces" default="false">true</setting>
|
||||
</settings>
|
||||
'';
|
||||
in
|
||||
{
|
||||
home.packages = [
|
||||
pkgs.kodi
|
||||
];
|
||||
|
||||
# Read-only declarative config (Kodi only reads these)
|
||||
home.file = {
|
||||
".kodi/userdata/sources.xml".source = kodiSources;
|
||||
".kodi/userdata/advancedsettings.xml".source = kodiAdvancedSettings;
|
||||
};
|
||||
|
||||
# Writable seed for guisettings — copy once, leave Kodi to manage afterwards.
|
||||
home.activation.kodiGuiSettings = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
guifile="$HOME/.kodi/userdata/guisettings.xml"
|
||||
if [ ! -f "$guifile" ]; then
|
||||
run mkdir -p "$HOME/.kodi/userdata"
|
||||
run cp ${kodiGuiSettings} "$guifile"
|
||||
run chmod u+w "$guifile"
|
||||
fi
|
||||
'';
|
||||
|
||||
# XDG desktop entry for Kodi - add to Steam via "Add a Non-Steam Game" in Desktop Mode
|
||||
xdg.desktopEntries = {
|
||||
kodi-gamemode = {
|
||||
name = "Kodi";
|
||||
genericName = "Media Center";
|
||||
comment = "Browse and play local media";
|
||||
exec = "${pkgs.kodi}/bin/kodi --standalone";
|
||||
icon = "kodi";
|
||||
categories = [ "AudioVideo" "Video" "Player" ];
|
||||
terminal = false;
|
||||
settings = {
|
||||
StartupWMClass = "Kodi";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
138
home/profiles/steamdeck/syncthing.nix
Normal file
138
home/profiles/steamdeck/syncthing.nix
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
{ config, lib, pkgs, inputs, ... }:
|
||||
|
||||
let
|
||||
# Secrets file location
|
||||
secretsFile = "${inputs.secrets}/secrets/steamdeck/syncthing.yaml";
|
||||
|
||||
# Device IDs - obtained from Syncthing on each device (run:
|
||||
# syncthing cli show system | grep myID); kept in the private
|
||||
# flake's vars so they stay out of the public repo
|
||||
deviceIds = inputs.secrets.vars.syncthing.deviceIds;
|
||||
|
||||
# Common versioning configuration
|
||||
# Trashcan versioning moves deleted/replaced files to .stversions
|
||||
versioningConfig = {
|
||||
type = "trashcan";
|
||||
params = {
|
||||
cleanoutDays = "30"; # Keep deleted files for 30 days
|
||||
};
|
||||
};
|
||||
in {
|
||||
# SOPS secrets configuration for Syncthing GUI authentication
|
||||
sops = {
|
||||
age.keyFile = "/home/deck/.config/sops/age/keys.txt";
|
||||
secrets = {
|
||||
syncthing_gui_password = {
|
||||
sopsFile = secretsFile;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.syncthing = {
|
||||
enable = true;
|
||||
|
||||
# Declarative configuration
|
||||
overrideDevices = true;
|
||||
overrideFolders = true;
|
||||
|
||||
# Note: We use 127.0.0.1 here because home-manager's syncthing-init service
|
||||
# uses guiAddress for API calls (0.0.0.0 is not a valid connect address).
|
||||
# The actual binding address (0.0.0.0:8384) is set via ExecStartPost below.
|
||||
guiAddress = "127.0.0.1:8384";
|
||||
|
||||
# GUI credentials from SOPS secrets
|
||||
guiCredentials = {
|
||||
username = "deck";
|
||||
passwordFile = config.sops.secrets.syncthing_gui_password.path;
|
||||
};
|
||||
|
||||
settings = {
|
||||
gui = {
|
||||
theme = "dark";
|
||||
# Note: address is set via systemd ExecStartPost below because
|
||||
# home-manager's syncthing module overwrites settings.gui.address
|
||||
# with guiAddress at the end of the init script.
|
||||
};
|
||||
|
||||
options = {
|
||||
localAnnounceEnabled = true;
|
||||
globalAnnounceEnabled = true;
|
||||
relaysEnabled = true;
|
||||
natEnabled = true;
|
||||
relayReconnectIntervalM = 30;
|
||||
autoUpgradeIntervalH = 0; # Disabled - managed by Nix
|
||||
crashReportingEnabled = false;
|
||||
startBrowser = false;
|
||||
};
|
||||
|
||||
devices = {
|
||||
"Framework13" = {
|
||||
id = deviceIds.framework13;
|
||||
autoAcceptFolders = false;
|
||||
addresses = [ "dynamic" ];
|
||||
};
|
||||
};
|
||||
|
||||
folders = {
|
||||
# Passwords folder - bidirectional sync with Framework13
|
||||
"Passwords" = {
|
||||
id = "passwords";
|
||||
path = "/home/deck/Passwords";
|
||||
devices = [ "Framework13" ];
|
||||
type = "sendreceive";
|
||||
versioning = versioningConfig;
|
||||
fsWatcherEnabled = true;
|
||||
rescanIntervalS = 3600;
|
||||
ignorePerms = true;
|
||||
};
|
||||
|
||||
# Steam game recordings - send-only to Framework13
|
||||
# Recordings made on Steam Deck sync to Framework13 for backup/editing
|
||||
"Steam Recordings" = {
|
||||
id = "steam-recordings";
|
||||
path = "/home/deck/.local/share/Steam/userdata/1834090003/gamerecordings";
|
||||
devices = [ "Framework13" ];
|
||||
# Send-only: Steam Deck sends recordings but doesn't receive changes
|
||||
type = "sendonly";
|
||||
# No versioning needed for recordings (large files, append-only)
|
||||
fsWatcherEnabled = true;
|
||||
rescanIntervalS = 300; # Check every 5 minutes for new recordings
|
||||
ignorePerms = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Ensure syncthing and syncthing-init services wait for SOPS to decrypt secrets
|
||||
systemd.user.services.syncthing = {
|
||||
Unit = {
|
||||
After = [ "sops-nix.service" ];
|
||||
Wants = [ "sops-nix.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
systemd.user.services.syncthing-init = {
|
||||
Unit = {
|
||||
After = [ "sops-nix.service" ];
|
||||
Wants = [ "sops-nix.service" ];
|
||||
};
|
||||
Service = {
|
||||
# After the init script runs, set the GUI address to 0.0.0.0:8384 for remote access.
|
||||
# This is needed because home-manager's syncthing module uses guiAddress for both
|
||||
# API calls (must be 127.0.0.1) and the final bind address (we want 0.0.0.0).
|
||||
ExecStartPost = let
|
||||
script = pkgs.writeShellScript "syncthing-set-gui-address" ''
|
||||
API_KEY=$(${pkgs.gnugrep}/bin/grep -oP '(?<=<apikey>)[^<]+' ~/.local/state/syncthing/config.xml)
|
||||
${pkgs.curl}/bin/curl -s -X PATCH \
|
||||
-H "X-API-Key: $API_KEY" \
|
||||
-d '{"address": "0.0.0.0:8384"}' \
|
||||
http://127.0.0.1:8384/rest/config/gui
|
||||
# Restart syncthing to apply the address change
|
||||
${pkgs.curl}/bin/curl -s -X POST \
|
||||
-H "X-API-Key: $API_KEY" \
|
||||
http://127.0.0.1:8384/rest/system/restart
|
||||
'';
|
||||
in "${script}";
|
||||
};
|
||||
};
|
||||
}
|
||||
50
home/programs/browsers/arkenfox-overrides.js
Normal file
50
home/programs/browsers/arkenfox-overrides.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*** arkenfox user-overrides.js — `personal` profile (identified daily driver)
|
||||
*
|
||||
* Loaded AFTER arkenfox's user.js, so every pref here WINS over arkenfox.
|
||||
* This file is OURS to own — arkenfox itself says you MUST maintain overrides
|
||||
* and read the wiki; treat it as the single place we relax/tighten arkenfox.
|
||||
*
|
||||
* Deliberately NO resistFingerprinting here: `personal` is logged-in, so
|
||||
* blending into a crowd is pointless and just breaks sites. The ghost/anonymity
|
||||
* job lives in Mullvad Browser (browsers/mullvad.nix), not this profile.
|
||||
***/
|
||||
|
||||
/* --- light anti-fingerprinting: FPP, NOT RFP ---------------------------
|
||||
* FPP randomizes canvas + audio (and curbs some enumeration) per-site,
|
||||
* per-session, WITHOUT RFP's usability tax (no UTC clock, no letterbox, no
|
||||
* forced theme). The right tier for a logged-in driver: it neutralizes the
|
||||
* STABLE cross-site IDs (canvas/audio) that survive cookie partitioning —
|
||||
* exactly personal's anti-correlation goal. arkenfox leaves these at default
|
||||
* (off in normal windows on non-strict ETP), so set them explicitly. */
|
||||
user_pref("privacy.fingerprintingProtection", true); // [arkenfox: default/off in normal windows]
|
||||
user_pref("privacy.fingerprintingProtection.pbmode", true); // also in private windows
|
||||
/* FPP=true only ARMS the engine; which targets fire comes from remote-settings
|
||||
* and did NOT include canvas (verified: toDataURL stayed at the unprotected
|
||||
* hash). Force the canvas target locally so readback is noised per-session.
|
||||
* `+Target` augments the remote set; CanvasRandomization is the silent one
|
||||
* (CanvasImageExtractionPrompt would nag on every extraction — omitted). */
|
||||
user_pref("privacy.fingerprintingProtection.overrides", "+CanvasRandomization");
|
||||
|
||||
/* --- stay logged in across restarts ------------------------------------
|
||||
* arkenfox 2811 wipes cookies + storage on shutdown -> logged out every
|
||||
* restart. Keep the rest of the shutdown wipe (cache, formdata) but persist
|
||||
* logins. This is the single change that makes arkenfox livable as a daily. */
|
||||
user_pref("privacy.clearOnShutdown_v2.cookiesAndStorage", false); // [arkenfox: true]
|
||||
|
||||
/* --- daily-driver quality of life --------------------------------------- */
|
||||
user_pref("browser.cache.disk.enable", true); // [arkenfox: false] still wiped on shutdown via clearOnShutdown_v2.cache
|
||||
user_pref("browser.startup.page", 3); // [arkenfox: 0] restore previous session (cookies now persist)
|
||||
user_pref("browser.sessionstore.privacy_level", 0); // [arkenfox: 2] let session restore actually work
|
||||
|
||||
/* --- our standing prefs (arkenfox leaves these at default/unset) -------- */
|
||||
user_pref("network.trr.mode", 5); // DoH off: system DNS is Mullvad in-tunnel; browser DoH to Cloudflare bypasses it (DNS leak)
|
||||
user_pref("signon.rememberSignons", false); // KeePassXC handles autofill, not Firefox's pw manager
|
||||
|
||||
/* --- AI off (arkenfox doesn't touch browser.ai.control) ----------------- */
|
||||
user_pref("browser.ai.control.default", "blocked");
|
||||
user_pref("browser.ai.control.linkPreviewKeyPoints", "blocked");
|
||||
user_pref("browser.ai.control.pdfjsAltText", "blocked");
|
||||
user_pref("browser.ai.control.sidebarChatbot", "blocked");
|
||||
user_pref("browser.ai.control.smartTabGroups", "blocked");
|
||||
user_pref("browser.ai.control.smartWindow", "blocked");
|
||||
user_pref("browser.ai.control.translations", "blocked");
|
||||
70
home/programs/browsers/firefox.nix
Normal file
70
home/programs/browsers/firefox.nix
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
# personal — daily driver. Full arkenfox user.js + OUR maintained overrides
|
||||
# (browsers/arkenfox-overrides.js). RFP stays OFF: you're logged
|
||||
# in, so blending is pointless; the ghost job is Mullvad Browser.
|
||||
# work — sacrificial / walled. Permissive for corp SSO; kept separate so
|
||||
# employer MITM/MDM never touches personal.
|
||||
stylix.targets.firefox.profileNames = [ "personal" "work" ];
|
||||
|
||||
programs.firefox =
|
||||
let
|
||||
# arkenfox user.js — hash-pinned. Applied to `personal`, then overridden
|
||||
# by arkenfox-overrides.js (loaded after, so our prefs win).
|
||||
arkenfoxUserJs =
|
||||
let
|
||||
version = "144.0";
|
||||
in
|
||||
pkgs.fetchurl {
|
||||
url = "https://raw.githubusercontent.com/arkenfox/user.js/${version}/user.js";
|
||||
hash = "sha256-5KszxpFImRdc9wNeDlei1/CKyIfY+VfxGZ5+Sbvn4z4=";
|
||||
};
|
||||
|
||||
aiOff = {
|
||||
"browser.ai.control.default" = "blocked";
|
||||
"browser.ai.control.linkPreviewKeyPoints" = "blocked";
|
||||
"browser.ai.control.pdfjsAltText" = "blocked";
|
||||
"browser.ai.control.sidebarChatbot" = "blocked";
|
||||
"browser.ai.control.smartTabGroups" = "blocked";
|
||||
"browser.ai.control.smartWindow" = "blocked";
|
||||
"browser.ai.control.translations" = "blocked";
|
||||
};
|
||||
|
||||
# shared anti-cruft baseline (no fingerprint/cookie aggression here)
|
||||
baseline = aiOff // {
|
||||
"datareporting.healthreport.uploadEnabled" = false;
|
||||
"toolkit.telemetry.enabled" = false;
|
||||
"browser.newtabpage.activity-stream.feeds.telemetry" = false;
|
||||
"extensions.pocket.enabled" = false;
|
||||
"browser.newtabpage.activity-stream.showSponsored" = false;
|
||||
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
|
||||
};
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
configPath = ".mozilla/firefox"; # keep legacy path (26.05 default moved to XDG); silences the warning
|
||||
|
||||
# daily driver — reuses existing on-disk dir → history + addons + containers intact.
|
||||
# arkenfox first (it loses to anything written after it), then OUR overrides win.
|
||||
profiles.personal = {
|
||||
id = 0;
|
||||
isDefault = true;
|
||||
path = "4f3s13kd.default";
|
||||
extraConfig =
|
||||
builtins.readFile arkenfoxUserJs
|
||||
+ "\n"
|
||||
+ builtins.readFile ./arkenfox-overrides.js;
|
||||
};
|
||||
|
||||
# permissive profile for corp SSO / internal tooling — starts empty, add addons manually
|
||||
profiles.work = {
|
||||
id = 1;
|
||||
path = "work.work";
|
||||
settings = baseline // {
|
||||
"network.cookie.cookieBehavior" = 0; # accept all (cross-site SSO redirects)
|
||||
"signon.rememberSignons" = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
7
home/programs/browsers/ladybird.nix
Normal file
7
home/programs/browsers/ladybird.nix
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
ladybird
|
||||
];
|
||||
}
|
||||
34
home/programs/browsers/mullvad.nix
Normal file
34
home/programs/browsers/mullvad.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{ pkgs, inputs, ... }:
|
||||
|
||||
let
|
||||
inherit (inputs.secrets.vars.i2pProxy) host port;
|
||||
|
||||
mullvad-browser-i2p = pkgs.mullvad-browser.override {
|
||||
extraPrefs = ''
|
||||
lockPref("network.proxy.type", 1); // 1 = manual
|
||||
lockPref("network.proxy.http", "${host}");
|
||||
lockPref("network.proxy.http_port", ${toString port});
|
||||
lockPref("network.proxy.ssl", "${host}");
|
||||
lockPref("network.proxy.ssl_port", ${toString port});
|
||||
lockPref("network.proxy.share_proxy_settings", true);
|
||||
|
||||
// HTTP proxy resolves hostnames proxy-side; disable DoH/TRR so stuff
|
||||
// never leaks to a clearnet resolver.
|
||||
lockPref("network.trr.mode", 5); // 5 = off, never use TRR
|
||||
lockPref("network.proxy.socks_remote_dns", true);
|
||||
|
||||
// I2P tunnels are end-to-end encrypted and a .i2p/b32 address is the
|
||||
// hash of the destination's key (self-authenticating), so layered TLS is
|
||||
// redundant. Stop HTTPS-Only mode from upgrading http:// eepsites and
|
||||
// throwing "connection is not secure" warning pages. Safe here only
|
||||
// because the browser is locked to the I2P proxy with no outproxy, so it
|
||||
// cannot reach clearnet to downgrade a real site.
|
||||
lockPref("dom.security.https_only_mode", false);
|
||||
lockPref("dom.security.https_only_mode_ever_enabled", false);
|
||||
lockPref("dom.security.https_first", false);
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
home.packages = [ mullvad-browser-i2p ];
|
||||
}
|
||||
15
home/programs/default.nix
Normal file
15
home/programs/default.nix
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
imports = [
|
||||
./browsers/firefox.nix
|
||||
./browsers/mullvad.nix
|
||||
./browsers/ladybird.nix
|
||||
./wayland
|
||||
./messaging.nix
|
||||
./office
|
||||
./hister.nix
|
||||
./development.nix
|
||||
./media
|
||||
./media/audacious
|
||||
./media/newsboat
|
||||
];
|
||||
}
|
||||
20
home/programs/development.nix
Normal file
20
home/programs/development.nix
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
(google-cloud-sdk.withExtraComponents (with google-cloud-sdk.components; [
|
||||
gke-gcloud-auth-plugin
|
||||
]))
|
||||
sops
|
||||
jq
|
||||
yq
|
||||
flyctl
|
||||
kubectl
|
||||
lazysql
|
||||
sqlit-tui
|
||||
lazydocker
|
||||
lazyjournal
|
||||
opensnitch-ui
|
||||
posting
|
||||
];
|
||||
}
|
||||
285
home/programs/games/minecraft/default.nix
Normal file
285
home/programs/games/minecraft/default.nix
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
# Declarative Minecraft Configuration for Steam Deck
|
||||
#
|
||||
# This module provides:
|
||||
# - Prism Launcher with multiple Java versions
|
||||
# - Declarative mod management (mods defined in mods/<name>/default.nix)
|
||||
# - Declarative server list (servers.nix)
|
||||
# - Declarative game options (options.nix)
|
||||
# - Automatic setup on home-manager switch
|
||||
# - Steam integration helpers
|
||||
#
|
||||
# Directory Structure:
|
||||
# minecraft/
|
||||
# ├── default.nix # This file - main configuration
|
||||
# ├── servers.nix # Server list configuration
|
||||
# ├── options.nix # Game options (FOV, render distance, etc.)
|
||||
# └── mods/
|
||||
# ├── default.nix # Mod aggregator
|
||||
# ├── fabric-api/ # Fabric API mod
|
||||
# │ └── default.nix
|
||||
# ├── yacl/ # Yet Another Config Lib
|
||||
# │ └── default.nix
|
||||
# └── controlify/ # Controller support
|
||||
# └── default.nix
|
||||
#
|
||||
{ config, pkgs, lib, inputs, ... }:
|
||||
|
||||
let
|
||||
# Minecraft version configuration
|
||||
mcVersion = "1.21.11";
|
||||
fabricLoaderVersion = "0.18.3";
|
||||
instanceName = "${mcVersion}";
|
||||
|
||||
# Import configurations
|
||||
modsConfig = import ./mods { inherit pkgs lib; };
|
||||
serversConfig = import ./servers.nix { inherit pkgs lib inputs; };
|
||||
optionsConfig = import ./options.nix { inherit lib; };
|
||||
|
||||
# Create a directory containing all mod files
|
||||
modsDir = pkgs.linkFarmFromDrvs "minecraft-mods-${mcVersion}" modsConfig.files;
|
||||
|
||||
# Prism Launcher with multiple Java versions
|
||||
prismlauncher = pkgs.prismlauncher.override {
|
||||
jdks = [
|
||||
pkgs.jdk21 # For 1.21.x
|
||||
pkgs.jdk17 # For 1.18-1.20.x
|
||||
pkgs.jdk8 # For older versions
|
||||
];
|
||||
gamemodeSupport = true;
|
||||
};
|
||||
|
||||
# Instance configuration file content
|
||||
instanceCfg = ''
|
||||
[General]
|
||||
AutoCloseConsole=false
|
||||
AutomaticJava=true
|
||||
CloseAfterLaunch=false
|
||||
ConfigVersion=1.2
|
||||
IgnoreJavaCompatibility=false
|
||||
InstanceType=OneSix
|
||||
LogPrePostOutput=true
|
||||
ManagedPack=false
|
||||
MaxMemAlloc=4096
|
||||
MinMemAlloc=512
|
||||
OverrideJavaLocation=false
|
||||
OverrideMemory=false
|
||||
iconKey=default
|
||||
name=${instanceName}
|
||||
notes=Declaratively managed by Nix
|
||||
'';
|
||||
|
||||
# mmc-pack.json for Fabric loader configuration
|
||||
mmcPackJson = builtins.toJSON {
|
||||
formatVersion = 1;
|
||||
components = [
|
||||
{
|
||||
cachedName = "LWJGL 3";
|
||||
cachedVersion = "3.3.3";
|
||||
cachedVolatile = true;
|
||||
dependencyOnly = true;
|
||||
uid = "org.lwjgl3";
|
||||
version = "3.3.3";
|
||||
}
|
||||
{
|
||||
cachedName = "Minecraft";
|
||||
cachedRequires = [
|
||||
{ suggests = "3.3.3"; uid = "org.lwjgl3"; }
|
||||
];
|
||||
cachedVersion = mcVersion;
|
||||
important = true;
|
||||
uid = "net.minecraft";
|
||||
version = mcVersion;
|
||||
}
|
||||
{
|
||||
cachedName = "Intermediary Mappings";
|
||||
cachedRequires = [
|
||||
{ equals = mcVersion; uid = "net.minecraft"; }
|
||||
];
|
||||
cachedVersion = mcVersion;
|
||||
cachedVolatile = true;
|
||||
dependencyOnly = true;
|
||||
uid = "net.fabricmc.intermediary";
|
||||
version = mcVersion;
|
||||
}
|
||||
{
|
||||
cachedName = "Fabric Loader";
|
||||
cachedRequires = [
|
||||
{ uid = "net.fabricmc.intermediary"; }
|
||||
];
|
||||
cachedVersion = fabricLoaderVersion;
|
||||
uid = "net.fabricmc.fabric-loader";
|
||||
version = fabricLoaderVersion;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# Create a directory with all config files for the setup script
|
||||
configFilesDir = pkgs.runCommand "minecraft-configs-${mcVersion}" {} ''
|
||||
mkdir -p $out
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (path: value: ''
|
||||
mkdir -p $out/$(dirname "${path}")
|
||||
cat > $out/${path} << 'CONFIGEOF'
|
||||
${value.text}
|
||||
CONFIGEOF
|
||||
'') allConfigFiles)}
|
||||
'';
|
||||
|
||||
# Setup script for mod installation
|
||||
setupMinecraftScript = pkgs.writeShellScriptBin "minecraft-setup" ''
|
||||
set -euo pipefail
|
||||
|
||||
PRISM_DATA="$HOME/.local/share/PrismLauncher"
|
||||
INSTANCE_NAME="${instanceName}"
|
||||
INSTANCE_DIR="$PRISM_DATA/instances/$INSTANCE_NAME"
|
||||
MODS_SRC="${modsDir}"
|
||||
CONFIGS_SRC="${configFilesDir}"
|
||||
|
||||
echo "============================================"
|
||||
echo " Minecraft Declarative Setup"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Instance: $INSTANCE_NAME"
|
||||
echo "Minecraft: ${mcVersion}"
|
||||
echo "Mod Loader: Fabric ${fabricLoaderVersion}"
|
||||
echo ""
|
||||
|
||||
# Create instance directory structure
|
||||
mkdir -p "$INSTANCE_DIR/.minecraft/mods"
|
||||
mkdir -p "$INSTANCE_DIR/.minecraft/config"
|
||||
|
||||
# === INSTANCE METADATA ===
|
||||
# Create instance.cfg if it doesn't exist
|
||||
if [ ! -f "$INSTANCE_DIR/instance.cfg" ]; then
|
||||
echo "Creating instance metadata..."
|
||||
cat > "$INSTANCE_DIR/instance.cfg" << 'INSTANCECFG'
|
||||
${instanceCfg}
|
||||
INSTANCECFG
|
||||
fi
|
||||
|
||||
# Create mmc-pack.json if it doesn't exist
|
||||
if [ ! -f "$INSTANCE_DIR/mmc-pack.json" ]; then
|
||||
echo "Creating mod loader configuration..."
|
||||
cat > "$INSTANCE_DIR/mmc-pack.json" << 'MMCPACK'
|
||||
${mmcPackJson}
|
||||
MMCPACK
|
||||
fi
|
||||
|
||||
# === MODS ===
|
||||
echo "Syncing declarative mods..."
|
||||
|
||||
MANAGED_MODS="$INSTANCE_DIR/.minecraft/mods/.nix-managed"
|
||||
|
||||
# Remove previously managed mods
|
||||
if [ -f "$MANAGED_MODS" ]; then
|
||||
while IFS= read -r mod; do
|
||||
if [ -f "$INSTANCE_DIR/.minecraft/mods/$mod" ]; then
|
||||
echo " - Removing old: $mod"
|
||||
rm -f "$INSTANCE_DIR/.minecraft/mods/$mod"
|
||||
fi
|
||||
done < "$MANAGED_MODS"
|
||||
fi
|
||||
|
||||
> "$MANAGED_MODS"
|
||||
|
||||
# Install mods
|
||||
echo ""
|
||||
echo "Installing mods:"
|
||||
for mod in "$MODS_SRC"/*; do
|
||||
mod_name=$(basename "$mod")
|
||||
echo " + $mod_name"
|
||||
cp -f "$mod" "$INSTANCE_DIR/.minecraft/mods/$mod_name"
|
||||
echo "$mod_name" >> "$MANAGED_MODS"
|
||||
done
|
||||
|
||||
# === CONFIG FILES ===
|
||||
# Install config files only if they don't exist (preserves user customizations)
|
||||
echo ""
|
||||
echo "Checking config files:"
|
||||
configs_created=0
|
||||
find "$CONFIGS_SRC" -type f | while read -r config_src; do
|
||||
config_rel="''${config_src#$CONFIGS_SRC/}"
|
||||
config_dest="$INSTANCE_DIR/.minecraft/$config_rel"
|
||||
|
||||
if [ ! -f "$config_dest" ]; then
|
||||
mkdir -p "$(dirname "$config_dest")"
|
||||
cp "$config_src" "$config_dest"
|
||||
echo " + Created: $config_rel"
|
||||
configs_created=$((configs_created + 1))
|
||||
else
|
||||
echo " ~ Exists: $config_rel (preserving user settings)"
|
||||
fi
|
||||
done
|
||||
|
||||
# === SERVERS ===
|
||||
echo ""
|
||||
${serversConfig.setupScript}
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Setup Complete!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Mods installed: $(wc -l < "$MANAGED_MODS")"
|
||||
echo "Servers configured: ${toString (builtins.length serversConfig.servers)}"
|
||||
echo ""
|
||||
echo "Instance '$INSTANCE_NAME' is ready in Prism Launcher!"
|
||||
echo ""
|
||||
echo "To add to Steam Gaming Mode, run: minecraft-add-to-steam"
|
||||
'';
|
||||
|
||||
# Steam integration helper
|
||||
addToSteamScript = pkgs.writeShellScriptBin "minecraft-add-to-steam" ''
|
||||
echo "============================================"
|
||||
echo " Add Minecraft to Steam Gaming Mode"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "To add Prism Launcher to Steam:"
|
||||
echo ""
|
||||
echo "1. Switch to Desktop Mode"
|
||||
echo "2. Open Steam"
|
||||
echo "3. Games → Add a Non-Steam Game"
|
||||
echo "4. Browse to: ${prismlauncher}/bin/prismlauncher"
|
||||
echo "5. Click 'Add Selected Programs'"
|
||||
echo ""
|
||||
echo "Or select 'Minecraft (Controller)' from the list."
|
||||
echo ""
|
||||
echo "The game will appear in Gaming Mode!"
|
||||
'';
|
||||
|
||||
# Combine all config files
|
||||
allConfigFiles = modsConfig.allConfigFiles // {
|
||||
# Add options.txt
|
||||
"options.txt" = optionsConfig.configFile;
|
||||
};
|
||||
|
||||
in {
|
||||
home.packages = [
|
||||
prismlauncher
|
||||
setupMinecraftScript
|
||||
addToSteamScript
|
||||
pkgs.ferium # CLI mod manager for manual updates
|
||||
];
|
||||
|
||||
# Run setup on home-manager switch
|
||||
home.activation.setupMinecraftMods = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
|
||||
run ${setupMinecraftScript}/bin/minecraft-setup
|
||||
'';
|
||||
|
||||
# Desktop entry for Steam discovery
|
||||
xdg.desktopEntries.minecraft-controller = {
|
||||
name = "Minecraft (Controller)";
|
||||
genericName = "Minecraft";
|
||||
comment = "Minecraft Java Edition with controller support";
|
||||
exec = "${prismlauncher}/bin/prismlauncher";
|
||||
icon = "prismlauncher";
|
||||
categories = [ "Game" ];
|
||||
terminal = false;
|
||||
settings = {
|
||||
StartupWMClass = "PrismLauncher";
|
||||
Keywords = "minecraft;game;gaming;";
|
||||
};
|
||||
};
|
||||
|
||||
# Config files are now installed by the setup script (copy if not exists)
|
||||
# This preserves user customizations made in-game
|
||||
}
|
||||
56
home/programs/games/minecraft/mods/controlify/default.nix
Normal file
56
home/programs/games/minecraft/mods/controlify/default.nix
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Controlify - Best controller support for Minecraft Java Edition
|
||||
# https://modrinth.com/mod/controlify
|
||||
#
|
||||
# Features:
|
||||
# - Steam Deck native support (back buttons, gyro, touchpads)
|
||||
# - Auto-pause when Steam/Quick Access menu opens
|
||||
# - Vibration/haptics and DualSense HD haptics
|
||||
# - On-screen keyboard
|
||||
# - Bedrock-like button guides
|
||||
# - GUI navigation with controller
|
||||
# - Gyroscope aiming support
|
||||
#
|
||||
# Dependencies: fabric-api, yacl
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
# Mod metadata
|
||||
name = "controlify";
|
||||
description = "Adds the best controller support to Minecraft Java Edition";
|
||||
homepage = "https://modrinth.com/mod/controlify";
|
||||
|
||||
# Mod file for Minecraft 1.21.11
|
||||
file = pkgs.fetchurl {
|
||||
url = "https://cdn.modrinth.com/data/DOUdJVEm/versions/4N7tPBpk/controlify-2.5.0%2B1.21.11-fabric.jar";
|
||||
sha512 = "cba7986ff8519a6f99c206f46d7990fccb73d25b3120f99f01f2bdec6b4715826d27aff2f294a2536e01e95fbaf8466b33f107cc7e3e2368fab7ec6d83e7a69e";
|
||||
name = "controlify.jar";
|
||||
};
|
||||
|
||||
# Default configuration optimized for Steam Deck
|
||||
configFiles = {
|
||||
"controlify.json" = {
|
||||
text = builtins.toJSON {
|
||||
# Enable Steam Deck enhanced driver for back buttons, gyro, touchpads
|
||||
enhanced_steam_deck_driver = true;
|
||||
|
||||
# Auto-calibrate controller deadzones on first use
|
||||
auto_calibrate_deadzone = true;
|
||||
|
||||
# Show button guides like Bedrock Edition
|
||||
show_button_guide = true;
|
||||
|
||||
# Enable vibration/haptics
|
||||
vibration_enabled = true;
|
||||
|
||||
# Enable on-screen keyboard for text input
|
||||
virtual_keyboard_enabled = true;
|
||||
|
||||
# Use Legacy Console Edition-like controls (optional)
|
||||
# Set to true for more console-like feel
|
||||
is_lce = false;
|
||||
};
|
||||
# Don't override if user has customized their settings
|
||||
force = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
49
home/programs/games/minecraft/mods/default.nix
Normal file
49
home/programs/games/minecraft/mods/default.nix
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Minecraft Mods Aggregator
|
||||
# This file collects all mod definitions from subdirectories
|
||||
#
|
||||
# To add a new mod:
|
||||
# 1. Create a new folder: mods/<mod-name>/
|
||||
# 2. Create default.nix with: { pkgs, lib, ... }: { name, description, homepage, file, configFiles }
|
||||
# 3. The mod will be automatically included
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
# Import a mod from its directory
|
||||
importMod = name: import ./${name} { inherit pkgs lib; };
|
||||
|
||||
# List of enabled mods (directory names)
|
||||
# Order matters for dependencies - load dependencies first
|
||||
enabledMods = [
|
||||
"fabric-api" # Core API - no dependencies
|
||||
"yacl" # Config library - depends on fabric-api
|
||||
"controlify" # Controller support - depends on fabric-api, yacl
|
||||
];
|
||||
|
||||
# Import all enabled mods
|
||||
mods = map importMod enabledMods;
|
||||
|
||||
# Create a lookup table by mod name
|
||||
modsByName = lib.listToAttrs (map (mod: { name = mod.name; value = mod; }) mods);
|
||||
|
||||
in {
|
||||
# List of all mod definitions
|
||||
inherit mods;
|
||||
|
||||
# Lookup table for accessing mods by name
|
||||
inherit modsByName;
|
||||
|
||||
# List of all mod files (derivations)
|
||||
files = map (mod: mod.file) mods;
|
||||
|
||||
# Combined config files from all mods
|
||||
# Returns: { "config/modname.json" = { text, force }; ... }
|
||||
allConfigFiles = lib.foldl' (acc: mod:
|
||||
acc // (lib.mapAttrs' (filename: value: {
|
||||
name = "config/${filename}";
|
||||
inherit value;
|
||||
}) mod.configFiles)
|
||||
) {} mods;
|
||||
|
||||
# Helper to get a specific mod
|
||||
getMod = name: modsByName.${name} or (throw "Unknown mod: ${name}");
|
||||
}
|
||||
20
home/programs/games/minecraft/mods/fabric-api/default.nix
Normal file
20
home/programs/games/minecraft/mods/fabric-api/default.nix
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Fabric API - Core modding API required by most Fabric mods
|
||||
# https://modrinth.com/mod/fabric-api
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
# Mod metadata
|
||||
name = "fabric-api";
|
||||
description = "Essential hooks for modding with Fabric";
|
||||
homepage = "https://modrinth.com/mod/fabric-api";
|
||||
|
||||
# Mod file for Minecraft 1.21.11
|
||||
file = pkgs.fetchurl {
|
||||
url = "https://cdn.modrinth.com/data/P7dR8mSH/versions/5oK85X7C/fabric-api-0.140.0%2B1.21.11.jar";
|
||||
sha512 = "f33d3aa6d4da877975eb0f814f9ac8c02f9641e0192402445912ddab43269efcc685ef14d59fd8ee53deb9b6ff4521442e06e1de1fd1284b426711404db5350b";
|
||||
name = "fabric-api.jar";
|
||||
};
|
||||
|
||||
# No additional config files needed
|
||||
configFiles = {};
|
||||
}
|
||||
21
home/programs/games/minecraft/mods/yacl/default.nix
Normal file
21
home/programs/games/minecraft/mods/yacl/default.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Yet Another Config Lib (YACL) - Configuration library
|
||||
# https://modrinth.com/mod/yacl
|
||||
# Required by: Controlify
|
||||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
# Mod metadata
|
||||
name = "yacl";
|
||||
description = "A builder-based configuration library for Minecraft";
|
||||
homepage = "https://modrinth.com/mod/yacl";
|
||||
|
||||
# Mod file for Minecraft 1.21.11
|
||||
file = pkgs.fetchurl {
|
||||
url = "https://cdn.modrinth.com/data/1eAoo2KR/versions/NWFsMIGn/yet_another_config_lib_v3-3.8.1%2B1.21.11-fabric.jar";
|
||||
sha512 = "3a4f015644454dffd423272a8b066e6b94d0f982184ca8c492d996a4f30eb8247b1648fe936413e178d4da3c9266c00e9b864eb9a91f0815348ba5662596986c";
|
||||
name = "yacl.jar";
|
||||
};
|
||||
|
||||
# No additional config files needed
|
||||
configFiles = {};
|
||||
}
|
||||
96
home/programs/games/minecraft/options.nix
Normal file
96
home/programs/games/minecraft/options.nix
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Declarative Minecraft Game Options
|
||||
# Generates options.txt from Nix configuration
|
||||
{ lib, ... }:
|
||||
|
||||
let
|
||||
# Game options - modify these as needed
|
||||
# Reference: https://minecraft.wiki/w/Options.txt
|
||||
options = {
|
||||
# Version (don't change unless you know what you're doing)
|
||||
version = 3700;
|
||||
|
||||
# Video Settings
|
||||
renderDistance = 12; # Chunks (2-32)
|
||||
simulationDistance = 12; # Chunks
|
||||
maxFps = 120; # FPS limit (10-260, or 260 for unlimited)
|
||||
graphicsMode = 1; # 0=Fast, 1=Fancy, 2=Fabulous
|
||||
ao = true; # Smooth lighting
|
||||
enableVsync = false; # VSync
|
||||
guiScale = 0; # 0=Auto, 1-4=Fixed
|
||||
fullscreen = false;
|
||||
entityDistanceScaling = 1.0; # Entity render distance (0.5-5.0)
|
||||
mipmapLevels = 4; # Mipmap (0-4)
|
||||
biomeBlendRadius = 2; # Biome blend (0-7)
|
||||
renderClouds = "true"; # Clouds: "true", "false", "fast"
|
||||
particles = 0; # 0=All, 1=Decreased, 2=Minimal
|
||||
entityShadows = true;
|
||||
|
||||
# Camera/Controls
|
||||
fov = 0.0; # FOV: -1.0 (30) to 1.0 (110), 0.0 = 70 (normal)
|
||||
mouseSensitivity = 0.5; # 0.0-1.0
|
||||
invertYMouse = false;
|
||||
rawMouseInput = true;
|
||||
autoJump = false; # Auto-jump (annoying, disable)
|
||||
toggleCrouch = false;
|
||||
toggleSprint = false;
|
||||
bobView = true; # View bobbing
|
||||
|
||||
# Accessibility
|
||||
gamma = 0.5; # Brightness: 0.0 (Moody) to 1.0 (Bright)
|
||||
showSubtitles = false;
|
||||
narrator = 0; # 0=Off, 1=All, 2=Chat, 3=System
|
||||
darkMojangStudiosBackground = true; # Easier on eyes
|
||||
|
||||
# Chat/UI
|
||||
chatVisibility = 0; # 0=Shown, 1=Commands only, 2=Hidden
|
||||
chatOpacity = 1.0;
|
||||
chatScale = 1.0;
|
||||
chatWidth = 1.0;
|
||||
chatHeightFocused = 1.0;
|
||||
chatHeightUnfocused = 0.4375;
|
||||
|
||||
# Misc
|
||||
pauseOnLostFocus = true; # Pause when window loses focus
|
||||
advancedItemTooltips = false; # F3+H tooltips
|
||||
realmsNotifications = false; # Disable Realms ads
|
||||
tutorialStep = "none"; # Skip tutorial
|
||||
skipMultiplayerWarning = true;
|
||||
skipRealms32bitWarning = true;
|
||||
|
||||
# Language
|
||||
lang = "en_us";
|
||||
|
||||
# Resource packs
|
||||
resourcePacks = [ "vanilla" "Fabric Mods" ];
|
||||
incompatibleResourcePacks = [];
|
||||
|
||||
# Main hand
|
||||
mainHand = "right"; # "left" or "right"
|
||||
};
|
||||
|
||||
# Convert Nix values to options.txt format
|
||||
formatValue = v:
|
||||
if builtins.isList v then
|
||||
"[${lib.concatMapStringsSep "," (x: "\"${toString x}\"") v}]"
|
||||
else if builtins.isBool v then
|
||||
(if v then "true" else "false")
|
||||
else if builtins.isString v then
|
||||
v
|
||||
else
|
||||
toString v;
|
||||
|
||||
# Generate options.txt content
|
||||
optionsText = lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (k: v: "${k}:${formatValue v}") options
|
||||
);
|
||||
|
||||
in {
|
||||
inherit options optionsText;
|
||||
|
||||
# For use in xdg.configFile
|
||||
configFile = {
|
||||
text = optionsText;
|
||||
# Don't force - let user customize in-game, only set defaults
|
||||
force = false;
|
||||
};
|
||||
}
|
||||
58
home/programs/games/minecraft/servers.nix
Normal file
58
home/programs/games/minecraft/servers.nix
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Declarative Minecraft Server List
|
||||
# Generates servers.dat (NBT format) from Nix configuration
|
||||
{ pkgs, lib, inputs, ... }:
|
||||
|
||||
let
|
||||
# Server list lives in the private flake's vars. Optional per-server keys:
|
||||
# hideAddress = false; # hide IP in UI
|
||||
# acceptTextures = 1; # 0=prompt, 1=enabled, 2=disabled
|
||||
servers = inputs.secrets.vars.minecraft.servers;
|
||||
|
||||
# Python script to generate servers.dat using nbtlib
|
||||
generateServersDat = pkgs.writers.writePython3Bin "generate-servers-dat" {
|
||||
libraries = [ pkgs.python3Packages.nbtlib ];
|
||||
} ''
|
||||
import sys
|
||||
import json
|
||||
from nbtlib import File, Compound, List, String, Byte
|
||||
|
||||
servers_json = sys.argv[1]
|
||||
output_path = sys.argv[2]
|
||||
|
||||
servers = json.loads(servers_json)
|
||||
|
||||
server_list = List[Compound]([
|
||||
Compound({
|
||||
'name': String(s['name']),
|
||||
'ip': String(s['ip']),
|
||||
'hideAddress': Byte(s.get('hideAddress', 0)),
|
||||
**({"acceptTextures": Byte(s['acceptTextures'])}
|
||||
if 'acceptTextures' in s else {})
|
||||
})
|
||||
for s in servers
|
||||
])
|
||||
|
||||
nbt_file = File({'servers': server_list})
|
||||
nbt_file.save(output_path, gzipped=False)
|
||||
print(f"Generated {output_path} with {len(servers)} server(s)")
|
||||
'';
|
||||
|
||||
# JSON representation of servers for the script
|
||||
serversJson = builtins.toJSON servers;
|
||||
|
||||
in {
|
||||
inherit servers generateServersDat serversJson;
|
||||
|
||||
# Setup script fragment to generate servers.dat
|
||||
setupScript = ''
|
||||
SERVERS_DAT="$INSTANCE_DIR/.minecraft/servers.dat"
|
||||
|
||||
# Only generate if servers.dat doesn't exist (preserve user additions)
|
||||
if [ ! -f "$SERVERS_DAT" ]; then
|
||||
echo "Generating servers.dat..."
|
||||
${generateServersDat}/bin/generate-servers-dat '${serversJson}' "$SERVERS_DAT"
|
||||
else
|
||||
echo "servers.dat exists, skipping (delete to regenerate)"
|
||||
fi
|
||||
'';
|
||||
}
|
||||
257
home/programs/games/openrct2/default.nix
Normal file
257
home/programs/games/openrct2/default.nix
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
# Declarative OpenRCT2 (Claude-playable "coding-agent" fork) for Steam Deck
|
||||
#
|
||||
# This installs jaysobel/OpenRCT2 @ coding-agent — a fork of OpenRCT2 that adds an
|
||||
# in-game "AI agent terminal": OpenRCT2 spawns the `claude` CLI (Claude Code) in a
|
||||
# pseudo-terminal and lets it drive the park through a small JSON-RPC bridge binary
|
||||
# called `rctctl`. The base game is unchanged, so the original RCT2 game data is
|
||||
# still REQUIRED to play.
|
||||
#
|
||||
# Packaging strategy
|
||||
# We override nixpkgs' `openrct2` with `overrideAttrs` rather than rewriting it,
|
||||
# so we inherit upstream's data provisioning (the objects/openmusic/opensfx/
|
||||
# title-sequences fetchurls + `postUnpack`) and its install() logic untouched.
|
||||
# We only swap in the fork source and layer on what the fork additionally needs:
|
||||
# * src -> the jaysobel coding-agent commit (pinned by rev+hash)
|
||||
# * python3 -> the fork's root CMakeLists unconditionally
|
||||
# `find_package(Python3 ... REQUIRED)`s it (base does not)
|
||||
# * libvterm-neovim -> the agent terminal renders ANSI via libvterm; CMake
|
||||
# probes the pkg-config module `vterm` (the neovim fork's
|
||||
# module/API) and hard-fails (FATAL_ERROR) when missing
|
||||
# * cmakeFlags -> REPLACED: the fork uses DOWNLOAD_OPENMSX (the older
|
||||
# option name), NOT 0.5.x's DOWNLOAD_OPENMUSIC, so the
|
||||
# inherited flag would silently not disable the download
|
||||
# * preConfigure="" -> DROPPED: this fork has no assets.json, so nixpkgs 0.5.x's
|
||||
# "grep the asset URLs out of assets.json" check would fail;
|
||||
# the asset versions are pinned in CMakeLists.txt instead
|
||||
# * buildFlags -> `agent_bundle` (game + CLI + rctctl + graphics assets)
|
||||
# * doInstallCheck -> false: versionCheckHook would assert a version string the
|
||||
# fork does not report
|
||||
# * postInstall -> copy the `rctctl` binary out (it has NO install() rule)
|
||||
# and materialise the "repo root" the launcher requires
|
||||
#
|
||||
# In-game agent runtime
|
||||
# When you open the in-game terminal the launcher (AIAgentLaunch.cpp) first
|
||||
# insists on finding a "repo root" — a directory holding BOTH `rctctl/CMakeLists
|
||||
# .txt` and `src/`, with the rctctl binary at `<root>/build/rctctl/rctctl` and the
|
||||
# agent brief at `<root>/ai-agent-workspace/IN_GAME_AGENT.md`. It locates that
|
||||
# root by walking up from the cwd / data dir / exe dir. Because the Nix store has
|
||||
# none of that next to the binary, `postInstall` builds a minimal synthetic root
|
||||
# under `$out/share/openrct2-agent/repo` and the wrapper `--chdir`s into it so the
|
||||
# cwd probe matches on the first try. OpenRCT2 resolves its OWN data via the
|
||||
# install prefix / XDG dirs (never cwd), so the chdir is safe for normal play.
|
||||
#
|
||||
# The terminal then launches the real `claude` binary, found by a plain PATH scan
|
||||
# (`FindExecutable("claude")`). Two things make that reliable here:
|
||||
# 1. The Steam Deck profile imports ../../terminal/programs/claude, enabling
|
||||
# `programs.claude-code` — so the deck user has a fully configured Claude Code
|
||||
# (settings, agents, MCP, plugins) under ~/.claude, read from $HOME at runtime.
|
||||
# 2. We ALSO prepend `pkgs.claude-code` to this package's own wrapper PATH, so the
|
||||
# openrct2 process sees `claude` even when launched from Steam Gaming Mode,
|
||||
# which does NOT source the Home Manager session PATH. Both reference the same
|
||||
# `pkgs.claude-code`, so the ~/.claude config applies either way.
|
||||
# claude-code is unfree (this host already allows unfree for Steam). If `claude` is
|
||||
# somehow absent the launcher degrades to a bundled REPL (`scripts/agent_bootstrap.sh`,
|
||||
# needs python3 — also on the wrapper PATH); that fallback is spawned via a hardcoded
|
||||
# `/bin/bash`, which exists on SteamOS but not on stock NixOS.
|
||||
#
|
||||
# RCT2 game data
|
||||
# No build-time path is hardcoded. On first launch OpenRCT2 prompts for the RCT2
|
||||
# install directory. This works whether RCT2 lives on the internal disk or an SD
|
||||
# card. The Steam install is typically at:
|
||||
# ~/.steam/steam/steamapps/common/RollerCoaster Tycoon 2 Triple Thrill Pack
|
||||
#
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
# jaysobel/OpenRCT2 @ coding-agent. Pinned to an exact commit; the fork has no
|
||||
# release tags. The hash is the NAR hash of the unpacked fetchFromGitHub output.
|
||||
openrct2 = pkgs.openrct2.overrideAttrs (old: {
|
||||
pname = "openrct2-agent";
|
||||
# Cosmetic only (doInstallCheck is disabled below). The fork tracks a pre-0.5.x
|
||||
# tree, so we do NOT label it 0.5.x to avoid implying release parity. (We ship
|
||||
# the inherited objects v1.7.9 data — a superset of the fork's pinned v1.7.3.)
|
||||
version = "unstable-3c278a85";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "jaysobel";
|
||||
repo = "OpenRCT2";
|
||||
rev = "3c278a8544d3f8f280e5043b5396c3a33751e20e";
|
||||
sha256 = "sha256-AQyTFQDr+ORJROoBxhy6KnuCcl/tQqCT1SGjkzsjYig=";
|
||||
};
|
||||
|
||||
# The fork's CMake unconditionally requires a Python3 interpreter at configure
|
||||
# time (CLI-validation test harness registration). pkg-config + makeWrapper +
|
||||
# versionCheckHook are already in the inherited nativeBuildInputs.
|
||||
nativeBuildInputs = old.nativeBuildInputs ++ [ pkgs.python3 ];
|
||||
|
||||
# libvterm-neovim ships `vterm.h` + a pkg-config module named `vterm`, which is
|
||||
# exactly what `PKG_CHECK_MODULES(LIBVTERM ... IMPORTED_TARGET vterm)` looks for
|
||||
# and what the fork's terminal code targets (neovim libvterm 0.3.x API). The
|
||||
# unrelated `libvterm` (0.99.x) would not satisfy the probe.
|
||||
buildInputs = old.buildInputs ++ [ pkgs.libvterm-neovim ];
|
||||
|
||||
# Replaced wholesale. Same intent as upstream (don't download assets at build
|
||||
# time; no Discord RPC) but using THIS fork's option names. Note the fork uses
|
||||
# DOWNLOAD_OPENMSX, not 0.5.x's DOWNLOAD_OPENMUSIC. DOWNLOAD_REPLAYS is a
|
||||
# CMAKE_DEPENDENT_OPTION that is forced OFF unless WITH_TESTS, so it is omitted.
|
||||
cmakeFlags = [
|
||||
(lib.cmakeBool "DOWNLOAD_OBJECTS" false)
|
||||
(lib.cmakeBool "DOWNLOAD_TITLE_SEQUENCES" false)
|
||||
(lib.cmakeBool "DOWNLOAD_OPENSFX" false)
|
||||
(lib.cmakeBool "DOWNLOAD_OPENMSX" false)
|
||||
(lib.cmakeBool "DISABLE_DISCORD_RPC" true)
|
||||
];
|
||||
|
||||
# Convenience target that builds everything the agent needs in one shot:
|
||||
# rctctl + openrct2 + openrct2-cli + graphics (the g2/fonts/tracks .dat
|
||||
# generators). `make install` still installs the game/CLI/data regardless,
|
||||
# because CMake's `install` target depends on `preinstall` -> `all`.
|
||||
buildFlags = [ "agent_bundle" ];
|
||||
|
||||
# Upstream's preConfigure greps asset URLs out of assets.json to verify pinned
|
||||
# versions. This fork has no assets.json (versions live in CMakeLists.txt via
|
||||
# set(..._VERSION ...)), so that check would error out. Drop it — the inherited
|
||||
# fetchurls/postUnpack still supply the data; see residual risk re: objects ver.
|
||||
preConfigure = "";
|
||||
|
||||
# Extend the inherited postPatch (which rewrites the doc path in
|
||||
# Platform.Linux.cpp — that "/usr/share/doc/openrct2" literal is still present
|
||||
# exactly once on this fork) with a Linux/GCC portability fix for the fork's own
|
||||
# agent code, which was written against macOS/clang: Diagnostic.cpp uses
|
||||
# time/localtime/strftime without <ctime> (libc++ includes it transitively,
|
||||
# libstdc++ does not).
|
||||
postPatch = (old.postPatch or "") + ''
|
||||
substituteInPlace src/openrct2/Diagnostic.cpp \
|
||||
--replace-fail '#include "Diagnostic.h"' '#include <ctime>${"\n"}#include "Diagnostic.h"'
|
||||
'';
|
||||
|
||||
# OpenRCT2 builds with global -Werror and a large strict warning set. The fork's
|
||||
# own agent code was written against macOS/clang and trips several warnings that
|
||||
# are GCC-only or behave differently there — e.g. ShellProcess.cpp's
|
||||
# `errno == EAGAIN || errno == EWOULDBLOCK` (-Wlogical-op: those macros are equal
|
||||
# on Linux) and AIAgentTerminal.cpp parameters shadowing window members (-Wshadow).
|
||||
# We package this third-party fork rather than maintain it, so downgrade -Werror to
|
||||
# plain warnings instead of patching each site. cc-wrapper appends NIX_CFLAGS_COMPILE
|
||||
# after the project's own flags, so this wins.
|
||||
NIX_CFLAGS_COMPILE = "-Wno-error";
|
||||
|
||||
# The fork reports a different version string than upstream's `version`, which
|
||||
# would trip versionCheckHook. Disable the install check entirely.
|
||||
doInstallCheck = false;
|
||||
|
||||
# Keep the inherited wrapProgram (rct1Path/rct2Path support — no-ops here since
|
||||
# this module pins neither), then provision the agent bits on top.
|
||||
postInstall = (old.postInstall or "") + ''
|
||||
|
||||
###########################################################################
|
||||
# jaysobel coding-agent wiring (in-game Claude terminal)
|
||||
###########################################################################
|
||||
# postInstall runs from the CMake build dir (<src>/build); the source tree is
|
||||
# its parent. agent_bundle built rctctl at build/rctctl/rctctl.
|
||||
srcRoot=$(cd .. && pwd)
|
||||
rctctlBin=$(find "$PWD" -type f -name rctctl -executable | head -n1)
|
||||
if [ -z "$rctctlBin" ]; then
|
||||
echo "openrct2-agent: rctctl binary not found after agent_bundle build" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# rctctl has no install() rule. Put it on PATH so the agent child (claude or
|
||||
# the bootstrap REPL) can discover it.
|
||||
install -Dm755 "$rctctlBin" "$out/bin/rctctl"
|
||||
|
||||
# Synthetic "repo root" the launcher insists on. LooksLikeRepoRoot() only
|
||||
# checks that rctctl/CMakeLists.txt and src/ exist; FindRctctlBinary() wants
|
||||
# <root>/build/rctctl/rctctl; SeedWorkspaceReadme() copies <root>/ai-agent-
|
||||
# workspace/IN_GAME_AGENT.md -> $HOME/.openrct2-agent/.claude/CLAUDE.md.
|
||||
repo="$out/share/openrct2-agent/repo"
|
||||
install -Dm644 "$srcRoot/rctctl/CMakeLists.txt" "$repo/rctctl/CMakeLists.txt"
|
||||
install -Dm755 "$rctctlBin" "$repo/build/rctctl/rctctl"
|
||||
install -Dm644 "$srcRoot/ai-agent-workspace/IN_GAME_AGENT.md" "$repo/ai-agent-workspace/IN_GAME_AGENT.md"
|
||||
install -Dm755 "$srcRoot/scripts/agent_bootstrap.sh" "$repo/scripts/agent_bootstrap.sh"
|
||||
mkdir -p "$repo/src" # marker dir; only its existence is checked
|
||||
|
||||
###########################################################################
|
||||
# Agent terminal fonts
|
||||
###########################################################################
|
||||
# The fork's TerminalFontPipeline (AIAgentTerminal.cpp) loads TTFs by hardcoded
|
||||
# filename from <data-dir>/data/fonts/ — i.e. $out/share/openrct2/data/fonts/.
|
||||
# But CMake's install(DIRECTORY "data/" ...) FLATTENS data/ into
|
||||
# $out/share/openrct2, so the bundled TTFs land in $out/share/openrct2/fonts and
|
||||
# the loader's data/fonts/ path does not exist → no font loads → the agent
|
||||
# console renders blank. Recreate the dir the loader expects, and make the
|
||||
# primary monospace a JetBrains Mono NERD FONT (the Mono variant keeps every
|
||||
# glyph single-cell) so Claude's TUI icons render instead of tofu. The two Noto
|
||||
# Sans Symbols faces (installed by the flattened data/) stay as glyph fallbacks.
|
||||
install -Dm644 ${pkgs.nerd-fonts.jetbrains-mono}/share/fonts/truetype/NerdFonts/JetBrainsMono/JetBrainsMonoNerdFontMono-Regular.ttf \
|
||||
"$out/share/openrct2/data/fonts/JetBrainsMono-Regular.ttf"
|
||||
install -Dm644 "$out/share/openrct2/fonts/NotoSansSymbols-Regular.ttf" \
|
||||
"$out/share/openrct2/data/fonts/NotoSansSymbols-Regular.ttf"
|
||||
install -Dm644 "$out/share/openrct2/fonts/NotoSansSymbols2-Regular.ttf" \
|
||||
"$out/share/openrct2/data/fonts/NotoSansSymbols2-Regular.ttf"
|
||||
|
||||
# Re-wrap: chdir into the synthetic root so DetectRepoRoot()'s cwd seed
|
||||
# matches, and make claude (the agent), rctctl, and python3 (REPL fallback)
|
||||
# reachable on the openrct2 process PATH regardless of how it was launched —
|
||||
# Steam Gaming Mode does not source the Home Manager session PATH. PATH is
|
||||
# PREPENDED, so a claude already on PATH (from programs.claude-code) still wins.
|
||||
wrapProgram "$out/bin/openrct2" \
|
||||
--chdir "$repo" \
|
||||
--prefix PATH : "$out/bin" \
|
||||
--prefix PATH : ${lib.makeBinPath [ pkgs.claude-code pkgs.python3 pkgs.bash ]}
|
||||
'';
|
||||
|
||||
meta = old.meta // {
|
||||
description = "OpenRCT2 jaysobel coding-agent fork (Claude-playable RollerCoaster Tycoon 2)";
|
||||
homepage = "https://github.com/jaysobel/OpenRCT2";
|
||||
changelog = "https://github.com/jaysobel/OpenRCT2/commits/coding-agent";
|
||||
mainProgram = "openrct2";
|
||||
platforms = lib.platforms.linux;
|
||||
broken = false;
|
||||
};
|
||||
});
|
||||
|
||||
# Steam integration helper
|
||||
addToSteamScript = pkgs.writeShellScriptBin "openrct2-add-to-steam" ''
|
||||
echo "============================================"
|
||||
echo " Add OpenRCT2 to Steam Gaming Mode"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "To add OpenRCT2 to Steam:"
|
||||
echo ""
|
||||
echo "1. Switch to Desktop Mode"
|
||||
echo "2. Open Steam"
|
||||
echo "3. Games -> Add a Non-Steam Game"
|
||||
echo "4. Browse to: ${openrct2}/bin/openrct2"
|
||||
echo "5. Click 'Add Selected Programs'"
|
||||
echo ""
|
||||
echo "Or select 'OpenRCT2' from the list."
|
||||
echo ""
|
||||
echo "On first launch, point OpenRCT2 at your RCT2 data, e.g.:"
|
||||
echo " ~/.steam/steam/steamapps/common/RollerCoaster Tycoon 2 Triple Thrill Pack"
|
||||
echo ""
|
||||
echo "The game will appear in Gaming Mode!"
|
||||
echo ""
|
||||
echo "In-game: click the robot toolbar button to open the Claude agent terminal."
|
||||
'';
|
||||
|
||||
in {
|
||||
home.packages = [
|
||||
openrct2
|
||||
addToSteamScript
|
||||
];
|
||||
|
||||
# Desktop entry for Steam discovery
|
||||
xdg.desktopEntries.openrct2 = {
|
||||
name = "OpenRCT2";
|
||||
genericName = "RollerCoaster Tycoon 2";
|
||||
comment = "Open source re-implementation of RollerCoaster Tycoon 2";
|
||||
exec = "${openrct2}/bin/openrct2";
|
||||
icon = "openrct2";
|
||||
categories = [ "Game" "Simulation" ];
|
||||
terminal = false;
|
||||
settings = {
|
||||
StartupWMClass = "openrct2";
|
||||
Keywords = "rct;rollercoaster;tycoon;game;gaming;";
|
||||
};
|
||||
};
|
||||
}
|
||||
60
home/programs/gpg.nix
Normal file
60
home/programs/gpg.nix
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
programs.gpg = {
|
||||
enable = true;
|
||||
# Use standard GPG settings
|
||||
settings = {
|
||||
# Use AES256 for symmetric encryption
|
||||
personal-cipher-preferences = "AES256 AES192 AES";
|
||||
personal-digest-preferences = "SHA512 SHA384 SHA256";
|
||||
personal-compress-preferences = "ZLIB BZIP2 ZIP Uncompressed";
|
||||
default-preference-list = "SHA512 SHA384 SHA256 AES256 AES192 AES ZLIB BZIP2 ZIP Uncompressed";
|
||||
|
||||
cert-digest-algo = "SHA512";
|
||||
|
||||
s2k-digest-algo = "SHA512";
|
||||
s2k-cipher-algo = "AES256";
|
||||
|
||||
charset = "utf-8";
|
||||
|
||||
no-comments = "";
|
||||
no-emit-version = "";
|
||||
no-greeting = "";
|
||||
|
||||
# Show fingerprints
|
||||
keyid-format = "0xlong";
|
||||
with-fingerprint = "";
|
||||
|
||||
list-options = "show-uid-validity";
|
||||
verify-options = "show-uid-validity";
|
||||
|
||||
# Ensure cross-certification on subkeys
|
||||
require-cross-certification = "";
|
||||
|
||||
require-secmem = "";
|
||||
no-symkey-cache = "";
|
||||
armor = "";
|
||||
|
||||
use-agent = "";
|
||||
|
||||
# Disable recipient key ID in messages (privacy)
|
||||
throw-keyids = "";
|
||||
|
||||
# Default signing key (master key - GPG auto-selects signing subkey from inserted YubiKey)
|
||||
default-key = "0x785150ECAABF7352";
|
||||
|
||||
# When encrypting, always include yourself as a recipient
|
||||
default-recipient-self = "";
|
||||
};
|
||||
|
||||
# Smart card daemon settings for YubiKey
|
||||
scdaemonSettings = {
|
||||
# Disable CCID driver to let PC/SC handle it
|
||||
disable-ccid = true;
|
||||
# Use PC/SC for card access
|
||||
pcsc-driver = "${pkgs.pcsclite.lib}/lib/libpcsclite.so";
|
||||
};
|
||||
};
|
||||
home.packages = [ pkgs.gnupg ];
|
||||
}
|
||||
10
home/programs/gtk.nix
Normal file
10
home/programs/gtk.nix
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
gtk = {
|
||||
enable = true;
|
||||
|
||||
gtk3.extraConfig = {
|
||||
gtk-im-module = "fcitx";
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
53
home/programs/hister.nix
Normal file
53
home/programs/hister.nix
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
conf = config.xdg.configHome;
|
||||
home = config.home.homeDirectory;
|
||||
yamlFormat = pkgs.formats.yaml { };
|
||||
|
||||
settings = {
|
||||
app = {
|
||||
directory = "${conf}/hister";
|
||||
search_url = "https://google.com/search?q={query}";
|
||||
log_level = "info";
|
||||
debug_sql = false;
|
||||
};
|
||||
server = {
|
||||
address = "127.0.0.1:4433";
|
||||
database = "${home}/Documents/hister.db";
|
||||
};
|
||||
hotkeys = {
|
||||
"alt+k" = "select_previous_result";
|
||||
"alt+j" = "select_next_result";
|
||||
"/" = "focus_search_input";
|
||||
enter = "open_result";
|
||||
"alt+enter" = "open_result_in_new_tab";
|
||||
"alt+o" = "open_query_in_search_engine";
|
||||
"alt+v" = "view_result_popup";
|
||||
tab = "autocomplete";
|
||||
"?" = "show_hotkeys";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
hister
|
||||
];
|
||||
|
||||
xdg.configFile."hister/config.yml".source = yamlFormat.generate "hister-config.yml" settings;
|
||||
|
||||
systemd.user.services.hister = {
|
||||
Unit = {
|
||||
Description = "Hister - Web history listener";
|
||||
After = [ "default.target" ];
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${pkgs.hister}/bin/hister listen";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
Install = {
|
||||
WantedBy = [ "default.target" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
60
home/programs/keepassxc.nix
Normal file
60
home/programs/keepassxc.nix
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{ config, ... }:
|
||||
|
||||
let
|
||||
# Stylix is only wired on framework13; live-iso/phantom lack it.
|
||||
hasStylix = config ? lib && config.lib ? stylix;
|
||||
# KeePassXC ApplicationTheme: "auto" follows the desktop, else light/dark.
|
||||
# config.stylix.polarity is "dark"/"light", which are valid theme values.
|
||||
appTheme = if hasStylix then config.stylix.polarity else "auto";
|
||||
in
|
||||
{
|
||||
programs.keepassxc = {
|
||||
enable = true;
|
||||
autostart = true;
|
||||
settings = {
|
||||
General = {
|
||||
ConfigVersion = 2;
|
||||
# Single running instance; reopen prior databases on launch.
|
||||
SingleInstance = true;
|
||||
RememberLastDatabases = true;
|
||||
RememberLastKeyFiles = true;
|
||||
OpenPreviousDatabasesOnStartup = true;
|
||||
AutoSaveAfterEveryChange = true;
|
||||
AutoSaveOnExit = true;
|
||||
AutoReloadOnChange = true;
|
||||
# No update nag (NixOS owns the package).
|
||||
UpdateCheckMessageShown = true;
|
||||
};
|
||||
|
||||
GUI = {
|
||||
ApplicationTheme = appTheme;
|
||||
CompactMode = true;
|
||||
CheckForUpdates = false;
|
||||
# Close button minimises instead of quitting -> process stays alive so
|
||||
# Firefox's KeePassXC-Browser connection survives. Quit deliberately
|
||||
# with Ctrl+Q. MinimizeToTray hides the window on Wayland/Hyprland
|
||||
# (no visible tray icon without a tray host, which is fine).
|
||||
MinimizeOnClose = true;
|
||||
MinimizeToTray = true;
|
||||
ShowTrayIcon = true;
|
||||
};
|
||||
|
||||
Security = {
|
||||
ClearClipboard = true;
|
||||
ClearClipboardTimeout = 10;
|
||||
|
||||
LockDatabaseIdle = false;
|
||||
LockDatabaseIdleSeconds = 900;
|
||||
LockDatabaseScreenLock = true;
|
||||
LockDatabaseOnUserSwitch = true;
|
||||
|
||||
PasswordsHidden = true;
|
||||
HidePasswordPreviewPanel = true;
|
||||
};
|
||||
|
||||
Browser = {
|
||||
Enabled = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
7
home/programs/media/audacious/default.nix
Normal file
7
home/programs/media/audacious/default.nix
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{ pkgs, config, lib, ... }:
|
||||
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
audacious
|
||||
];
|
||||
}
|
||||
21
home/programs/media/default.nix
Normal file
21
home/programs/media/default.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{ pkgs, ... }:
|
||||
# media - control and enjoy audio/video
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
# audio control
|
||||
pulsemixer
|
||||
pwvucontrol
|
||||
crosspipe # helvum removed in nixpkgs 26.05 (unmaintained); crosspipe is the successor
|
||||
|
||||
# audio
|
||||
amberol
|
||||
librepods
|
||||
|
||||
# images
|
||||
loupe
|
||||
|
||||
# videos
|
||||
celluloid
|
||||
grayjay
|
||||
];
|
||||
}
|
||||
142
home/programs/media/newsboat/default.nix
Normal file
142
home/programs/media/newsboat/default.nix
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
# newsboat — terminal RSS, vim-keyed. Feeds migrated from Read You
|
||||
# (Read-You OPML export; tags = old reader categories). The filelist.io feed
|
||||
# was deliberately left out: its URL embeds a private passkey — secret, not
|
||||
# for git. Re-add via sops if needed.
|
||||
let
|
||||
# bookmark-cmd target: save the full article as a markdown note in the Keep
|
||||
# vault. newsboat calls: <cmd> <url> <title> <description> <feed-title>
|
||||
newsboat-clip = pkgs.writeShellApplication {
|
||||
name = "newsboat-clip";
|
||||
runtimeInputs = [ pkgs.python3Packages.trafilatura pkgs.coreutils ];
|
||||
text = ''
|
||||
url=''${1:-}
|
||||
title=''${2:-untitled}
|
||||
desc=''${3:-}
|
||||
feed=''${4:-}
|
||||
|
||||
dir="$HOME/Documents/Keep/5 - Main Notes"
|
||||
mkdir -p "$dir"
|
||||
|
||||
# filename: article title, path-hostile chars -> _, whitespace collapsed
|
||||
safe=$(printf '%s' "$title" | tr '/\\:*?"<>|' '_________' | tr -s ' ' | sed 's/^ *//; s/ *$//')
|
||||
[ -z "$safe" ] && safe=untitled
|
||||
file="$dir/$safe.md"
|
||||
n=1
|
||||
while [ -e "$file" ]; do
|
||||
file="$dir/$safe ($n).md"
|
||||
n=$((n + 1))
|
||||
done
|
||||
|
||||
# full text via trafilatura; fall back to feed description on failure
|
||||
body=$(trafilatura -u "$url" --markdown 2>/dev/null || true)
|
||||
[ -z "$body" ] && body="$desc"
|
||||
|
||||
# escape double-quotes for YAML frontmatter values
|
||||
esc() { printf '%s' "$1" | sed 's/"/\\"/g'; }
|
||||
today=$(date +%Y-%m-%d)
|
||||
|
||||
{
|
||||
printf -- '---\n'
|
||||
printf 'title: "%s"\n' "$(esc "$title")"
|
||||
printf 'source: "%s"\n' "$(esc "$url")"
|
||||
printf 'feed: "%s"\n' "$(esc "$feed")"
|
||||
printf 'date: %s\n' "$today"
|
||||
printf 'topic: ""\n'
|
||||
printf 'tags: [clipping]\n'
|
||||
printf -- '---\n\n'
|
||||
printf '%s\n' "$body"
|
||||
} > "$file"
|
||||
'';
|
||||
};
|
||||
|
||||
# notify-program target: notify-format string arrives as args
|
||||
newsboat-notify = pkgs.writeShellApplication {
|
||||
name = "newsboat-notify";
|
||||
runtimeInputs = [ pkgs.libnotify ];
|
||||
text = ''
|
||||
notify-send --app-name=newsboat "Newsboat" "$*"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
home.packages = [
|
||||
pkgs.newsboat
|
||||
pkgs.python3Packages.trafilatura
|
||||
newsboat-clip
|
||||
newsboat-notify
|
||||
];
|
||||
|
||||
# plain files instead of programs.newsboat so the urls list stays a
|
||||
# readable one-line-per-feed file rather than 84 Nix attrsets
|
||||
xdg.configFile."newsboat/urls".source = ./urls;
|
||||
|
||||
xdg.configFile."newsboat/config".text = ''
|
||||
# behaviour
|
||||
auto-reload yes
|
||||
reload-time 30
|
||||
reload-threads 8
|
||||
show-read-feeds no
|
||||
cleanup-on-quit yes
|
||||
datetime-format "%Y-%m-%d"
|
||||
text-width 100
|
||||
|
||||
# killfile — hide obvious junk; extend as needed
|
||||
ignore-mode "display"
|
||||
ignore-article "*" "title =~ \"[Ss]ponsored|[Aa]dvertisement\""
|
||||
|
||||
# triage
|
||||
prepopulate-query-feeds yes
|
||||
# newsboat 2.41 inverts the date direction: date-asc = NEWEST first
|
||||
# (verified empirically; the man page says the opposite and is wrong here)
|
||||
article-sort-order date-asc
|
||||
articlelist-format "%4i %f %D %?T?|%-17T| ?%t"
|
||||
feedlist-format "%4i %n %11u %t"
|
||||
highlight article "[Tt]aiwan|[Kk]ubernetes|[Nn]ix([Oo][Ss])?|[Bb]ogdan|[Bb]uduroiu" yellow default bold
|
||||
|
||||
# capture → Obsidian (Keep vault, 5 - Main Notes/)
|
||||
bookmark-cmd "${newsboat-clip}/bin/newsboat-clip"
|
||||
bookmark-autopilot yes
|
||||
|
||||
# notifications (global — fires for any new articles)
|
||||
notify-program "${newsboat-notify}/bin/newsboat-notify"
|
||||
notify-format "newsboat: %d new (%n unread)"
|
||||
notify-always no
|
||||
|
||||
# vim-style navigation
|
||||
bind-key j down
|
||||
bind-key k up
|
||||
bind-key J next-feed articlelist
|
||||
bind-key K prev-feed articlelist
|
||||
bind-key g home
|
||||
bind-key G end
|
||||
bind-key h quit articlelist
|
||||
bind-key l open feedlist
|
||||
bind-key F edit-flags
|
||||
bind-key b bookmark
|
||||
bind-key A mark-feed-read
|
||||
bind-key C mark-all-feeds-read
|
||||
bind-key U show-urls
|
||||
bind-key R reload-all
|
||||
bind-key s sort
|
||||
bind-key S rev-sort
|
||||
|
||||
# open links
|
||||
browser "uwsm app -- xdg-open %u"
|
||||
macro o set browser "uwsm app -- firefox --new-tab %u"; open-in-browser ; set browser "uwsm app -- xdg-open %u"
|
||||
macro v set browser "uwsm app -- celluloid %u"; open-in-browser ; set browser "uwsm app -- xdg-open %u"
|
||||
macro y set browser "echo %u | wl-copy"; open-in-browser ; set browser "uwsm app -- xdg-open %u"
|
||||
# full-text on demand — fetch the real article into vim even on truncated feeds
|
||||
macro f set browser "trafilatura -u %u --markdown --images 2>/dev/null | nvim -R -c 'set filetype=markdown' -"; open-in-browser ; set browser "uwsm app -- xdg-open %u"
|
||||
|
||||
# colors — palette slots inherit the stylix-themed terminal
|
||||
color background default default
|
||||
color listnormal default default
|
||||
color listnormal_unread default default bold
|
||||
color listfocus color0 color4
|
||||
color listfocus_unread color0 color4 bold
|
||||
color info color4 default bold
|
||||
color article default default
|
||||
'';
|
||||
}
|
||||
70
home/programs/media/newsboat/urls
Normal file
70
home/programs/media/newsboat/urls
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"query:★ Unread:unread = \"yes\""
|
||||
"query:⚑ Read-later:flags # \"r\""
|
||||
"query:Eng + AI:(tags # \"engineering\" or tags # \"ai\") and unread = \"yes\""
|
||||
"query:Taiwan watch:title =~ \"[Tt]aiwan|[Cc]hina|[Pp][Ll][Aa]\""
|
||||
https://github.com/ReadYouApp/ReadYou/releases.atom default "~ReadYou Releases"
|
||||
https://www.aluxian.com/feed/ friends "~Alexandru ROSIANU"
|
||||
https://blog.calebjay.com/index.xml friends "~Caleb's Blog"
|
||||
http://danluu.com/atom.xml engineering "~Dan Luu"
|
||||
https://domk.website/blog/feed.xml engineering "~Domk.website Blog"
|
||||
https://jvns.ca/atom.xml engineering "~Julia Evans"
|
||||
https://www.theregister.com/software/devops/headlines.atom engineering "~The Register - Software: Devops"
|
||||
https://www.theregister.com/software/virtualization/headlines.atom engineering "~The Register - Software: Virtualization"
|
||||
https://www.saxrag.com/feed.xml engineering "~saxrag"
|
||||
https://sxyz.blog/index.xml engineering "~Sxyazi’s blog"
|
||||
https://buduroiu.com/links/index.xml engineering "~links on Bogdan Buduroiu"
|
||||
https://pagedout.institute/rss.xml engineering "~Paged Out! Magazine"
|
||||
https://minimaxir.com/post/index.xml engineering "~Posts on Max Woolf's Blog"
|
||||
https://www.youtube.com/feeds/videos.xml?channel_id=UCxAS_aK7sS2x_bqnlJHDSHw food "~America's Test Kitchen"
|
||||
https://cafeandcowork.com/feed.xml food "~Cafe and Cowork"
|
||||
https://pipedapi.kavin.rocks/feed/unauthenticated/rss?channels=UCn5TG-OvjnX9O1UC7fpyUhA food "~Hana Kitchen - 日式料理"
|
||||
https://www.bingingwithbabish.com/recipes?format=RSS food "~Recipes - Binging With Babish"
|
||||
http://feeds.seriouseats.com/seriouseatsfeaturesvideos food "~Serious Eats"
|
||||
http://hunch.net/?feed=rss2 "machine learning" "~Machine Learning (Theory)"
|
||||
http://amti.csis.org/feed/ military "~Asia Maritime Transparency Initiative"
|
||||
https://xcancel.com/PLAOpsOSINT/rss military "~PLAOps / @PLAOpsOSINT"
|
||||
https://xcancel.com/tingtingliuTVBS/rss military "~Tingting Liu 劉亭廷 / @tingtingliuTVBS"
|
||||
https://warontherocks.com/feed/ military "~War on the Rocks"
|
||||
https://xcancel.com/MoNDefense/rss military "~國防部 Ministry of National Defense, R.O.C. 🇹🇼 / @MoNDefense"
|
||||
https://restofworld.org/feed/latest/ news "~Rest of World -"
|
||||
https://www.15minutecity.com/blog?format=rss news "~blog - 15-Minute City"
|
||||
https://www.anildash.com/feed.xml "opinion / independent" "~Anil Dash"
|
||||
https://howtodothingswithmemes.substack.com/feed "opinion / independent" "~How To Do Things With Memes"
|
||||
https://pluralistic.net/feed/ "opinion / independent" "~Pluralistic: Daily links from Cory Doctorow"
|
||||
https://etymology.substack.com/feed "opinion / independent" "~The Etymology Nerd"
|
||||
https://www.theindex.media/rss/ "opinion / independent" "~The Index."
|
||||
https://www.unpopularfront.news/feed "opinion / independent" "~Unpopular Front"
|
||||
https://www.joanwestenberg.com/rss/ "opinion / independent" "~Westenberg."
|
||||
https://geohot.github.io/blog/feed.xml "opinion / independent" "~the singularity is nearer"
|
||||
https://jkap.io/feed/ "opinion / independent" "~jae kaplan"
|
||||
https://www.wheresyoured.at/feed "opinion / independent" "~Ed Zitron's Where's Your Ed At"
|
||||
http://www.sciencedaily.com/newsfeed.xml science "~ScienceDaily"
|
||||
http://news.nationalgeographic.com/index.rss science "~National Geographic News"
|
||||
https://nautil.us/feed/ science "~Nautilus"
|
||||
https://www.theregister.com/on_prem/personal_tech/headlines.atom tech "~The Register - On-Prem: Personal Tech"
|
||||
https://www.theregister.com/on_prem/storage/headlines.atom tech "~The Register - On-Prem: Storage"
|
||||
https://www.404media.co/rss/ tech "~404 Media"
|
||||
https://buduroiu.com/index.xml tech "~Bogdan Buduroiu"
|
||||
http://info.crunchbase.com/feed/ tech "~Crunchbase"
|
||||
http://www.darkreading.com/rss/all.xml tech "~Dark Reading"
|
||||
https://www.theverge.com/authors/kylie-robison/rss tech "~Kylie Robison | The Verge"
|
||||
https://github.com/arkenfox/user.js/releases.atom tech "~Release notes from user.js"
|
||||
https://tante.cc/feed/ tech "~Smashing Frames"
|
||||
https://www.theepochtimes.com/c-tech/feed/ tech "~Tech News"
|
||||
https://techwontsave.us/feed.xml tech "~Tech Won’t Save Us"
|
||||
https://www.theregister.com/security/headlines.atom tech "~The Register – Security"
|
||||
https://www.bleepingcomputer.com/feed/ tech "~BleepingComputer"
|
||||
http://blog.project2049.net/feeds/posts/default 台灣新聞 "~Project 2049 Institute"
|
||||
http://blog.readmoo.com/feed/ 台灣新聞 "~Readmoo閱讀最前線"
|
||||
https://www.trendingonweibo.com/feed.xml 台灣新聞 "~Trending on Weibo | Developing stories from China"
|
||||
https://feeds.feedburner.com/rsscna/mainland 台灣新聞 "~中央社即時新聞 兩岸透視"
|
||||
https://feeds.feedburner.com/rsscna/politics 台灣新聞 "~中央社即時新聞 國內政治"
|
||||
https://papers.takara.ai/api/feed ai "~Takara TLDR - Daily AI Papers"
|
||||
https://chinarxiv.org/feed/atom.xml?category=ai_cs ai "~ChinaRxiv: in ai_cs"
|
||||
https://rss.arxiv.org/atom/cs.ai ai "~cs.AI updates on arXiv.org"
|
||||
https://axisofordinary.substack.com/feed ai "~Axis of Ordinary"
|
||||
https://alexzhang13.github.io/feed.xml ai "~Alex Zhang"
|
||||
https://novaramedia.com/feed/ "opinion / independent" "~Novara Media"
|
||||
https://www.youtube.com/feeds/videos.xml?channel_id=UCOzMAa6IhV6uwYQATYG_2kg "opinion / independent" "~Novara Media (Youtube)"
|
||||
https://simonwillison.net/atom/entries/ ai "~Simon Willison (entries)"
|
||||
https://uruky.com/rss.xml tech "~Uruky"
|
||||
16
home/programs/messaging.nix
Normal file
16
home/programs/messaging.nix
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{ pkgs, inputs, ... }:
|
||||
|
||||
let
|
||||
pkgs-unstable = import inputs.nixpkgs-unstable {
|
||||
inherit (pkgs.stdenv.hostPlatform) system;
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
in
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
pkgs-unstable.telegram-desktop
|
||||
discord-ptb
|
||||
signal-desktop
|
||||
element-desktop
|
||||
];
|
||||
}
|
||||
22
home/programs/office/default.nix
Normal file
22
home/programs/office/default.nix
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./thunar.nix
|
||||
./zathura.nix
|
||||
./obsidian
|
||||
./thunderbird.nix
|
||||
];
|
||||
|
||||
home.packages = with pkgs; [
|
||||
invoice
|
||||
libreoffice
|
||||
onlyoffice-desktopeditors
|
||||
gimp
|
||||
pinta
|
||||
slack
|
||||
sheets
|
||||
zulip-term
|
||||
xournalpp
|
||||
];
|
||||
}
|
||||
262
home/programs/office/obsidian/default.nix
Normal file
262
home/programs/office/obsidian/default.nix
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
{ config
|
||||
, lib
|
||||
, pkgs
|
||||
, ...
|
||||
}:
|
||||
|
||||
let
|
||||
# Shared app.json defaults. The obsidian module merges `settings` per
|
||||
# top-level key (app/appearance/...), NOT deeply — a vault that sets
|
||||
# `settings.app` replaces this whole block, so reuse it explicitly.
|
||||
appDefaults = {
|
||||
defaultViewMode = "preview";
|
||||
tabSize = 2;
|
||||
vimMode = true;
|
||||
focusNewTab = true;
|
||||
propertiesInDocument = "visible";
|
||||
foldHeading = true;
|
||||
foldIndent = true;
|
||||
rightToLeft = false;
|
||||
spellcheck = true;
|
||||
autoPairBrackets = true;
|
||||
autoPairMarkdown = true;
|
||||
smartIndentList = true;
|
||||
useTab = true;
|
||||
autoConvertHtml = true;
|
||||
promptDelete = false;
|
||||
trashOption = "local";
|
||||
alwaysUpdateLinks = false;
|
||||
newFileLocation = "current";
|
||||
newLinkFormat = "shortest";
|
||||
useMarkdownLinks = false;
|
||||
showUnsupportedFiles = true;
|
||||
};
|
||||
|
||||
website = "${config.home.homeDirectory}/development/bruvduroiu/website";
|
||||
blogsFolder = "${config.home.homeDirectory}/Documents/Keep/4 - Blogs";
|
||||
|
||||
# Single action: mirror the vault's `4 - Blogs` folder into the Hugo repo's content/.
|
||||
# Everything else (build, commit, push, local preview) is handled manually outside
|
||||
# Obsidian. rsync --delete gives true mirror semantics (deletions propagate); cp -a
|
||||
# would leave ghosts of deleted posts behind.
|
||||
blog-sync = pkgs.writeShellApplication {
|
||||
name = "blog-sync";
|
||||
runtimeInputs = with pkgs; [ rsync ];
|
||||
text = ''
|
||||
vault="${blogsFolder}"
|
||||
repo="${website}"
|
||||
|
||||
# Safety: refuse to mirror an unseeded vault — rsync --delete would otherwise
|
||||
# wipe every post in the repo.
|
||||
if [ ! -d "$vault/blog" ] || [ ! -f "$vault/_index.md" ]; then
|
||||
echo "blog-sync: '4 - Blogs' looks unseeded (missing blog/ or _index.md); aborting." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rsync -a --delete --exclude='.obsidian' "$vault/" "$repo/content/"
|
||||
echo "blog-sync: mirrored 4 - Blogs -> $repo/content"
|
||||
'';
|
||||
};
|
||||
|
||||
# One Shell-commands `data.json` command entry. Mirrors the plugin's
|
||||
# newShellCommandConfiguration() exactly — the module writes data.json as a
|
||||
# read-only store symlink, and Shell commands uses the loaded file as-is (it only
|
||||
# merges defaults to backfill `debug`/`settings_version`), so every field a current
|
||||
# install expects must be present or command registration can throw.
|
||||
mkShellCommand = id: alias: command: {
|
||||
inherit id alias;
|
||||
platform_specific_commands.default = command;
|
||||
shells = { };
|
||||
icon = null;
|
||||
confirm_execution = false;
|
||||
ignore_error_codes = [ ];
|
||||
input_contents.stdin = null;
|
||||
output_handlers = {
|
||||
stdout = { handler = "ignore"; convert_ansi_code = true; };
|
||||
stderr = { handler = "notification"; convert_ansi_code = true; };
|
||||
};
|
||||
output_wrappers = { stdout = null; stderr = null; };
|
||||
output_channel_order = "stdout-first";
|
||||
output_handling_mode = "buffered";
|
||||
execution_notification_mode = null;
|
||||
events = { };
|
||||
debounce = null;
|
||||
command_palette_availability = "enabled";
|
||||
preactions = [ ];
|
||||
variable_default_values = { };
|
||||
};
|
||||
|
||||
# Shell commands' built-in shells hardcode /bin/bash, /bin/dash, /bin/zsh in
|
||||
# getBinaryPath() and spawn THAT (the configured shell string only drives matching) —
|
||||
# none of those exist on NixOS, which provides only /bin/sh. So a built-in shell is
|
||||
# unusable here by construction (an unset shell falls back to $SHELL=fish, which the
|
||||
# plugin also doesn't support). Define a custom shell pinned to /bin/sh instead.
|
||||
# Mirrors CustomShellModel.getDefaultConfiguration(); our command is an absolute-path
|
||||
# binary, so a POSIX sh is all that is needed.
|
||||
nixShellId = "nixos-binsh";
|
||||
nixShell = {
|
||||
id = nixShellId;
|
||||
name = "NixOS /bin/sh";
|
||||
description = "POSIX sh via /bin/sh — built-in shells hardcode nonexistent /bin/bash and /bin/dash on NixOS.";
|
||||
binary_path = "/bin/sh";
|
||||
shell_arguments = [ "-c" "{{shell_command_content}}" ];
|
||||
host_platform = "linux";
|
||||
host_platform_configurations = { };
|
||||
shell_platform = null;
|
||||
escaper = "UnixShell";
|
||||
path_translator = null;
|
||||
shell_command_wrapper = null;
|
||||
shell_command_test = null;
|
||||
};
|
||||
|
||||
# Full SC_MainSettings default object (settings_version pinned to the packaged
|
||||
# plugin version, so no migration runs against the read-only file) + our command.
|
||||
shellcommandsSettings = {
|
||||
settings_version = "0.23.0";
|
||||
debug = false;
|
||||
obsidian_command_palette_prefix = "Execute: ";
|
||||
preview_variables_in_command_palette = true;
|
||||
show_autocomplete_menu = true;
|
||||
working_directory = "";
|
||||
default_shells = { linux = nixShellId; };
|
||||
environment_variable_path_augmentations = { };
|
||||
show_installation_warnings = true;
|
||||
error_message_duration = 20;
|
||||
notification_message_duration = 10;
|
||||
execution_notification_mode = "disabled";
|
||||
output_channel_clipboard_also_outputs_to_notification = true;
|
||||
output_channel_notification_decorates_output = true;
|
||||
enable_events = true;
|
||||
approve_modals_by_pressing_enter_key = true;
|
||||
command_palette.re_execute_last_shell_command = {
|
||||
enabled = true;
|
||||
prefix = "Re-execute: ";
|
||||
};
|
||||
max_visible_lines_in_shell_command_fields = false;
|
||||
shell_commands = [
|
||||
(mkShellCommand "blog-sync" "Blog: sync vault -> repo content" "${blog-sync}/bin/blog-sync")
|
||||
];
|
||||
prompts = [ ];
|
||||
builtin_variables = { };
|
||||
custom_variables = [ ];
|
||||
custom_variables_notify_changes_via = {
|
||||
obsidian_uri = true;
|
||||
output_assignment = true;
|
||||
};
|
||||
custom_shells = [ nixShell ];
|
||||
output_wrappers = [ ];
|
||||
};
|
||||
in
|
||||
{
|
||||
# Scripts, templates, and bases are stored here as version-controlled backup.
|
||||
# They live in the vault directly (synced via Syncthing).
|
||||
|
||||
# blog-sync is also exposed on PATH for use outside Obsidian.
|
||||
home.packages = [ blog-sync ];
|
||||
|
||||
programs.obsidian = {
|
||||
enable = true;
|
||||
package = pkgs.obsidian;
|
||||
vaults = {
|
||||
Keep = {
|
||||
enable = true;
|
||||
target = "Documents/Keep";
|
||||
settings = {
|
||||
app = appDefaults // {
|
||||
attachmentFolderPath = "9 - Attachments";
|
||||
# All new notes land in 5 - Main Notes. Blogs start there too but the Blog Post
|
||||
# template immediately tp.file.move()s them into 4 - Blogs/blog/<slug>/.
|
||||
newFileLocation = "folder";
|
||||
newFileFolderPath = "5 - Main Notes";
|
||||
};
|
||||
# Declarative custom hotkeys (.obsidian/hotkeys.json). Shell-commands command
|
||||
# ids are `obsidian-shellcommands:shell-command-<id>`; Templater registers each
|
||||
# enabled template-hotkey as `templater-obsidian:<vault-relative path>`.
|
||||
hotkeys = {
|
||||
"obsidian-shellcommands:shell-command-blog-sync" = [
|
||||
{ modifiers = [ "Mod" "Shift" ]; key = "P"; }
|
||||
];
|
||||
"templater-obsidian:9 - Templates/Blog Post.md" = [
|
||||
{ modifiers = [ "Mod" "Shift" ]; key = "B"; }
|
||||
];
|
||||
};
|
||||
corePlugins = [
|
||||
"backlink"
|
||||
"bases"
|
||||
"bookmarks"
|
||||
"canvas"
|
||||
"command-palette"
|
||||
"editor-status"
|
||||
"file-explorer"
|
||||
"file-recovery"
|
||||
"global-search"
|
||||
"graph"
|
||||
"note-composer"
|
||||
"outgoing-link"
|
||||
"outline"
|
||||
"page-preview"
|
||||
"slash-command"
|
||||
"switcher"
|
||||
"tag-pane"
|
||||
"templates"
|
||||
"word-count"
|
||||
];
|
||||
communityPlugins = [
|
||||
{
|
||||
pkg = pkgs.callPackage ./plugins/templater.nix { };
|
||||
settings = {
|
||||
trigger_on_file_creation = true;
|
||||
auto_jump_to_cursor = true;
|
||||
enable_system_commands = true;
|
||||
syntax_highlighting = true;
|
||||
shell_path = "fish";
|
||||
command_palette = true;
|
||||
templates_folder = "9 - Templates";
|
||||
user_scripts_folder = "9 - Scripts";
|
||||
# Register the Blog Post template as a hotkeyable command.
|
||||
enabled_templates_hotkeys = [ "9 - Templates/Blog Post.md" ];
|
||||
};
|
||||
}
|
||||
{
|
||||
pkg = pkgs.callPackage ./plugins/dataview.nix { };
|
||||
settings = {
|
||||
enableDataviewJs = true;
|
||||
enableInlineDataviewJs = true;
|
||||
taskCompletionUseEmojiShorthand = true;
|
||||
taskCompletionTracking = true;
|
||||
defaultDateFormat = "dd/MM/yyyy";
|
||||
defaultDateTimeFormat = "HH:mm - dd/MM/yyyy";
|
||||
};
|
||||
}
|
||||
{
|
||||
pkg = pkgs.callPackage ./plugins/maps.nix { };
|
||||
settings = {
|
||||
tileSets = [
|
||||
{
|
||||
id = "protomaps";
|
||||
name = "Protomaps";
|
||||
lightTiles = "https://tiles.openfreemap.org/styles/liberty";
|
||||
darkTiles = "https://tiles.openfreemap.org/styles/dark";
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
{
|
||||
# Single rsync command (see blog-sync above).
|
||||
pkg = pkgs.callPackage ./plugins/shellcommands.nix { };
|
||||
settings = shellcommandsSettings;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
defaultSettings = {
|
||||
appearance = {
|
||||
baseFontSize = lib.mkForce 16;
|
||||
};
|
||||
|
||||
app = appDefaults;
|
||||
};
|
||||
};
|
||||
}
|
||||
43
home/programs/office/obsidian/plugins/dataview.nix
Normal file
43
home/programs/office/obsidian/plugins/dataview.nix
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{ pkgs
|
||||
, version ? "0.5.70"
|
||||
}:
|
||||
|
||||
pkgs.stdenv.mkDerivation rec {
|
||||
pname = "obsidian-dataview";
|
||||
inherit version;
|
||||
|
||||
srcs = [
|
||||
(pkgs.fetchurl {
|
||||
url = "https://github.com/blacksmithgu/obsidian-dataview/releases/download/${version}/manifest.json";
|
||||
sha256 = "sha256-kjXbRxEtqBuFWRx57LmuJXTl5yIHBW6XZHL5BhYoYYU=";
|
||||
name = "manifest.json";
|
||||
})
|
||||
(pkgs.fetchurl {
|
||||
url = "https://github.com/blacksmithgu/obsidian-dataview/releases/download/${version}/main.js";
|
||||
sha256 = "sha256-a7HPcBCvrYMOc1dfyg4r+9MnnFYuPZ0k8tL0UWHrfQA=";
|
||||
name = "main.js";
|
||||
})
|
||||
(pkgs.fetchurl {
|
||||
url = "https://github.com/blacksmithgu/obsidian-dataview/releases/download/${version}/styles.css";
|
||||
sha256 = "sha256-MwbdkDLgD5ibpyM6N/0lW8TT9DQM7mYXYulS8/aqHek=";
|
||||
name = "styles.css";
|
||||
})
|
||||
];
|
||||
|
||||
dontBuild = true;
|
||||
dontStrip = true;
|
||||
|
||||
unpackPhase = ''
|
||||
for src in $srcs; do
|
||||
cp $src ./
|
||||
done
|
||||
find . -name "*manifest.json" -exec mv {} manifest.json \;
|
||||
find . -name "*main.js" -exec mv {} main.js \;
|
||||
find . -name "*styles.css" -exec mv {} styles.css \;
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp manifest.json main.js styles.css $out/
|
||||
'';
|
||||
}
|
||||
26
home/programs/office/obsidian/plugins/maps.nix
Normal file
26
home/programs/office/obsidian/plugins/maps.nix
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{ pkgs, ... }:
|
||||
pkgs.buildNpmPackage rec {
|
||||
pname = "obsidian.plugins.maps";
|
||||
version = "0.1.6";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "obsidianmd";
|
||||
repo = "obsidian-maps";
|
||||
rev = version;
|
||||
hash = "sha256-KYWgPu08tV4vMpTZt4ygcenel1JBsXWdh7VFl9vVuU8=";
|
||||
};
|
||||
|
||||
# obsidian-maps ships package-lock.json (npm), not pnpm-lock.yaml.
|
||||
npmDepsHash = "sha256-KGTRLJkbJU8BXED6ziEFlSTLXNHPYIE6pWoWxiQXSMU=";
|
||||
|
||||
nativeBuildInputs = with pkgs; [ nodejs ];
|
||||
|
||||
dontNpmPrune = true; # hangs forever on both Linux/darwin
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
mkdir -p $out
|
||||
cp manifest.json main.js styles.css $out/
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
||||
35
home/programs/office/obsidian/plugins/shellcommands.nix
Normal file
35
home/programs/office/obsidian/plugins/shellcommands.nix
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{ pkgs, ... }:
|
||||
# Unlike templater.nix / dataview.nix (which build from source), Shell commands is
|
||||
# fetched as its prebuilt release artifacts. Obsidian plugins are distributed as just
|
||||
# three files (main.js / manifest.json / styles.css), so fetching the release avoids an
|
||||
# npm/rollup build and the per-release npmDeps-hash churn that comes with it.
|
||||
let
|
||||
version = "0.23.0";
|
||||
base = "https://github.com/Taitava/obsidian-shellcommands/releases/download/${version}";
|
||||
|
||||
mainJs = pkgs.fetchurl {
|
||||
url = "${base}/main.js";
|
||||
hash = "sha256-DF4kepHJbAr18+MzukO+QoVGlWbIGdWbfeEprTPRTYo=";
|
||||
};
|
||||
manifest = pkgs.fetchurl {
|
||||
url = "${base}/manifest.json";
|
||||
hash = "sha256-IK6Mz6iXICfTW0RX0htquGrryOOx3uXro02tGeRqJGI=";
|
||||
};
|
||||
styles = pkgs.fetchurl {
|
||||
url = "${base}/styles.css";
|
||||
hash = "sha256-O9g4DlqlP8RH6mpMFN37lBmPGm0APm5fmUtFnPloTFM=";
|
||||
};
|
||||
in
|
||||
pkgs.runCommandLocal "obsidian.plugins.shellcommands-${version}"
|
||||
{
|
||||
inherit version;
|
||||
# The obsidian HM module reads `pkg.manifestId` (falling back to manifest.json's id)
|
||||
# to name the plugin's directory under .obsidian/plugins/.
|
||||
passthru.manifestId = "obsidian-shellcommands";
|
||||
}
|
||||
''
|
||||
mkdir -p $out
|
||||
cp ${mainJs} $out/main.js
|
||||
cp ${manifest} $out/manifest.json
|
||||
cp ${styles} $out/styles.css
|
||||
''
|
||||
32
home/programs/office/obsidian/plugins/templater.nix
Normal file
32
home/programs/office/obsidian/plugins/templater.nix
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{ pkgs, ... }:
|
||||
pkgs.buildNpmPackage rec {
|
||||
pname = "obsidian.plugins.templater";
|
||||
version = "2.16.2";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "SilentVoid13";
|
||||
repo = "Templater";
|
||||
rev = version;
|
||||
hash = "sha256-jFELX0gW0aLW/eY685hMBgrbECNMqg0zbumpF2q0HFk=";
|
||||
};
|
||||
|
||||
pnpmDeps = pkgs.fetchPnpmDeps {
|
||||
inherit pname version src;
|
||||
fetcherVersion = 3;
|
||||
hash = "sha256-xI6Hz6UPgz+4B6w3cxVsTuYpgPwwpSkgWgdadPeR8XM=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [ nodejs pnpm ]; # pnpmConfigHook needs pnpm on PATH (pnpm.configHook used to add it)
|
||||
|
||||
npmConfigHook = pkgs.pnpmConfigHook;
|
||||
npmDeps = pnpmDeps;
|
||||
|
||||
dontNpmPrune = true; # hangs forever on both Linux/darwin
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp ./manifest.json $out/manifest.json
|
||||
cp ./main.js $out/main.js
|
||||
cp ./styles.css $out/styles.css
|
||||
'';
|
||||
}
|
||||
53
home/programs/office/obsidian/scripts/moedict_fetch.js
Normal file
53
home/programs/office/obsidian/scripts/moedict_fetch.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* Templater user script: Fetch and cache moedict.tw data
|
||||
* Returns an object with { meaning, zhuyin } - call once per template
|
||||
* Usage:
|
||||
* <%* const moe = await tp.user.moedict_fetch(tp.file.title) %>
|
||||
* Then use: <% moe.meaning %> and <% moe.zhuyin %>
|
||||
*/
|
||||
async function moedict_fetch(word) {
|
||||
const result = {
|
||||
meaning: "TODO add meaning",
|
||||
zhuyin: "TODO add zhuyin",
|
||||
};
|
||||
|
||||
if (!word || word.trim() === "") {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://www.moedict.tw/a/${encodeURIComponent(word)}.json`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Extract zhuyin from first heteronym
|
||||
if (data.h?.length > 0 && data.h[0].b) {
|
||||
result.zhuyin = data.h[0].b.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
// Extract English meaning
|
||||
if (data.translation?.English?.length > 0) {
|
||||
const meanings = data.translation.English.filter(
|
||||
(m) => !m.startsWith("CL:"),
|
||||
).join("; ");
|
||||
if (meanings) {
|
||||
result.meaning = meanings;
|
||||
}
|
||||
} else if (data.English) {
|
||||
result.meaning = data.English;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("moedict_fetch error:", error);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = moedict_fetch;
|
||||
37
home/programs/office/obsidian/templates/blog-post.md
Normal file
37
home/programs/office/obsidian/templates/blog-post.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<%*
|
||||
// Blog Post — Hugo page-bundle scaffold.
|
||||
// Backup of the vault file `9 - Templates/Blog Post.md`. Templates live in the vault
|
||||
// (Syncthing-synced); this copy is version-controlled, like scripts/moedict_fetch.js.
|
||||
const title = await tp.system.prompt("Post title");
|
||||
const slug = title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/(^-|-$)/g, "");
|
||||
// Move into a page bundle: 4 - Blogs/blog/<slug>/index.md (co-located images work).
|
||||
await tp.file.rename("index");
|
||||
await tp.file.move("/4 - Blogs/blog/" + slug + "/index");
|
||||
-%>
|
||||
---
|
||||
title: "<% title %>"
|
||||
date: <% tp.date.now("YYYY-MM-DDTHH:mm:ssZ") %>
|
||||
topics: []
|
||||
draft: true
|
||||
---
|
||||
|
||||
<!--
|
||||
Optional front matter (uncomment as needed):
|
||||
categories: []
|
||||
series: "" # name of the series this post belongs to
|
||||
tools: [] # technologies mentioned (taxonomy)
|
||||
layout: "default" # default | tutorial | longform
|
||||
description: # auto-generated from first paragraph if unset
|
||||
image: # auto-detects cover.* in the page bundle
|
||||
-->
|
||||
|
||||
## Introduction
|
||||
|
||||
|
||||
## Main Content
|
||||
|
||||
|
||||
## Conclusion
|
||||
42
home/programs/office/thunar.nix
Normal file
42
home/programs/office/thunar.nix
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
# xfce.thunar* moved to top-level in nixpkgs 26.05
|
||||
thunar
|
||||
thunar-volman
|
||||
thunar-vcs-plugin
|
||||
thunar-archive-plugin
|
||||
thunar-media-tags-plugin
|
||||
tumbler
|
||||
];
|
||||
xdg.configFile."Thunar/uca.xml".text = ''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<actions>
|
||||
<action>
|
||||
<icon>utilities-x-terminal</icon>
|
||||
<name>Open Terminal Here</name>
|
||||
<submenu></submenu>
|
||||
<unique-id>1747642747798278-1</unique-id>
|
||||
<command>ghostty --working-directory %f</command>
|
||||
<description>Open terminal here</description>
|
||||
<range></range>
|
||||
<patterns>*</patterns>
|
||||
<startup-notify/>
|
||||
<directories/>
|
||||
</action>
|
||||
<action>
|
||||
<icon>nvim</icon>
|
||||
<name>Open file with neovim</name>
|
||||
<submenu></submenu>
|
||||
<unique-id>1747716628944583-1</unique-id>
|
||||
<command>ghostty --working-directory %d nvim %F</command>
|
||||
<description>Open file with nvim </description>
|
||||
<range></range>
|
||||
<patterns>*</patterns>
|
||||
<startup-notify/>
|
||||
<directories/>
|
||||
</action>
|
||||
</actions>
|
||||
'';
|
||||
}
|
||||
19
home/programs/office/thunderbird.nix
Normal file
19
home/programs/office/thunderbird.nix
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
tbkeys = pkgs.fetchFirefoxAddon {
|
||||
name = "tbkeys";
|
||||
url = "https://github.com/wshanks/tbkeys/releases/download/v2.4.3/tbkeys.xpi";
|
||||
hash = "sha256-2e+T5Nr5kc2s8EykFzWKaJZ2jPUDHh9Cqn4hCuDCLaM=";
|
||||
};
|
||||
in
|
||||
{
|
||||
programs.thunderbird = {
|
||||
enable = true;
|
||||
|
||||
profiles.default = {
|
||||
isDefault = true;
|
||||
extensions = [ tbkeys ];
|
||||
};
|
||||
};
|
||||
}
|
||||
21
home/programs/office/zathura.nix
Normal file
21
home/programs/office/zathura.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{ lib, ... }:
|
||||
|
||||
{
|
||||
programs.zathura = {
|
||||
enable = true;
|
||||
options = {
|
||||
recolor-lightcolor = lib.mkForce "rgba(0,0,0,0)";
|
||||
default-bg = lib.mkForce "rgba(0,0,0,0.7)";
|
||||
|
||||
selection-notification = true;
|
||||
|
||||
selection-clipboard = "clipboard";
|
||||
adjust-open = "best-fit";
|
||||
pages-per-row = "1";
|
||||
scroll-page-aware = "true";
|
||||
scroll-full-overlap = "0.01";
|
||||
scroll-step = "100";
|
||||
zoom-min = "10";
|
||||
};
|
||||
};
|
||||
}
|
||||
33
home/programs/wayland/default.nix
Normal file
33
home/programs/wayland/default.nix
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{ pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./walker.nix
|
||||
./waybar
|
||||
./hyprlock.nix
|
||||
./hypridle.nix
|
||||
./wf-recorder.nix
|
||||
];
|
||||
|
||||
home.packages = with pkgs; [
|
||||
# screenshot: grim captures, satty annotates (wayland-native — no
|
||||
# fractional-scale jank like flameshot's Qt overlay)
|
||||
grim
|
||||
slurp
|
||||
satty
|
||||
|
||||
wl-clipboard
|
||||
|
||||
# standalone bluetooth TUI (deliberately no waybar module / tray)
|
||||
bluetui
|
||||
];
|
||||
|
||||
home.sessionVariables = {
|
||||
QT_QPA_PLATFORM = "wayland";
|
||||
SDL_VIDEODRIVER = "wayland";
|
||||
XDG_SESSION_TYPE = "wayland";
|
||||
};
|
||||
|
||||
systemd.user.targets.tray.Unit.Requires = lib.mkForce ["graphical-session.target"];
|
||||
|
||||
}
|
||||
32
home/programs/wayland/hypridle.nix
Normal file
32
home/programs/wayland/hypridle.nix
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{ pkgs, lib, config, inputs, ... }: let
|
||||
lock = "${pkgs.systemd}/bin/loginctl lock-session";
|
||||
|
||||
timeout = 300;
|
||||
in
|
||||
{
|
||||
services.hypridle = {
|
||||
enable = true;
|
||||
|
||||
package = inputs.hypridle.packages.${pkgs.stdenv.hostPlatform.system}.hypridle;
|
||||
|
||||
settings = {
|
||||
general = {
|
||||
before_sleep_cmd = "loginctl lock-session";
|
||||
after_sleep_cmd = "hyprctl dispatch dpms on";
|
||||
lock_cmd = "pgrep hyprlock || ${lib.getExe config.programs.hyprlock.package}";
|
||||
};
|
||||
|
||||
listener = [
|
||||
{
|
||||
inherit timeout;
|
||||
|
||||
on-timeout = "hyprctl dispatch dpms off";
|
||||
on-resume = "hyprctl dispatch dpms on";
|
||||
}
|
||||
{
|
||||
on-timeout = lock;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
530
home/programs/wayland/hyprland.nix
Normal file
530
home/programs/wayland/hyprland.nix
Normal file
|
|
@ -0,0 +1,530 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
# Hyprland config generated natively by home-manager (configType = "lua").
|
||||
#
|
||||
# Replaces the hand-authored system/programs/hyprland/lua.nix: HM 26.05's
|
||||
# wayland.windowManager.hyprland lua backend renders `settings` to hl.<name>(...)
|
||||
# calls, with _var -> lua locals, _args -> multi-arg calls, and
|
||||
# lib.generators.mkLuaInline -> verbatim raw lua. This sidesteps the
|
||||
# nix-community/home-manager#9468 `$variable` bug that forced hand-authoring.
|
||||
#
|
||||
# Split: the NixOS module (system/programs/hyprland) keeps the UWSM session,
|
||||
# portals and helper scripts and emits NO config; HM owns ~/.config/hypr/hyprland.lua.
|
||||
#
|
||||
# framework13-first: monitors/device/per-ws-monitor + stylix live here for now;
|
||||
# generalised to a shared module + per-host overrides when phantom/iso migrate.
|
||||
|
||||
let
|
||||
inherit (lib.generators) mkLuaInline;
|
||||
|
||||
# stylix border colours at HM level (framework13 has stylix; fall back to white).
|
||||
hasStylix = config ? lib && config.lib ? stylix;
|
||||
c = lib.optionalAttrs hasStylix config.lib.stylix.colors;
|
||||
active = if hasStylix then "rgba(${c.base05}cc)" else "rgba(ffffffcc)";
|
||||
inactive = if hasStylix then "rgba(${c.base04}44)" else "rgba(ffffff44)";
|
||||
|
||||
# framework13 touchpad accel + scroll curve (was the hosts/framework13 luaPrelude).
|
||||
accelpoints = "0.000 0.391 1.375 3.422 7.000 12.578 20.625 31.609 46.000 64.266 86.875 114.297 146.000 185.453 230.125 281.484 340.000 406.141 480.375 563.172";
|
||||
|
||||
# bind helper: keys (raw lua expr) + dispatcher (raw lua expr) [+ opts attrs]
|
||||
b = keys: disp: { _args = [ (mkLuaInline keys) (mkLuaInline disp) ]; };
|
||||
bo = keys: disp: opts: { _args = [ (mkLuaInline keys) (mkLuaInline disp) opts ]; };
|
||||
# plain-key bind (no mod expression — bare key string)
|
||||
bk = key: disp: { _args = [ key (mkLuaInline disp) ]; };
|
||||
bko = key: disp: opts: { _args = [ key (mkLuaInline disp) opts ]; };
|
||||
# function-callback bind: key + raw lua body. Needed when one keypress runs
|
||||
# several dispatches (Hyprland 0.55 lua binds accept a function as the action).
|
||||
bkf = key: body: { _args = [ key (mkLuaInline "function() ${body} end") ]; };
|
||||
# same but with a mod-expression key (raw lua expr like the `b` helper).
|
||||
bf = keys: body: { _args = [ (mkLuaInline keys) (mkLuaInline "function() ${body} end") ]; };
|
||||
|
||||
# workspaces 1..10: switch / move / move-silent (was the genList in binds.nix /
|
||||
# the for-loop in lua.nix). key 10 -> "0".
|
||||
wsBinds = builtins.concatMap (i:
|
||||
let key = toString (lib.mod i 10); in [
|
||||
(b ''mod .. " + ${key}"'' "hl.dsp.focus({ workspace = ${toString i} })")
|
||||
(b ''mod .. " + SHIFT + ${key}"'' "hl.dsp.window.move({ workspace = ${toString i} })")
|
||||
(b ''mod .. " + CTRL + ${key}"'' "hl.dsp.window.move({ workspace = ${toString i}, follow = false })")
|
||||
]) (lib.range 1 10);
|
||||
|
||||
# vim-style window marks a..z (was markBinds): set / jump submap bodies.
|
||||
markLetters = lib.stringToCharacters "abcdefghijklmnopqrstuvwxyz";
|
||||
markSubmapBinds = action:
|
||||
(map (l: bk l ''hl.dsp.exec_cmd("hypr-mark ${action} ${l}")'') markLetters)
|
||||
++ [ (bk "escape" ''hl.dsp.submap("reset")'') ];
|
||||
|
||||
# dialog windows: float + center per title (was dialogRules).
|
||||
dialogTitles = [ "Open File" "Select a File" "Choose wallpaper" "Open Folder" "Save As" "Library" "File Upload" ];
|
||||
dialogRules = builtins.concatMap (t: [
|
||||
{ match = { title = "^(${t})(.*)$"; }; float = true; }
|
||||
{ match = { title = "^(${t})(.*)$"; }; center = true; }
|
||||
]) dialogTitles;
|
||||
|
||||
# place-submap window snapping lives in a Lua `place()` helper (see extraConfig).
|
||||
# It replaces the old exec_cmd("hyprctl --batch \"dispatch ...\"") strings: Hyprland
|
||||
# 0.55 reparses `hyprctl dispatch` args as Lua, so the plaintext dispatchers errored.
|
||||
in {
|
||||
wayland.windowManager.hyprland = {
|
||||
enable = true;
|
||||
package = null;
|
||||
portalPackage = null;
|
||||
systemd.enable = false;
|
||||
configType = "lua";
|
||||
|
||||
# Raw Lua appended after the generated config. `place()` drives the `place`
|
||||
# submap: float the active window, then size + position it as fractions of the
|
||||
# focused monitor. Monitor width/height are PHYSICAL px, but exact move/resize
|
||||
# work in LOGICAL space, so divide by scale (x/y are already logical).
|
||||
extraConfig = ''
|
||||
function place(fw, fh, fx, fy)
|
||||
local m = hl.get_active_monitor()
|
||||
local w = m.width / m.scale
|
||||
local h = m.height / m.scale
|
||||
hl.dispatch(hl.dsp.window.float({ action = "enable" }))
|
||||
hl.dispatch(hl.dsp.window.resize({ x = math.floor(w * fw), y = math.floor(h * fh), exact = true }))
|
||||
hl.dispatch(hl.dsp.window.move({ x = math.floor(m.x + w * fx), y = math.floor(m.y + h * fy), exact = true }))
|
||||
end
|
||||
|
||||
-- cycle_next({ next = false, tiled = true }) is broken on monocle/master:
|
||||
-- upstream routes it to layoutMessage("cyclenext, b") (ConfigActions.cpp),
|
||||
-- which neither layout parses -- monocle wants exactly "cycleprev", master
|
||||
-- only knows a "noloop" arg. Branch on the workspace layout and use the
|
||||
-- layoutmsg path there; generic window cycle everywhere else.
|
||||
function cycle_win(next)
|
||||
local ws = hl.get_active_workspace()
|
||||
local l = ws and ws.tiled_layout or ""
|
||||
if l == "monocle" or l == "master" then
|
||||
hl.dispatch(hl.dsp.layout(next and "cyclenext" or "cycleprev"))
|
||||
else
|
||||
hl.dispatch(hl.dsp.window.cycle_next({ tiled = true, next = next }))
|
||||
end
|
||||
end
|
||||
'';
|
||||
|
||||
settings = {
|
||||
# ----- variables (-> lua locals) ------------------------------------
|
||||
mod = { _var = "SUPER"; };
|
||||
menu = { _var = "walker"; };
|
||||
terminal = { _var = "uwsm app -- ghostty"; };
|
||||
fileManager = { _var = "uwsm app -- yazi"; };
|
||||
browser = { _var = "uwsm app -- firefox"; };
|
||||
workBrowser = { _var = "uwsm app -- firefox -P work --no-remote"; }; # -P matches HM profile name; --no-remote = separate instance alongside personal
|
||||
hardBrowser = { _var = "uwsm app -- mullvad-browser"; }; # ghost: Tor-Browser-minus-Tor, blends into the anti-fingerprint crowd
|
||||
webapp = { _var = mkLuaInline ''browser .. " --new-tab"''; };
|
||||
|
||||
# ----- env ----------------------------------------------------------
|
||||
env = [
|
||||
{ _args = [ "HYPRCURSOR_THEME" "rose-pine-hyprcursor" ]; }
|
||||
{ _args = [ "HYPRCURSOR_SIZE" "24" ]; }
|
||||
];
|
||||
|
||||
# ----- monitors + per-device (framework13) --------------------------
|
||||
monitor = [
|
||||
{ _args = [ { output = "DP-4"; mode = "3840x2160@60.00"; position = "0x0"; scale = 1; vrr = 1; } ]; }
|
||||
{ _args = [ { output = "eDP-1"; mode = "preferred"; position = "3840x0"; scale = 1.3333; vrr = 1; } ]; }
|
||||
];
|
||||
device = [
|
||||
{ _args = [ { name = "pixa3854:00-093a:0274-touchpad"; accel_profile = "custom ${accelpoints}"; scroll_points = accelpoints; natural_scroll = true; } ]; }
|
||||
];
|
||||
|
||||
# ----- autostart (was exec-once) ------------------------------------
|
||||
on = { _args = [ "hyprland.start" (mkLuaInline ''function()
|
||||
hl.exec_cmd("uwsm finalize")
|
||||
hl.exec_cmd("uwsm app -- waybar")
|
||||
hl.exec_cmd("uwsm app -- wl-paste --type text --watch cliphist store")
|
||||
hl.exec_cmd("uwsm app -- wl-paste --type image --watch cliphist store")
|
||||
hl.exec_cmd("hyprlock")
|
||||
hl.exec_cmd("uwsm app -- mako")
|
||||
-- fcitx5 is owned by the NixOS i18n.inputMethod XDG-autostart systemd
|
||||
-- unit (app-org.fcitx.Fcitx5@autostart.service). Launching it again
|
||||
-- here raced that unit via --replace and left the survivor with an
|
||||
-- empty IM group: apps connected over wayland_v2 but Ctrl+Space (and
|
||||
-- even dbus -o) toggled nothing. Let systemd own the single instance.
|
||||
hl.exec_cmd("[workspace 1 silent] uwsm app -- firefox")
|
||||
hl.exec_cmd("[workspace 1 silent] uwsm app -- obsidian")
|
||||
end'') ]; };
|
||||
|
||||
# ----- keyword config (hl.config) -----------------------------------
|
||||
config = {
|
||||
general = {
|
||||
gaps_in = 1; gaps_out = 0; border_size = 1;
|
||||
col = { active_border = active; inactive_border = inactive; };
|
||||
resize_on_border = false; allow_tearing = false; layout = "dwindle";
|
||||
};
|
||||
decoration = {
|
||||
rounding = 0;
|
||||
shadow = { enabled = false; range = 4; render_power = 3; color = "rgba(00000055)"; };
|
||||
blur = {
|
||||
enabled = false; size = 6; passes = 1; ignore_opacity = true;
|
||||
new_optimizations = true; special = true; popups = true; vibrancy = 0.1696;
|
||||
};
|
||||
};
|
||||
cursor = { no_hardware_cursors = false; hide_on_key_press = true; };
|
||||
master = {
|
||||
mfact = 0.60; new_status = "slave"; new_on_top = false; allow_small_split = true;
|
||||
# master on the RIGHT, stack on the left — better on the narrow laptop
|
||||
# panel than center-master. (slave_count/fallback only apply if you
|
||||
# cycle back to center via SUPER+CTRL+i.)
|
||||
orientation = "right"; slave_count_for_center_master = 2; center_master_fallback = "left";
|
||||
};
|
||||
dwindle = {
|
||||
preserve_split = true; # keep split orientation as windows come/go
|
||||
force_split = 2; # always split to the right / below (predictable)
|
||||
smart_split = false; # deterministic (not cursor-position based)
|
||||
default_split_ratio = 1.0; # even 50/50 splits
|
||||
};
|
||||
scrolling = {
|
||||
column_width = 0.5; fullscreen_on_one_column = true; follow_focus = true;
|
||||
wrap_focus = true; explicit_column_widths = "0.333, 0.5, 0.667, 1.0";
|
||||
};
|
||||
misc = { force_default_wallpaper = 0; enable_swallow = true; swallow_regex = "^(com.mitchellh.ghostty)$"; vrr = 1; };
|
||||
input = {
|
||||
kb_layout = "us"; follow_mouse = 1; sensitivity = 0;
|
||||
touchpad = { natural_scroll = true; disable_while_typing = true; };
|
||||
};
|
||||
group = {
|
||||
auto_group = false;
|
||||
groupbar = {
|
||||
enabled = true; font_size = 10; height = 18; rounding = 0;
|
||||
render_titles = true; scrolling = true;
|
||||
col = { active = active; inactive = inactive; locked_active = active; locked_inactive = inactive; };
|
||||
};
|
||||
col = { border_active = active; border_inactive = inactive; border_locked_active = active; border_locked_inactive = inactive; };
|
||||
};
|
||||
binds = { window_direction_monitor_fallback = false; allow_workspace_cycles = true; };
|
||||
animations = { enabled = false; };
|
||||
};
|
||||
|
||||
# ----- gestures -----------------------------------------------------
|
||||
gesture = [
|
||||
{ fingers = 3; direction = "left"; action = mkLuaInline ''function() hl.dispatch(hl.dsp.layout("move +col")) end''; }
|
||||
{ fingers = 3; direction = "right"; action = mkLuaInline ''function() hl.dispatch(hl.dsp.layout("move -col")) end''; }
|
||||
# 4-finger vertical: pull the comms scratchpads up/down
|
||||
{ fingers = 4; direction = "up"; action = mkLuaInline ''function() hl.dispatch(hl.dsp.workspace.toggle_special("chat")) end''; }
|
||||
{ fingers = 4; direction = "down"; action = mkLuaInline ''function() hl.dispatch(hl.dsp.workspace.toggle_special("work")) end''; }
|
||||
];
|
||||
|
||||
# ----- keybinds (was bind / bindm / bindle / bindl) -----------------
|
||||
bind = [
|
||||
# mouse (bindm)
|
||||
(bo ''mod .. " + mouse:272"'' "hl.dsp.window.drag()" { mouse = true; })
|
||||
(bo ''mod .. " + mouse:273"'' "hl.dsp.window.resize()" { mouse = true; })
|
||||
(bo ''mod .. " + ALT + mouse:272"'' "hl.dsp.window.resize()" { mouse = true; })
|
||||
|
||||
# compositor
|
||||
(b ''mod .. " + CTRL + ALT + Q"'' "hl.dsp.exit()")
|
||||
(b ''mod .. " + Q"'' "hl.dsp.window.close()")
|
||||
(b ''mod .. " + SHIFT + ESCAPE"'' ''hl.dsp.exec_cmd("systemctl suspend")'')
|
||||
(b ''mod .. " + SHIFT + CTRL + ESCAPE"'' ''hl.dsp.exec_cmd("systemctl poweroff")'')
|
||||
|
||||
# tiling control
|
||||
(b ''mod .. " + SHIFT + F"'' ''hl.dsp.window.fullscreen({ action = "toggle" })'')
|
||||
(b ''mod .. " + G"'' "hl.dsp.group.toggle()")
|
||||
(b ''mod .. " + F"'' ''hl.dsp.window.float({ action = "toggle" })'')
|
||||
(b ''mod .. " + T"'' ''hl.dsp.layout("togglesplit")'')
|
||||
(b ''mod .. " + SHIFT + P"'' "hl.dsp.window.pseudo()")
|
||||
(b ''mod .. " + SHIFT + C"'' "hl.dsp.window.center()")
|
||||
(b ''mod .. " + P"'' ''hl.dsp.window.pin({ action = "toggle" })'')
|
||||
(b ''mod .. " + D"'' "hl.dsp.focus({ urgent_or_last = true })")
|
||||
|
||||
# window cycling — cycle_win() branches per layout (see extraConfig): the
|
||||
# generic cycle_next reverse is broken upstream on monocle/master.
|
||||
(bf ''mod .. " + TAB"'' "cycle_win(true)")
|
||||
(bf ''mod .. " + SHIFT + TAB"'' "cycle_win(false)")
|
||||
(b ''mod .. " + CTRL + G"'' "hl.dsp.window.move({ out_of_group = true })")
|
||||
(b ''mod .. " + ALT + G"'' ''hl.dsp.exec_cmd("hyprctl dispatch lockgroups toggle")'')
|
||||
|
||||
# utilities
|
||||
(b ''mod .. " + RETURN"'' "hl.dsp.exec_cmd(terminal)")
|
||||
(b ''mod .. " + y"'' "hl.dsp.exec_cmd(fileManager)")
|
||||
(b ''mod .. " + SPACE"'' "hl.dsp.exec_cmd(menu)")
|
||||
(b ''mod .. " + CTRL + Q"'' ''hl.dsp.exec_cmd("hyprlock")'')
|
||||
|
||||
# walker providers
|
||||
(b ''mod .. " + CTRL + V"'' ''hl.dsp.exec_cmd("walker -m clipboard")'')
|
||||
(b ''mod .. " + W"'' ''hl.dsp.exec_cmd("walker -m windows")'')
|
||||
(b ''mod .. " + CTRL + period"'' ''hl.dsp.exec_cmd("walker -m symbols")'')
|
||||
(b ''mod .. " + CTRL + N"'' ''hl.dsp.exec_cmd("walker -m todo")'')
|
||||
|
||||
# keybind cheatsheet
|
||||
(b ''mod .. " + slash"'' ''hl.dsp.exec_cmd("hypr-binds-menu")'')
|
||||
|
||||
# notifications
|
||||
(b ''mod .. " + SHIFT + D"'' ''hl.dsp.exec_cmd("makoctl mode -t dnd; pkill -RTMIN+9 waybar")'')
|
||||
(b ''mod .. " + CTRL + D"'' ''hl.dsp.exec_cmd("mako-history-menu")'')
|
||||
|
||||
# move focus
|
||||
(b ''mod .. " + h"'' ''hl.dsp.focus({ direction = "left" })'')
|
||||
(b ''mod .. " + l"'' ''hl.dsp.focus({ direction = "right" })'')
|
||||
(b ''mod .. " + k"'' ''hl.dsp.focus({ direction = "up" })'')
|
||||
(b ''mod .. " + j"'' ''hl.dsp.focus({ direction = "down" })'')
|
||||
|
||||
# group tab cycling (moved off SUPER+TAB, now the window cycler)
|
||||
(bk "ALT + TAB" "hl.dsp.group.next()")
|
||||
(bk "ALT + SHIFT + TAB" "hl.dsp.group.prev()")
|
||||
|
||||
# move window or group
|
||||
(b ''mod .. " + SHIFT + h"'' ''hl.dsp.window.move({ direction = "l", group_aware = true })'')
|
||||
(b ''mod .. " + SHIFT + l"'' ''hl.dsp.window.move({ direction = "r", group_aware = true })'')
|
||||
(b ''mod .. " + SHIFT + k"'' ''hl.dsp.window.move({ direction = "u", group_aware = true })'')
|
||||
(b ''mod .. " + SHIFT + j"'' ''hl.dsp.window.move({ direction = "d", group_aware = true })'')
|
||||
|
||||
# cycle workspaces
|
||||
(b ''mod .. " + bracketleft"'' ''hl.dsp.focus({ workspace = "m-1" })'')
|
||||
(b ''mod .. " + bracketright"'' ''hl.dsp.focus({ workspace = "m+1" })'')
|
||||
(b ''mod .. " + ESCAPE"'' ''hl.dsp.focus({ workspace = "previous" })'')
|
||||
|
||||
# cycle monitors
|
||||
(b ''mod .. " + SHIFT + bracketleft"'' ''hl.dsp.focus({ monitor = "l" })'')
|
||||
(b ''mod .. " + SHIFT + bracketright"'' ''hl.dsp.focus({ monitor = "r" })'')
|
||||
|
||||
# send workspace to monitor
|
||||
(b ''mod .. " + SHIFT + ALT + bracketleft"'' ''hl.dsp.workspace.move({ monitor = "l" })'')
|
||||
(b ''mod .. " + SHIFT + ALT + bracketright"'' ''hl.dsp.workspace.move({ monitor = "r" })'')
|
||||
|
||||
# master: promote to master
|
||||
(b ''mod .. " + CTRL + RETURN"'' ''hl.dsp.layout("swapwithmaster master")'')
|
||||
|
||||
# column resize (scrolling)
|
||||
(b ''mod .. " + equal"'' ''hl.dsp.layout("colresize +conf")'')
|
||||
(b ''mod .. " + minus"'' ''hl.dsp.layout("colresize -conf")'')
|
||||
(b ''mod .. " + SHIFT + SPACE"'' ''hl.dsp.layout("orientationnext")'')
|
||||
(b ''mod .. " + CTRL + SHIFT + SPACE"'' ''hl.dsp.layout("orientationprev")'')
|
||||
|
||||
# scroll layout
|
||||
(b ''mod .. " + c"'' ''hl.dsp.layout("fit active")'')
|
||||
(b ''mod .. " + SHIFT + c"'' ''hl.dsp.layout("fit visible")'')
|
||||
(b ''mod .. " + CTRL + c"'' ''hl.dsp.layout("fit all")'')
|
||||
(b ''mod .. " + period"'' ''hl.dsp.layout("move +col")'')
|
||||
(b ''mod .. " + comma"'' ''hl.dsp.layout("move -col")'')
|
||||
|
||||
# theme toggle
|
||||
(b ''mod .. " + F5"'' ''hl.dsp.exec_cmd("theme-switch")'')
|
||||
|
||||
# screenshot
|
||||
(bk "SHIFT + Print" ''hl.dsp.exec_cmd("screenshot area")'')
|
||||
(bk "Print" ''hl.dsp.exec_cmd("screenshot full")'')
|
||||
|
||||
# screen recording
|
||||
(b ''mod .. " + Print"'' ''hl.dsp.exec_cmd("wf-recorder-toggle")'')
|
||||
(b ''mod .. " + SHIFT + Print"'' ''hl.dsp.exec_cmd("wf-recorder-toggle area")'')
|
||||
|
||||
# special workspaces (chat-toggle retired -> plain toggle)
|
||||
(b ''mod .. " + M"'' ''hl.dsp.workspace.toggle_special("chat")'')
|
||||
(b ''mod .. " + SHIFT + M"'' ''hl.dsp.window.move({ workspace = "special:chat" })'')
|
||||
(b ''mod .. " + B"'' ''hl.dsp.workspace.toggle_special("work")'')
|
||||
(b ''mod .. " + SHIFT + B"'' ''hl.dsp.window.move({ workspace = "special:work" })'')
|
||||
|
||||
# z-order for floating windows
|
||||
(b ''mod .. " + ALT + k"'' ''hl.dsp.window.alter_zorder({ mode = "top" })'')
|
||||
(b ''mod .. " + ALT + j"'' ''hl.dsp.window.alter_zorder({ mode = "bottom" })'')
|
||||
|
||||
# submap entries
|
||||
(b ''mod .. " + R"'' ''hl.dsp.submap("resize")'')
|
||||
(b ''mod .. " + X"'' ''hl.dsp.submap("launch")'')
|
||||
(b ''mod .. " + V"'' ''hl.dsp.submap("place")'')
|
||||
(b ''mod .. " + N"'' ''hl.dsp.submap("mark")'')
|
||||
(b ''mod .. " + apostrophe"'' ''hl.dsp.submap("jump")'')
|
||||
|
||||
# dynamic layout control --------------------------------------------
|
||||
# scrolling tape (ws1/3/5 + specials): swap columns / promote / recentre.
|
||||
# (consume/expel dropped — SUPER+SHIFT+h/l already moves windows between
|
||||
# columns. layoutmsg is layout-scoped, so these no-op off scrolling.)
|
||||
(b ''mod .. " + SHIFT + u"'' ''hl.dsp.layout("swapcol l")'')
|
||||
(b ''mod .. " + SHIFT + o"'' ''hl.dsp.layout("swapcol r")'')
|
||||
(b ''mod .. " + CTRL + u"'' ''hl.dsp.layout("promote")'')
|
||||
(b ''mod .. " + CTRL + o"'' ''hl.dsp.layout("center")'')
|
||||
# master stack (ws2 coding): add/remove a master window, cycle orientation
|
||||
(b ''mod .. " + i"'' ''hl.dsp.layout("addmaster")'')
|
||||
(b ''mod .. " + SHIFT + i"'' ''hl.dsp.layout("removemaster")'')
|
||||
(b ''mod .. " + CTRL + i"'' ''hl.dsp.layout("orientationcycle")'')
|
||||
|
||||
# locked / repeating media + brightness (bindle / bindl)
|
||||
(bko "XF86AudioRaiseVolume" ''hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+")'' { locked = true; repeating = true; })
|
||||
(bko "XF86AudioLowerVolume" ''hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-")'' { locked = true; repeating = true; })
|
||||
(bko "XF86AudioMute" ''hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle")'' { locked = true; repeating = true; })
|
||||
(bko "XF86AudioMicMute" ''hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle")'' { locked = true; repeating = true; })
|
||||
(bko "XF86MonBrightnessUp" ''hl.dsp.exec_cmd("brightnessctl -c backlight s 10%+")'' { locked = true; repeating = true; })
|
||||
(bko "XF86MonBrightnessDown" ''hl.dsp.exec_cmd("brightnessctl -c backlight s 10%-")'' { locked = true; repeating = true; })
|
||||
(bko "XF86AudioNext" ''hl.dsp.exec_cmd("playerctl next")'' { locked = true; })
|
||||
(bko "XF86AudioPause" ''hl.dsp.exec_cmd("playerctl play-pause")'' { locked = true; })
|
||||
(bko "XF86AudioPlay" ''hl.dsp.exec_cmd("playerctl play-pause")'' { locked = true; })
|
||||
(bko "XF86AudioPrev" ''hl.dsp.exec_cmd("playerctl previous")'' { locked = true; })
|
||||
] ++ wsBinds;
|
||||
|
||||
# ----- window rules -------------------------------------------------
|
||||
window_rule = [
|
||||
# tags
|
||||
{ match = { class = "^([Ff]irefox|org.mozilla.firefox|[Ff]irefox-esr|[Ff]irefox-bin)$"; }; tag = "+browser"; }
|
||||
{ match = { class = "^([Gg]oogle-chrome(-beta|-dev|-unstable)?)$"; }; tag = "+browser"; }
|
||||
{ match = { class = "^([Cc]hromium)$"; }; tag = "+browser"; }
|
||||
{ match = { class = "^([Cc]hromium-browser)$"; }; tag = "+browser"; }
|
||||
{ match = { class = "^(com.mitchellh.ghostty)$"; }; tag = "+terminal"; }
|
||||
{ match = { class = "^([Tt]hunderbird)$"; }; tag = "+im-work"; }
|
||||
{ match = { class = "^([Ss]lack)$"; }; tag = "+im-work"; }
|
||||
{ match = { class = "^([Dd]iscord).*$"; }; tag = "+im"; }
|
||||
{ match = { class = "^(org.telegram.desktop)$"; }; tag = "+im"; }
|
||||
{ match = { class = "^([Ss]ignal)$"; }; tag = "+im"; }
|
||||
{ match = { class = "^([Tt]hunar|org.gnome.Nautilus)$"; }; tag = "+file-manager"; }
|
||||
{ match = { class = "^(firefox)$"; title = "^(Picture-in-Picture)$"; }; tag = "+pip"; }
|
||||
{ match = { class = "^(org.keepassxc.KeePassXC)$"; }; tag = "+passwd"; }
|
||||
|
||||
{ match = { class = ".*"; }; suppress_event = "maximize"; }
|
||||
{ match = { class = "^$"; title = "^$"; xwayland = true; float = true; fullscreen = false; pin = false; }; no_focus = true; }
|
||||
|
||||
# opacity
|
||||
{ match = { class = ".*"; }; opacity = "1.0 0.97"; }
|
||||
{ match = { tag = "file-manager"; }; opacity = "0.97 0.8"; }
|
||||
|
||||
{ match = { tag = "im-work"; }; workspace = "special:work"; }
|
||||
{ match = { tag = "im"; }; workspace = "special:chat"; }
|
||||
|
||||
# notes = obsidian beside firefox on ws1
|
||||
{ match = { tag = "browser"; }; workspace = "1"; }
|
||||
{ match = { class = "^(obsidian)$"; }; workspace = "1"; }
|
||||
|
||||
{ match = { title = "^(Grayjay)$"; }; float = false; }
|
||||
{ match = { title = "^(Grayjay)$"; }; workspace = "4"; }
|
||||
|
||||
# picture-in-picture
|
||||
{ match = { initial_title = "^Picture-in-Picture$"; }; opacity = "1 1"; }
|
||||
{ match = { tag = "pip"; }; float = true; }
|
||||
{ match = { tag = "pip"; }; pin = true; }
|
||||
{ match = { tag = "pip"; }; no_focus = true; }
|
||||
{ match = { tag = "pip"; }; size = "300 169"; }
|
||||
{ match = { tag = "pip"; }; move = "100%-320 100%-190"; }
|
||||
{ match = { tag = "pip"; }; keep_aspect_ratio = true; }
|
||||
|
||||
# keepassxc
|
||||
{ match = { tag = "passwd"; }; float = true; }
|
||||
{ match = { tag = "passwd"; }; center = true; }
|
||||
{ match = { tag = "passwd"; }; size = "60% 70%"; }
|
||||
|
||||
{ match = { tag = "terminal"; }; tile = true; }
|
||||
|
||||
# scrolling layout column widths
|
||||
{ match = { class = "^(obsidian)$"; }; scrolling_width = 0.333; }
|
||||
{ match = { tag = "terminal"; }; scrolling_width = 0.5; }
|
||||
{ match = { tag = "browser"; }; scrolling_width = 0.667; }
|
||||
{ match = { tag = "im-work"; }; scrolling_width = 0.333; }
|
||||
{ match = { tag = "im"; }; scrolling_width = 0.5; }
|
||||
|
||||
# screen sharing
|
||||
{ match = { tag = "im"; }; no_screen_share = true; }
|
||||
{ match = { tag = "im-work"; }; no_screen_share = true; }
|
||||
{ match = { tag = "passwd"; }; no_screen_share = true; }
|
||||
|
||||
{ match = { class = "([Tt]hunar)"; title = "negative:(.*[Tt]hunar.*)"; }; center = true; }
|
||||
|
||||
# satty annotation editor
|
||||
{ match = { class = "^(com.gabm.satty)$"; }; float = true; }
|
||||
{ match = { class = "^(com.gabm.satty)$"; }; size = "80% 80%"; }
|
||||
{ match = { class = "^(com.gabm.satty)$"; }; center = true; }
|
||||
|
||||
# smart borders
|
||||
{ match = { float = false; workspace = "w[tv1]"; }; border_size = 0; }
|
||||
{ match = { float = false; workspace = "w[tv1]"; }; rounding = 0; }
|
||||
{ match = { float = false; workspace = "f[1]"; }; border_size = 0; }
|
||||
{ match = { float = false; workspace = "f[1]"; }; rounding = 0; }
|
||||
] ++ dialogRules;
|
||||
|
||||
# ----- workspace rules ----------------------------------------------
|
||||
workspace_rule = [
|
||||
# per-workspace layouts
|
||||
{ workspace = "1"; layout = "scrolling"; }
|
||||
{ workspace = "2"; layout = "master"; }
|
||||
{ workspace = "3"; layout = "scrolling"; }
|
||||
{ workspace = "4"; layout = "monocle"; }
|
||||
{ workspace = "5"; layout = "scrolling"; }
|
||||
{ workspace = "special:work"; layout = "scrolling"; }
|
||||
{ workspace = "special:chat"; layout = "scrolling"; }
|
||||
|
||||
{ workspace = "special:work"; gaps_out = 40; gaps_in = 10; }
|
||||
{ workspace = "special:chat"; gaps_out = 40; gaps_in = 10; }
|
||||
|
||||
# smart gaps
|
||||
{ workspace = "w[tv1]"; gaps_out = 0; gaps_in = 0; }
|
||||
{ workspace = "f[1]"; gaps_out = 0; gaps_in = 0; }
|
||||
|
||||
# per-workspace monitor pins (framework13)
|
||||
{ workspace = "1"; monitor = "DP-4"; default = true; }
|
||||
{ workspace = "2"; monitor = "DP-4"; }
|
||||
{ workspace = "3"; monitor = "DP-4"; }
|
||||
{ workspace = "4"; monitor = "eDP-1"; default = true; }
|
||||
{ workspace = "5"; monitor = "eDP-1"; }
|
||||
];
|
||||
};
|
||||
|
||||
# ----- submaps --------------------------------------------------------
|
||||
submaps = {
|
||||
# resize: sticky, repeatable; exit with escape/return
|
||||
resize.settings.bind = [
|
||||
(bko "h" "hl.dsp.window.resize({ x = -100, y = 0, relative = true })" { repeating = true; })
|
||||
(bko "l" "hl.dsp.window.resize({ x = 100, y = 0, relative = true })" { repeating = true; })
|
||||
(bko "k" "hl.dsp.window.resize({ x = 0, y = -100, relative = true })" { repeating = true; })
|
||||
(bko "j" "hl.dsp.window.resize({ x = 0, y = 100, relative = true })" { repeating = true; })
|
||||
(bk "escape" ''hl.dsp.submap("reset")'')
|
||||
(bk "RETURN" ''hl.dsp.submap("reset")'')
|
||||
];
|
||||
|
||||
# launch: auto-resets to global after each action
|
||||
launch = {
|
||||
onDispatch = "reset";
|
||||
settings.bind = [
|
||||
(bk "A" ''hl.dsp.exec_cmd(webapp .. " https://openrouter.ai/chat")'')
|
||||
(bk "B" "hl.dsp.exec_cmd(browser)")
|
||||
(bk "W" "hl.dsp.exec_cmd(workBrowser)")
|
||||
(bk "H" "hl.dsp.exec_cmd(hardBrowser)")
|
||||
(bk "C" ''hl.dsp.exec_cmd(webapp .. " https://app.hey.com/calendar/weeks/")'')
|
||||
(bk "D" ''hl.dsp.exec_cmd(terminal .. " -e lazydocker")'')
|
||||
(bk "E" ''hl.dsp.exec_cmd(webapp .. " https://app.hey.com")'')
|
||||
(bk "O" ''hl.dsp.exec_cmd("uwsm app -- obsidian")'')
|
||||
(bk "R" ''hl.dsp.exec_cmd(terminal .. " -e newsboat")'')
|
||||
(bk "slash" ''hl.dsp.exec_cmd("uwsm app -- keepassxc")'')
|
||||
(bk "escape" ''hl.dsp.submap("reset")'')
|
||||
];
|
||||
};
|
||||
|
||||
# place: snap floating windows to zones; stays active for multiple placements
|
||||
place.settings.bind = [
|
||||
# halves — place(width, height, x, y) as monitor fractions
|
||||
(bkf "h" "place(0.5, 1.0, 0.0, 0.0)")
|
||||
(bkf "l" "place(0.5, 1.0, 0.5, 0.0)")
|
||||
(bkf "k" "place(1.0, 0.5, 0.0, 0.0)")
|
||||
(bkf "j" "place(1.0, 0.5, 0.0, 0.5)")
|
||||
# center variants (centred = equal margins: x = (1 - w) / 2)
|
||||
(bkf "c" "place(0.7, 0.7, 0.15, 0.15)")
|
||||
(bkf "f" "place(0.95, 0.95, 0.025, 0.025)")
|
||||
(bkf "m" "place(0.5, 0.5, 0.25, 0.25)")
|
||||
# quadrants
|
||||
(bkf "1" "place(0.5, 0.5, 0.0, 0.0)")
|
||||
(bkf "2" "place(0.5, 0.5, 0.5, 0.0)")
|
||||
(bkf "3" "place(0.5, 0.5, 0.0, 0.5)")
|
||||
(bkf "4" "place(0.5, 0.5, 0.5, 0.5)")
|
||||
# thirds (4K panel): u/i/o = left/middle/right column
|
||||
(bkf "u" "place(0.333, 1.0, 0.0, 0.0)")
|
||||
(bkf "i" "place(0.334, 1.0, 0.333, 0.0)")
|
||||
(bkf "o" "place(0.333, 1.0, 0.667, 0.0)")
|
||||
# two-thirds: SHIFT+u = left, SHIFT+o = right
|
||||
(bkf "SHIFT + u" "place(0.667, 1.0, 0.0, 0.0)")
|
||||
(bkf "SHIFT + o" "place(0.667, 1.0, 0.333, 0.0)")
|
||||
# t = back to tiling and out of the submap
|
||||
(bkf "t" ''hl.dispatch(hl.dsp.window.float({ action = "disable" })); hl.dispatch(hl.dsp.submap("reset"))'')
|
||||
# incremental move (repeatable)
|
||||
(bko "SHIFT + h" "hl.dsp.window.move({ x = -40, y = 0, relative = true })" { repeating = true; })
|
||||
(bko "SHIFT + l" "hl.dsp.window.move({ x = 40, y = 0, relative = true })" { repeating = true; })
|
||||
(bko "SHIFT + k" "hl.dsp.window.move({ x = 0, y = -40, relative = true })" { repeating = true; })
|
||||
(bko "SHIFT + j" "hl.dsp.window.move({ x = 0, y = 40, relative = true })" { repeating = true; })
|
||||
# incremental resize (repeatable)
|
||||
(bko "equal" "hl.dsp.window.resize({ x = 40, y = 40, relative = true })" { repeating = true; })
|
||||
(bko "minus" "hl.dsp.window.resize({ x = -40, y = -40, relative = true })" { repeating = true; })
|
||||
(bk "escape" ''hl.dsp.submap("reset")'')
|
||||
(bk "RETURN" ''hl.dsp.submap("reset")'')
|
||||
];
|
||||
|
||||
# vim marks: set / jump, auto-reset after each letter
|
||||
mark = { onDispatch = "reset"; settings.bind = markSubmapBinds "set"; };
|
||||
jump = { onDispatch = "reset"; settings.bind = markSubmapBinds "jump"; };
|
||||
};
|
||||
};
|
||||
}
|
||||
103
home/programs/wayland/hyprlock.nix
Normal file
103
home/programs/wayland/hyprlock.nix
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
{ config, inputs, pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
programs.hyprlock = {
|
||||
enable = true;
|
||||
|
||||
package = inputs.hyprlock.packages.${pkgs.stdenv.hostPlatform.system}.hyprlock;
|
||||
|
||||
settings = {
|
||||
general = {
|
||||
disable_loading_bar = true;
|
||||
immediate_render = true;
|
||||
hide_cursor = false;
|
||||
no_fade_in = true;
|
||||
};
|
||||
|
||||
auth = {
|
||||
fingerprint = {
|
||||
enabled = true;
|
||||
ready_message = "(Scan fingerprint to unlock)";
|
||||
present_message = "Scanning fingerprint";
|
||||
retry_delay = 250;
|
||||
};
|
||||
};
|
||||
|
||||
animation = [
|
||||
"inputFieldDots, 1, 2, linear"
|
||||
"fadeIn, 0"
|
||||
];
|
||||
|
||||
background = lib.mkForce [
|
||||
{
|
||||
monitor = "";
|
||||
path = builtins.toString config.stylix.image;
|
||||
}
|
||||
];
|
||||
|
||||
input-field = lib.mkForce [
|
||||
{
|
||||
monitor = "eDP-1";
|
||||
|
||||
size = "300, 50";
|
||||
valign = "bottom";
|
||||
position = "0%, 10%";
|
||||
|
||||
outline_thickness = 1;
|
||||
|
||||
font_color = "rgb(b6c4ff)";
|
||||
outer_color = "rgba(180, 180, 180, 0.5)";
|
||||
inner_color = "rgba(200, 200, 200, 0.1)";
|
||||
check_color = "rgba(247, 193, 19, 0.5)";
|
||||
fail_color = "rgba(255, 106, 134, 0.5)";
|
||||
|
||||
fade_on_empty = false;
|
||||
placeholder_text = "Enter Password";
|
||||
|
||||
dots_spacing = 0.2;
|
||||
dots_center = true;
|
||||
dots_fade_time = 100;
|
||||
|
||||
shadow_color = "rgba(0, 0, 0, 0.1)";
|
||||
shadow_size = 7;
|
||||
shadow_passes = 2;
|
||||
}
|
||||
];
|
||||
|
||||
label = [
|
||||
{
|
||||
monitor = "";
|
||||
text = "$TIME";
|
||||
font_size = 150;
|
||||
color = "rgb(b6c4ff)";
|
||||
|
||||
position = "0%, 30%";
|
||||
|
||||
valign = "center";
|
||||
halign = "center";
|
||||
|
||||
shadow_color = "rgba(0, 0, 0, 0.1)";
|
||||
shadow_size = 20;
|
||||
shadow_passes = 2;
|
||||
shadow_boost = 0.3;
|
||||
}
|
||||
{
|
||||
monitor = "";
|
||||
text = "cmd[update:3600000] date +'%a %b %d'";
|
||||
font_size = 20;
|
||||
color = "rgb(b6c4ff)";
|
||||
|
||||
position = "0%, 40%";
|
||||
|
||||
valign = "center";
|
||||
halign = "center";
|
||||
|
||||
shadow_color = "rgba(0, 0, 0, 0.1)";
|
||||
shadow_size = 20;
|
||||
shadow_passes = 2;
|
||||
shadow_boost = 0.3;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
280
home/programs/wayland/walker.nix
Normal file
280
home/programs/wayland/walker.nix
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
{ inputs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
inputs.walker.homeManagerModules.default
|
||||
];
|
||||
programs.walker = {
|
||||
enable = true;
|
||||
runAsService = true;
|
||||
config = {
|
||||
# Global UX settings
|
||||
force_keyboard_focus = false;
|
||||
close_when_open = true;
|
||||
click_to_close = true;
|
||||
single_click_activation = true;
|
||||
selection_wrap = false;
|
||||
global_argument_delimiter = "#";
|
||||
exact_search_prefix = "'";
|
||||
theme = "default";
|
||||
disable_mouse = false;
|
||||
debug = false;
|
||||
page_jump_items = 10;
|
||||
hide_quick_activation = false;
|
||||
hide_action_hints = false;
|
||||
hide_action_hints_dmenu = true;
|
||||
hide_return_action = false;
|
||||
resume_last_query = false;
|
||||
actions_as_menu = false;
|
||||
|
||||
app_launch_prefix = "uwsm app -- ";
|
||||
|
||||
# Shell anchoring for proper positioning
|
||||
shell = {
|
||||
anchor_top = true;
|
||||
anchor_bottom = true;
|
||||
anchor_left = true;
|
||||
anchor_right = true;
|
||||
};
|
||||
|
||||
# Column configuration for grid layouts
|
||||
columns = {
|
||||
symbols = 3;
|
||||
};
|
||||
|
||||
# Placeholders for empty states
|
||||
placeholders = {
|
||||
default = {
|
||||
input = "Search";
|
||||
list = "No Results";
|
||||
};
|
||||
};
|
||||
|
||||
# Keybinds - preserving your vim-style navigation
|
||||
keys = {
|
||||
close = [ "esc" ];
|
||||
next = [ "ctrl j" ]; # vim-style
|
||||
prev = [ "ctrl k" ]; # vim-style
|
||||
left = [ "Left" ];
|
||||
right = [ "Right" ];
|
||||
down = [ "ctrl j" ];
|
||||
up = [ "ctrl k" ];
|
||||
accept_typeahead = [ "tab" ];
|
||||
trigger_labels = "lalt";
|
||||
toggle_exact_search = [ "ctrl m" ];
|
||||
resume_query = [ "ctrl r" ];
|
||||
remove_from_history = [ "shift backspace" ];
|
||||
quick_activate = [ "F1" "F2" "F3" "F4" ];
|
||||
page_down = [ "Page_Down" ];
|
||||
page_up = [ "Page_Up" ];
|
||||
show_actions = [ "alt j" ];
|
||||
activation_modifiers = {
|
||||
keep_open = "shift";
|
||||
alternate = "alt";
|
||||
};
|
||||
};
|
||||
|
||||
# Provider configuration
|
||||
providers = {
|
||||
default = [
|
||||
"desktopapplications"
|
||||
"calc"
|
||||
"websearch"
|
||||
];
|
||||
empty = [ "desktopapplications" ];
|
||||
ignore_preview = [ ];
|
||||
max_results = 50;
|
||||
|
||||
# Prefix-based quick access
|
||||
prefixes = [
|
||||
{ prefix = ";"; provider = "providerlist"; }
|
||||
{ prefix = ">"; provider = "runner"; }
|
||||
{ prefix = "/"; provider = "files"; }
|
||||
{ prefix = "."; provider = "symbols"; }
|
||||
{ prefix = "!"; provider = "todo"; }
|
||||
{ prefix = "%"; provider = "bookmarks"; }
|
||||
{ prefix = "="; provider = "calc"; }
|
||||
{ prefix = "@"; provider = "websearch"; }
|
||||
{ prefix = ":"; provider = "clipboard"; }
|
||||
{ prefix = "$"; provider = "windows"; }
|
||||
];
|
||||
|
||||
# Provider-specific configuration
|
||||
clipboard = {
|
||||
time_format = "relative";
|
||||
};
|
||||
|
||||
# Provider-specific actions
|
||||
actions = {
|
||||
fallback = [
|
||||
{ action = "menus:open"; label = "open"; after = "Nothing"; }
|
||||
{ action = "menus:default"; label = "run"; after = "Close"; }
|
||||
{ action = "menus:parent"; label = "back"; bind = "Escape"; after = "Nothing"; }
|
||||
{ action = "erase_history"; label = "clear hist"; bind = "ctrl h"; after = "AsyncReload"; }
|
||||
];
|
||||
|
||||
dmenu = [
|
||||
{ action = "select"; default = true; bind = "Return"; }
|
||||
];
|
||||
|
||||
providerlist = [
|
||||
{ action = "activate"; default = true; bind = "Return"; after = "ClearReload"; }
|
||||
];
|
||||
|
||||
calc = [
|
||||
{ action = "copy"; default = true; bind = "Return"; }
|
||||
{ action = "delete"; bind = "ctrl d"; after = "AsyncReload"; }
|
||||
{ action = "delete_all"; bind = "ctrl shift d"; after = "AsyncReload"; }
|
||||
{ action = "save"; bind = "ctrl s"; after = "AsyncClearReload"; }
|
||||
];
|
||||
|
||||
websearch = [
|
||||
{ action = "search"; default = true; bind = "Return"; }
|
||||
{ action = "open_url"; label = "open url"; default = true; bind = "Return"; }
|
||||
];
|
||||
|
||||
desktopapplications = [
|
||||
{ action = "start"; default = true; bind = "Return"; }
|
||||
{ action = "start:keep"; label = "open+next"; bind = "shift Return"; after = "KeepOpen"; }
|
||||
{ action = "new_instance"; label = "new instance"; bind = "ctrl Return"; }
|
||||
{ action = "new_instance:keep"; label = "new+next"; bind = "ctrl alt Return"; after = "KeepOpen"; }
|
||||
{ action = "pin"; bind = "ctrl p"; after = "AsyncReload"; }
|
||||
{ action = "unpin"; bind = "ctrl p"; after = "AsyncReload"; }
|
||||
{ action = "pinup"; bind = "ctrl n"; after = "AsyncReload"; }
|
||||
{ action = "pindown"; bind = "ctrl m"; after = "AsyncReload"; }
|
||||
];
|
||||
|
||||
files = [
|
||||
{ action = "open"; default = true; bind = "Return"; }
|
||||
{ action = "opendir"; label = "open dir"; bind = "ctrl Return"; }
|
||||
{ action = "copypath"; label = "copy path"; bind = "ctrl shift c"; }
|
||||
{ action = "copyfile"; label = "copy file"; bind = "ctrl c"; }
|
||||
{ action = "localsend"; label = "localsend"; bind = "ctrl l"; }
|
||||
{ action = "refresh_index"; label = "reload"; bind = "ctrl r"; after = "AsyncReload"; }
|
||||
];
|
||||
|
||||
runner = [
|
||||
{ action = "run"; default = true; bind = "Return"; }
|
||||
{ action = "runterminal"; label = "run in terminal"; bind = "shift Return"; }
|
||||
];
|
||||
|
||||
symbols = [
|
||||
{ action = "run_cmd"; label = "select"; default = true; bind = "Return"; }
|
||||
];
|
||||
|
||||
unicode = [
|
||||
{ action = "run_cmd"; label = "select"; default = true; bind = "Return"; }
|
||||
];
|
||||
|
||||
clipboard = [
|
||||
{ action = "copy"; default = true; bind = "Return"; }
|
||||
{ action = "remove"; bind = "ctrl d"; after = "AsyncClearReload"; }
|
||||
{ action = "remove_all"; label = "clear"; bind = "ctrl shift d"; after = "AsyncClearReload"; }
|
||||
{ action = "show_images_only"; label = "only images"; bind = "ctrl i"; after = "AsyncClearReload"; }
|
||||
{ action = "show_text_only"; label = "only text"; bind = "ctrl i"; after = "AsyncClearReload"; }
|
||||
{ action = "show_combined"; label = "show all"; bind = "ctrl i"; after = "AsyncClearReload"; }
|
||||
{ action = "pause"; bind = "ctrl shift p"; }
|
||||
{ action = "unpause"; bind = "ctrl shift p"; }
|
||||
{ action = "unpin"; bind = "ctrl p"; after = "AsyncClearReload"; }
|
||||
{ action = "pin"; bind = "ctrl p"; after = "AsyncClearReload"; }
|
||||
{ action = "edit"; bind = "ctrl o"; }
|
||||
{ action = "localsend"; bind = "ctrl l"; }
|
||||
];
|
||||
|
||||
bookmarks = [
|
||||
{ action = "save"; bind = "Return"; after = "AsyncClearReload"; }
|
||||
{ action = "open"; default = true; bind = "Return"; }
|
||||
{ action = "delete"; bind = "ctrl d"; after = "AsyncClearReload"; }
|
||||
{ action = "change_category"; label = "Change category"; bind = "ctrl y"; after = "Nothing"; }
|
||||
{ action = "change_browser"; label = "Change browser"; bind = "ctrl b"; after = "Nothing"; }
|
||||
{ action = "import"; label = "Import"; bind = "ctrl i"; after = "AsyncClearReload"; }
|
||||
{ action = "create"; bind = "ctrl a"; after = "AsyncClearReload"; }
|
||||
{ action = "search"; bind = "ctrl a"; after = "AsyncClearReload"; }
|
||||
];
|
||||
|
||||
todo = [
|
||||
{ action = "save"; default = true; bind = "Return"; after = "AsyncClearReload"; }
|
||||
{ action = "save_next"; label = "save & new"; bind = "shift Return"; after = "AsyncClearReload"; }
|
||||
{ action = "delete"; bind = "ctrl d"; after = "AsyncClearReload"; }
|
||||
{ action = "active"; default = true; bind = "Return"; after = "Nothing"; }
|
||||
{ action = "inactive"; default = true; bind = "Return"; after = "Nothing"; }
|
||||
{ action = "done"; bind = "ctrl f"; after = "Nothing"; }
|
||||
{ action = "change_category"; bind = "ctrl y"; label = "change category"; after = "Nothing"; }
|
||||
{ action = "clear"; bind = "ctrl x"; after = "AsyncClearReload"; }
|
||||
{ action = "create"; bind = "ctrl a"; after = "AsyncClearReload"; }
|
||||
{ action = "search"; bind = "ctrl a"; after = "AsyncClearReload"; }
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Builtins configuration
|
||||
builtins = {
|
||||
bookmarks = {
|
||||
weight = 5;
|
||||
placeholder = "Bookmarks";
|
||||
name = "bookmarks";
|
||||
icon = "bookmark";
|
||||
switcher_only = true;
|
||||
entries = [
|
||||
{
|
||||
label = "Walker";
|
||||
url = "https://github.com/abenz1267/walker";
|
||||
keywords = [ "walker" "github" ];
|
||||
}
|
||||
];
|
||||
};
|
||||
clipboard = {
|
||||
exec = "wl-copy";
|
||||
height = 5;
|
||||
name = "clipboard";
|
||||
avoid_line_breaks = true;
|
||||
placeholder = "Clipboard";
|
||||
image_height = 300;
|
||||
max_entries = 10;
|
||||
switcher_only = true;
|
||||
};
|
||||
commands = {
|
||||
weight = 5;
|
||||
icon = "utilities-terminal";
|
||||
switcher_only = true;
|
||||
name = "commands";
|
||||
placeholder = "Commands";
|
||||
};
|
||||
switcher = {
|
||||
weight = 5;
|
||||
icon = "switcher";
|
||||
placeholder = "Switcher";
|
||||
prefix = "/";
|
||||
};
|
||||
theme = {
|
||||
weight = 5;
|
||||
icon = "switcher";
|
||||
placeholder = "Theme";
|
||||
prefix = "theme";
|
||||
exec = "hyprland-theme-switcher";
|
||||
};
|
||||
translation = {
|
||||
provider = "googlefree";
|
||||
};
|
||||
websearch = {
|
||||
keep_selection = true;
|
||||
weight = 5;
|
||||
icon = "applications-internet";
|
||||
name = "websearch";
|
||||
placeholder = "Websearch";
|
||||
entries = [
|
||||
{
|
||||
name = "Google";
|
||||
url = "https://www.google.com/search?q=%TERM%";
|
||||
}
|
||||
{
|
||||
name = "DuckDuckGo";
|
||||
url = "https://duckduckgo.com/?q=%TERM%";
|
||||
switcher_only = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
428
home/programs/wayland/waybar/default.nix
Normal file
428
home/programs/wayland/waybar/default.nix
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
c = config.lib.stylix.colors.withHashtag;
|
||||
isDark = config.stylix.polarity == "dark";
|
||||
bgAlpha = if isDark then "0.6" else "0.85";
|
||||
hoverAlpha = if isDark then "0.9" else "0.95";
|
||||
|
||||
# monocle dots: cyan-ish base0C to complement the base0E workspace dots;
|
||||
# light schemes wash out low pango alpha, so dim less aggressively there
|
||||
monocleAccent = c.base0C;
|
||||
monocleDimAlpha = if isDark then "40%" else "65%";
|
||||
|
||||
# monocle window dots: one dot per tiled window on the active workspace,
|
||||
# active window filled — only when the workspace's layout is monocle
|
||||
# (hyprctl activeworkspace -j exposes tiledLayout on 0.55). Empty text
|
||||
# otherwise, which hides the module. Event-driven via the hyprland IPC
|
||||
# event socket, so no polling.
|
||||
monocle-indicator = pkgs.writeShellApplication {
|
||||
name = "waybar-monocle-indicator";
|
||||
runtimeInputs = with pkgs; [ jq socat ];
|
||||
text = ''
|
||||
emit() {
|
||||
local ws layout wsid active out tooltip addr title
|
||||
ws=$(hyprctl activeworkspace -j)
|
||||
layout=$(jq -r .tiledLayout <<<"$ws")
|
||||
if [ "$layout" != monocle ]; then
|
||||
printf '{"text":""}\n'
|
||||
return
|
||||
fi
|
||||
wsid=$(jq -r .id <<<"$ws")
|
||||
active=$(hyprctl activewindow -j | jq -r .address)
|
||||
out=""
|
||||
tooltip=""
|
||||
while IFS=$'\t' read -r addr title; do
|
||||
if [ "$addr" = "$active" ]; then
|
||||
out+="<span color='${monocleAccent}'></span> "
|
||||
tooltip+="● $title"$'\n'
|
||||
else
|
||||
out+="<span color='${monocleAccent}' alpha='${monocleDimAlpha}'></span> "
|
||||
tooltip+="○ $title"$'\n'
|
||||
fi
|
||||
done < <(hyprctl clients -j \
|
||||
| jq -r --argjson w "$wsid" \
|
||||
'.[] | select(.workspace.id == $w and .mapped and (.floating | not)) | [.address, .title] | @tsv')
|
||||
jq -cn --arg t "''${out% }" --arg tip "''${tooltip%$'\n'}" '{text: $t, tooltip: $tip}'
|
||||
}
|
||||
emit || true
|
||||
socat -U - "UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" \
|
||||
| while read -r ev; do
|
||||
case "$ev" in
|
||||
workspace*|focusedmon*|activewindowv2*|openwindow*|closewindow*|movewindow*|changefloatingmode*|configreloaded*)
|
||||
emit || true ;;
|
||||
esac
|
||||
done
|
||||
'';
|
||||
};
|
||||
in {
|
||||
programs.waybar = {
|
||||
enable = true;
|
||||
style = ''
|
||||
@define-color foreground ${if isDark then c.base05 else c.base00};
|
||||
@define-color foreground-inactive ${if isDark then c.base04 else c.base03};
|
||||
@define-color background ${if isDark then c.base00 else c.base04};
|
||||
@define-color color9 ${c.base0E};
|
||||
|
||||
* {
|
||||
font-family: JetBrainsMono Nerd Font;
|
||||
font-size: 17px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
window#waybar {
|
||||
all: unset;
|
||||
color: @foreground;
|
||||
}
|
||||
|
||||
.modules-left,
|
||||
.modules-center,
|
||||
.modules-right {
|
||||
color: @foreground;
|
||||
background: alpha(@background, ${bgAlpha});
|
||||
margin: 5px 10px;
|
||||
padding: 0 5px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.modules-left {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.modules-center {
|
||||
padding: 0 10px;
|
||||
margin: 5 0 5 0;
|
||||
}
|
||||
|
||||
#clock,
|
||||
#battery,
|
||||
#cpu,
|
||||
#memory,
|
||||
#disk,
|
||||
#temperature,
|
||||
#network,
|
||||
#pulseaudio,
|
||||
#custom-gpu,
|
||||
#custom-dnd,
|
||||
#custom-fcitx,
|
||||
#idle_inhibitor,
|
||||
#power-profiles-daemon {
|
||||
padding: 0 10px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
#submap {
|
||||
padding: 0 10px;
|
||||
border-radius: 15px;
|
||||
color: @color9;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#custom-dnd.dnd {
|
||||
color: @color9;
|
||||
}
|
||||
|
||||
#custom-fcitx.zh {
|
||||
color: @color9;
|
||||
}
|
||||
|
||||
#idle_inhibitor.activated {
|
||||
color: @color9;
|
||||
}
|
||||
|
||||
#clock:hover,
|
||||
#battery:hover,
|
||||
#cpu:hover,
|
||||
#memory:hover,
|
||||
#disk:hover,
|
||||
#temperature:hover,
|
||||
#network:hover,
|
||||
#pulseaudio:hover,
|
||||
#custom-gpu:hover,
|
||||
#idle_inhibitor:hover,
|
||||
#power-profiles-daemon:hover {
|
||||
background: alpha(@background, ${hoverAlpha});
|
||||
}
|
||||
|
||||
#workspaces {
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
#custom-monocle {
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
#workspaces button {
|
||||
all: unset;
|
||||
padding: 0px 5px;
|
||||
color: alpha(@color9, 0.4);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#workspaces button:hover {
|
||||
color: alpha(@color9, 0.7);
|
||||
border: none;
|
||||
text-shadow: 0px 0px 1.5px alpha(@color9, 0.5);
|
||||
transition: all 1s ease;
|
||||
}
|
||||
#workspaces button.active {
|
||||
color: @color9;
|
||||
border: none;
|
||||
text-shadow: 0px 0px 2px alpha(@color9, 0.5);
|
||||
}
|
||||
#workspaces button.empty {
|
||||
color: alpha(@foreground, 0.15);
|
||||
border: none;
|
||||
text-shadow: 0px 0px 1.5px alpha(@foreground, 0.1);
|
||||
}
|
||||
#workspaces button.empty:hover {
|
||||
color: alpha(@foreground, 0.3);
|
||||
border: none;
|
||||
text-shadow: 0px 0px 1.5px alpha(@foreground, 0.3);
|
||||
transition: all 1s ease;
|
||||
}
|
||||
#workspaces button.empty.active {
|
||||
color: @color9;
|
||||
border: none;
|
||||
text-shadow: 0px 0px 2px alpha(@color9, 0.5);
|
||||
}
|
||||
#workspaces button.urgent {
|
||||
color: ${c.base08};
|
||||
border: none;
|
||||
text-shadow: 0px 0px 2px alpha(${c.base08}, 0.7);
|
||||
}
|
||||
'';
|
||||
settings = [{
|
||||
layer = "top";
|
||||
position = "top";
|
||||
height = 24;
|
||||
spacing = 5;
|
||||
margin = "0";
|
||||
modules-left = ["clock" "custom/timew" "custom/playerctl"];
|
||||
modules-center = ["hyprland/submap" "hyprland/workspaces" "custom/monocle"];
|
||||
modules-right = ["custom/fcitx" "pulseaudio" "battery" "power-profiles-daemon" "network" "group/hardware" "custom/gpu" "temperature" "idle_inhibitor" "custom/dnd" "privacy"];
|
||||
"hyprland/workspaces" = {
|
||||
format = "{icon}";
|
||||
format-icons = {
|
||||
"active" = "";
|
||||
"default" = "";
|
||||
"empty" = "";
|
||||
# named special workspaces (waybar strips the "special:" prefix)
|
||||
"chat" = "";
|
||||
"work" = "";
|
||||
};
|
||||
on-click = "activate";
|
||||
icon-size = 10;
|
||||
sort-by-number= true;
|
||||
show-special = true;
|
||||
persistent-workspaces = {
|
||||
"1" = [];
|
||||
"2" = [];
|
||||
"3" = [];
|
||||
"4" = [];
|
||||
"5" = [];
|
||||
};
|
||||
};
|
||||
|
||||
# monocle window index dots (see monocle-indicator above); scroll cycles
|
||||
"custom/monocle" = {
|
||||
format = "{}";
|
||||
return-type = "json";
|
||||
exec = "${monocle-indicator}/bin/waybar-monocle-indicator";
|
||||
on-scroll-up = ''hyprctl dispatch "hl.dsp.layout('cyclenext')"'';
|
||||
on-scroll-down = ''hyprctl dispatch "hl.dsp.layout('cycleprev')"'';
|
||||
tooltip = true;
|
||||
};
|
||||
|
||||
# visible only while a submap (resize/launch/place) is active
|
||||
"hyprland/submap" = {
|
||||
format = " {}";
|
||||
tooltip = false;
|
||||
};
|
||||
|
||||
# mako do-not-disturb indicator; refreshed via RTMIN+9 from the toggle
|
||||
# bind and from meeting-guard (auto-mute while mic/screenshare live)
|
||||
"custom/dnd" = {
|
||||
format = "{}";
|
||||
return-type = "json";
|
||||
exec = "m=$(makoctl mode); if echo \"$m\" | grep -qx dnd; then printf '{\"text\":\"\",\"tooltip\":\"do not disturb\",\"class\":\"dnd\"}'; elif echo \"$m\" | grep -qx meeting; then printf '{\"text\":\"\",\"tooltip\":\"in a call — notifications muted\",\"class\":\"dnd\"}'; else printf '{\"text\":\"\",\"tooltip\":\"notifications on\"}'; fi";
|
||||
interval = "once";
|
||||
signal = 9;
|
||||
on-click = "makoctl mode -t dnd; pkill -RTMIN+9 waybar";
|
||||
on-click-right = "mako-history-menu";
|
||||
};
|
||||
|
||||
"group/hardware" = {
|
||||
orientation = "inherit";
|
||||
modules = [
|
||||
"cpu"
|
||||
"memory"
|
||||
"disk"
|
||||
];
|
||||
drawer = {
|
||||
transition-duration = 250;
|
||||
};
|
||||
};
|
||||
|
||||
"custom/playerctl" = {
|
||||
format = " {}";
|
||||
return-type = "json";
|
||||
max-length = 40;
|
||||
exec = "playerctl -a metadata --format '{\"text\": \"{{artist}} - {{markup_escape(title)}}\", \"tooltip\": \"{{playerName}} : {{artist}} - {{markup_escape(title)}}\", \"alt\": \"{{status}}\", \"class\": \"{{status}}\"}' -F";
|
||||
on-click = "playerctl play-pause";
|
||||
on-click-right = "playerctl next";
|
||||
};
|
||||
|
||||
"custom/timew" = {
|
||||
format = " {}";
|
||||
exec = "timew get dom.active.json | jq -r '.tags | join(\" \")'";
|
||||
exec-if = "timew get dom.active";
|
||||
interval = 10;
|
||||
};
|
||||
|
||||
"idle_inhibitor" = {
|
||||
format = "{icon}";
|
||||
"format-icons" = {
|
||||
activated = "";
|
||||
deactivated = "";
|
||||
};
|
||||
tooltip = true;
|
||||
};
|
||||
|
||||
clock = {
|
||||
format = "{:%H:%M}";
|
||||
interval = 15;
|
||||
format-alt = " {:%Y-%m-%d}";
|
||||
tooltip-format = "<big>{:%Y %B}</big>\n<tt><small>{calendar}</small></tt>";
|
||||
calendar = {
|
||||
mode = "month";
|
||||
mode-mon-col = 3;
|
||||
weeks-pos = "right";
|
||||
on-scroll = 1;
|
||||
on-click-right = "mode";
|
||||
format = {
|
||||
months = "<span color='#d3c6aa'><b>{}</b></span>";
|
||||
days = "<span color='#e67e80'>{}</span>";
|
||||
weeks = "<span color='#a7c080'><b>W{}</b></span>";
|
||||
weekdays = "<span color='#7fbbb3'><b>{}</b></span>";
|
||||
today = "<span color='#dbbc7f'><b><u>{}</u></b></span>";
|
||||
};
|
||||
};
|
||||
actions = {
|
||||
on-click-right = "mode";
|
||||
on-click-forward = "tz_up";
|
||||
on-click-backward = "tz_down";
|
||||
on-scroll-up = "shift_up";
|
||||
on-scroll-down = "shift_down";
|
||||
};
|
||||
};
|
||||
|
||||
cpu = {
|
||||
format = " {usage}%";
|
||||
tooltip = true;
|
||||
interval = 1;
|
||||
on-click = "ghostty -e btop";
|
||||
};
|
||||
|
||||
memory = {
|
||||
format = " {used:0.1f}G/{total:0.1f}G";
|
||||
interval = 1;
|
||||
on-click = "ghostty -e btop";
|
||||
};
|
||||
|
||||
temperature = {
|
||||
critical-threshold = 80;
|
||||
format = "{icon} {temperatureC}°C";
|
||||
format-icons = ["" "" ""];
|
||||
on-click = "ghostty -e s-tui";
|
||||
};
|
||||
|
||||
battery = {
|
||||
states = {
|
||||
good = 95;
|
||||
warning = 30;
|
||||
critical = 1;
|
||||
};
|
||||
format = "{icon} {capacity}%";
|
||||
format-charging = " {capacity}%";
|
||||
format-plugged = " {capacity}%";
|
||||
format-alt = "{icon} {time}";
|
||||
format-icons = ["" "" "" "" "" "" "" "" "" "" ""];
|
||||
};
|
||||
|
||||
network = {
|
||||
format-wifi = " {essid} ({signalStrength}%)";
|
||||
format-ethernet = " {ifname}";
|
||||
format-linked = " {ifname} (No IP)";
|
||||
format-disconnected = " Disconnected";
|
||||
format-alt = "{ifname}: {ipaddr}/{cidr}";
|
||||
tooltip-format = "{ifname}: {ipaddr}";
|
||||
on-click = "ghostty -e sudo nmtui";
|
||||
};
|
||||
|
||||
power-profiles-daemon = {
|
||||
format = "{icon}";
|
||||
tooltip-format = "Power profile: {profile}\nDriver: {driver}";
|
||||
tooltip = true;
|
||||
format-icons = {
|
||||
default = "";
|
||||
performance = "";
|
||||
balanced = "";
|
||||
power-saver = "";
|
||||
};
|
||||
};
|
||||
|
||||
pulseaudio = {
|
||||
format = "{icon} {volume}%";
|
||||
# no dedicated bluetooth module (duplicates badly) — BT sinks just
|
||||
# show as regular volume here
|
||||
format-bluetooth = "{icon} {volume}%";
|
||||
format-bluetooth-muted = "";
|
||||
format-muted = "";
|
||||
format-icons = {
|
||||
headphone = "";
|
||||
hands-free = "";
|
||||
headset = "";
|
||||
phone = "";
|
||||
portable = "";
|
||||
car = "";
|
||||
default = ["" "" ""];
|
||||
};
|
||||
on-click = "pwvucontrol";
|
||||
on-click-right = "pactl set-sink-mute @DEFAULT_SINK@ toggle";
|
||||
on-scroll-up = "pactl set-sink-volume @DEFAULT_SINK@ +2%";
|
||||
on-scroll-down = "pactl set-sink-volume @DEFAULT_SINK@ -2%";
|
||||
};
|
||||
|
||||
"custom/gpu" = {
|
||||
format = " {}%";
|
||||
exec = "cat /sys/class/drm/card*/device/gpu_busy_percent";
|
||||
interval = 2;
|
||||
tooltip-format = "GPU utilization";
|
||||
};
|
||||
|
||||
disk = {
|
||||
interval = 30;
|
||||
format = " {percentage_used}%";
|
||||
path = "/";
|
||||
on-click = "ghostty -e gdu /";
|
||||
};
|
||||
|
||||
privacy = {
|
||||
icon-spacing = 4;
|
||||
icon-size = 16;
|
||||
modules = [
|
||||
{
|
||||
type = "screenshare";
|
||||
tooltip = true;
|
||||
tooltip-icon-size = 24;
|
||||
}
|
||||
{
|
||||
type = "audio-in";
|
||||
tooltip = true;
|
||||
tooltip-icon-size = 24;
|
||||
}
|
||||
];
|
||||
};
|
||||
}];
|
||||
};
|
||||
}
|
||||
113
home/programs/wayland/waybar/style.css
Normal file
113
home/programs/wayland/waybar/style.css
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
@define-color foreground #e6d9db;
|
||||
@define-color foreground-inactive #aaaaaa;
|
||||
@define-color background #2c2525;
|
||||
@define-color color9 #00ffcc;
|
||||
|
||||
* {
|
||||
font-family: JetBrainsMono Nerd Font;
|
||||
font-size: 17px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
window#waybar {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.modules-left,
|
||||
.modules-center,
|
||||
.modules-right {
|
||||
background: alpha(@background, 0.6);
|
||||
margin: 5px 10px;
|
||||
padding: 0 5px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.modules-left {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.modules-center {
|
||||
padding: 0 10px;
|
||||
margin: 5 0 5 0;
|
||||
}
|
||||
|
||||
#clock,
|
||||
#battery,
|
||||
#cpu,
|
||||
#memory,
|
||||
#disk,
|
||||
#temperature,
|
||||
#backlight,
|
||||
#network,
|
||||
#pulseaudio,
|
||||
#wireplumber,
|
||||
#custom-media,
|
||||
#tray,
|
||||
#mode,
|
||||
#idle_inhibitor,
|
||||
#scratchpad,
|
||||
#power-profiles-daemon,
|
||||
#language,
|
||||
#mpd {
|
||||
padding: 0 10px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
#clock:hover,
|
||||
#battery:hover,
|
||||
#cpu:hover,
|
||||
#memory:hover,
|
||||
#disk:hover,
|
||||
#temperature:hover,
|
||||
#backlight:hover,
|
||||
#network:hover,
|
||||
#pulseaudio:hover,
|
||||
#wireplumber:hover,
|
||||
#custom-media:hover,
|
||||
#tray:hover,
|
||||
#mode:hover,
|
||||
#idle_inhibitor:hover,
|
||||
#scratchpad:hover,
|
||||
#power-profiles-daemon:hover,
|
||||
#language:hover,
|
||||
#mpd:hover {
|
||||
background: rgba(26, 27, 38, 0.9);
|
||||
}
|
||||
|
||||
#workspaces {
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
#workspaces button {
|
||||
all: unset;
|
||||
padding: 0px 5px;
|
||||
color: alpha(@color9, 0.4);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
#workspaces button:hover {
|
||||
color: rgba(0, 0, 0, 0);
|
||||
border: none;
|
||||
text-shadow: 0px 0px 1.5px rgba(0, 0, 0, 0.5);
|
||||
transition: all 1s ease;
|
||||
}
|
||||
#workspaces button.active {
|
||||
color: @color9;
|
||||
border: none;
|
||||
text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
#workspaces button.empty {
|
||||
color: rgba(0, 0, 0, 0);
|
||||
border: none;
|
||||
text-shadow: 0px 0px 1.5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
#workspaces button.empty:hover {
|
||||
color: rgba(0, 0, 0, 0);
|
||||
border: none;
|
||||
text-shadow: 0px 0px 1.5px rgba(0, 0, 0, 0.5);
|
||||
transition: all 1s ease;
|
||||
}
|
||||
#workspaces button.empty.active {
|
||||
color: @color9;
|
||||
border: none;
|
||||
text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
83
home/programs/wayland/wf-recorder.nix
Normal file
83
home/programs/wayland/wf-recorder.nix
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
wf-recorder-toggle = pkgs.writeShellApplication {
|
||||
name = "wf-recorder-toggle";
|
||||
runtimeInputs = with pkgs; [
|
||||
wf-recorder
|
||||
slurp
|
||||
libnotify
|
||||
coreutils
|
||||
procps # pgrep, pkill
|
||||
];
|
||||
text = ''
|
||||
RECORDINGS_DIR="''${HOME}/Videos/Recordings"
|
||||
LOOPBACK_PID_FILE="/tmp/wf-recorder-loopback.pid"
|
||||
mkdir -p "$RECORDINGS_DIR"
|
||||
|
||||
cleanup_loopback() {
|
||||
if [ -f "$LOOPBACK_PID_FILE" ]; then
|
||||
kill "$(cat "$LOOPBACK_PID_FILE")" 2>/dev/null || true
|
||||
rm -f "$LOOPBACK_PID_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# If already recording, stop and exit
|
||||
if pgrep -x wf-recorder > /dev/null; then
|
||||
pkill -SIGINT wf-recorder
|
||||
cleanup_loopback
|
||||
notify-send "Recording stopped" "Saved to $RECORDINGS_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Clean up any leftover loopback from a previous crashed session
|
||||
cleanup_loopback
|
||||
|
||||
FILENAME="$RECORDINGS_DIR/recording-$(date +%Y%m%d-%H%M%S).mp4"
|
||||
|
||||
# Get the PipeWire object ID of the default audio sink.
|
||||
# We use pw-loopback to capture the sink's audio output (what you hear)
|
||||
# because PipeWire doesn't expose .monitor sources for Bluetooth sinks
|
||||
# through PulseAudio compat — making pactl-based approaches unreliable.
|
||||
SINK_ID="$(wpctl inspect @DEFAULT_AUDIO_SINK@ | head -1 | awk '{print $2}' | tr -d ',')"
|
||||
|
||||
if [ -z "$SINK_ID" ]; then
|
||||
notify-send "Recording failed" "No default audio sink found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create a PipeWire loopback that captures the sink's monitor output
|
||||
# and exposes it as a virtual source for wf-recorder.
|
||||
pw-loopback \
|
||||
--capture-props="target.object=$SINK_ID stream.capture.sink=true" \
|
||||
--playback-props="media.class=Audio/Source node.name=wf-recorder-audio" &
|
||||
echo $! > "$LOOPBACK_PID_FILE"
|
||||
sleep 0.5
|
||||
|
||||
ARGS=(--audio --audio-device=wf-recorder-audio -f "$FILENAME")
|
||||
|
||||
if [ "''${1:-}" = "area" ]; then
|
||||
GEOMETRY=$(slurp)
|
||||
if [ -z "$GEOMETRY" ]; then
|
||||
cleanup_loopback
|
||||
exit 1
|
||||
fi
|
||||
ARGS+=(-g "$GEOMETRY")
|
||||
fi
|
||||
|
||||
wf-recorder "''${ARGS[@]}" &
|
||||
disown
|
||||
|
||||
if [ "''${1:-}" = "area" ]; then
|
||||
notify-send "Recording started" "Area recording..."
|
||||
else
|
||||
notify-send "Recording started" "Screen recording..."
|
||||
fi
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
home.packages = [
|
||||
wf-recorder-toggle
|
||||
];
|
||||
}
|
||||
7
home/programs/yt-dlp.nix
Normal file
7
home/programs/yt-dlp.nix
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
home.packages = with pkgs; [
|
||||
yt-dlp
|
||||
];
|
||||
}
|
||||
22
home/services/gpg.nix
Normal file
22
home/services/gpg.nix
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
services.gpg-agent = {
|
||||
enable = true;
|
||||
# Cache passphrases for 1 hour
|
||||
defaultCacheTtl = 3600;
|
||||
maxCacheTtl = 7200;
|
||||
# Use Qt pinentry for GUI prompts (works with vim-fugitive on Hyprland)
|
||||
pinentry.package = pkgs.pinentry-qt;
|
||||
# Enable SSH support via GPG agent (uses YubiKey for SSH auth)
|
||||
enableSshSupport = true;
|
||||
# Cache SSH keys for 1 hour
|
||||
defaultCacheTtlSsh = 3600;
|
||||
# YubiKey authentication subkey keygrips for SSH
|
||||
sshKeys = [
|
||||
"139C66F1D36546BFD8FBBE5570384F1130CDFE6E" # YubiKey 2 (5C NFC) auth key - 0x05AB33B50A345D4D
|
||||
"F25F34CF85F685E5C1B0DE428B8A7228EBDB03E5" # YubiKey 1 (NEO) auth key - 0x6D916A825C88D51F
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
41
home/services/mako.nix
Normal file
41
home/services/mako.nix
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
services = {
|
||||
mako = {
|
||||
enable = true;
|
||||
|
||||
settings = {
|
||||
sort = "-time";
|
||||
border-radius = 2;
|
||||
border-size = 3;
|
||||
actions = true;
|
||||
anchor = "top-right";
|
||||
default-timeout = 5000;
|
||||
icons = true;
|
||||
layer = "overlay";
|
||||
padding = "12,20";
|
||||
width = 420;
|
||||
height = 110;
|
||||
margin = 10;
|
||||
outer-margin = 20;
|
||||
max-icon-size = 32;
|
||||
# history pool consumed by mako-history-menu ($mod CTRL D)
|
||||
max-history = 100;
|
||||
|
||||
"urgency=critical" = {
|
||||
default-timeout = 0;
|
||||
};
|
||||
|
||||
# do-not-disturb: toggled via `makoctl mode -t dnd` (bind + waybar)
|
||||
"mode=dnd" = {
|
||||
invisible = 1;
|
||||
};
|
||||
|
||||
# auto-set by meeting-guard while mic/screenshare is live — separate
|
||||
# mode so it never fights the manual dnd toggle
|
||||
"mode=meeting" = {
|
||||
invisible = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
6
home/services/media/playerctl.nix
Normal file
6
home/services/media/playerctl.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{ pkgs, ... }:
|
||||
{
|
||||
home.packages = [ pkgs.playerctl ];
|
||||
|
||||
services.playerctld.enable = true;
|
||||
}
|
||||
48
home/services/meeting-guard.nix
Normal file
48
home/services/meeting-guard.nix
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
# Auto-DND during calls: while any pipewire capture stream is running
|
||||
# (mic or screenshare/camera consumption), mako gets a "meeting" mode —
|
||||
# invisible notifications, so nothing pops over a shared screen. Uses its
|
||||
# own mako mode rather than "dnd" so the manual SUPER+SHIFT+D toggle and
|
||||
# this guard never fight over the same flag.
|
||||
let
|
||||
meeting-guard = pkgs.writeShellApplication {
|
||||
name = "meeting-guard";
|
||||
runtimeInputs = with pkgs; [ pipewire jq mako procps ];
|
||||
text = ''
|
||||
# clear stale state from a previous run
|
||||
makoctl mode -r meeting >/dev/null || true
|
||||
state=0
|
||||
while true; do
|
||||
n=$(pw-dump 2>/dev/null | jq '[.[]
|
||||
| select(.type == "PipeWire:Interface:Node"
|
||||
and ((.info.props["media.class"] // "") | test("^Stream/Input/(Audio|Video)$"))
|
||||
and .info.state == "running")
|
||||
] | length' || echo 0)
|
||||
if [ "$n" -gt 0 ] && [ "$state" -eq 0 ]; then
|
||||
makoctl mode -a meeting >/dev/null
|
||||
state=1
|
||||
pkill -RTMIN+9 waybar || true
|
||||
elif [ "$n" -eq 0 ] && [ "$state" -eq 1 ]; then
|
||||
makoctl mode -r meeting >/dev/null
|
||||
state=0
|
||||
pkill -RTMIN+9 waybar || true
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
'';
|
||||
};
|
||||
in {
|
||||
systemd.user.services.meeting-guard = {
|
||||
Unit = {
|
||||
Description = "auto-DND while a call (mic/screenshare) is active";
|
||||
After = [ "graphical-session.target" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${meeting-guard}/bin/meeting-guard";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
}
|
||||
5
home/services/podman.nix
Normal file
5
home/services/podman.nix
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
home.sessionVariables = {
|
||||
DOCKER_HOST = "unix://$XDG_RUNTIME_DIR/podman/podman.sock";
|
||||
};
|
||||
}
|
||||
202
home/services/syncthing.nix
Normal file
202
home/services/syncthing.nix
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
{ config, lib, pkgs, inputs, ... }:
|
||||
|
||||
let
|
||||
# Secrets file location
|
||||
secretsFile = "${inputs.secrets}/secrets/framework13/syncthing.yaml";
|
||||
|
||||
# Device IDs - obtained from Syncthing on each device; kept in the
|
||||
# private flake's vars so they stay out of the public repo
|
||||
deviceIds = inputs.secrets.vars.syncthing.deviceIds;
|
||||
|
||||
# Common versioning configuration for 3-2-1 backup strategy
|
||||
# Trashcan versioning moves deleted/replaced files to .stversions
|
||||
versioningConfig = {
|
||||
type = "trashcan";
|
||||
params = {
|
||||
cleanoutDays = "30"; # Keep deleted files for 30 days
|
||||
};
|
||||
};
|
||||
in {
|
||||
# SOPS secrets configuration for Syncthing GUI authentication
|
||||
sops = {
|
||||
age.keyFile = "/home/bogdan/.config/sops/age/keys.txt";
|
||||
# YubiKey is the sole recipient now; the activation service runs with a
|
||||
# minimal PATH, so the plugin binary must be injected explicitly or the
|
||||
# AGE-PLUGIN-YUBIKEY identity in keys.txt can't be resolved.
|
||||
age.plugins = [ pkgs.age-plugin-yubikey ];
|
||||
secrets = {
|
||||
syncthing_gui_password = {
|
||||
sopsFile = secretsFile;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.syncthing = {
|
||||
enable = true;
|
||||
|
||||
# Declarative configuration - overwrite any manual changes
|
||||
overrideDevices = true;
|
||||
overrideFolders = true;
|
||||
|
||||
# GUI accessible from all interfaces (secured by password)
|
||||
# Access via: http://localhost:8384 or http://<tailscale-ip>:8384
|
||||
guiAddress = "0.0.0.0:8384";
|
||||
|
||||
# GUI credentials from SOPS secrets
|
||||
# HM 26.05 removed the standalone `passwordFile`; use `guiCredentials`.
|
||||
guiCredentials = {
|
||||
username = "bogdan";
|
||||
passwordFile = config.sops.secrets.syncthing_gui_password.path;
|
||||
};
|
||||
|
||||
settings = {
|
||||
gui = {
|
||||
theme = "dark";
|
||||
# Hardcoded username for GUI authentication
|
||||
user = "bogdan";
|
||||
};
|
||||
|
||||
options = {
|
||||
# Prefer local network syncing to save Tailscale/mobile data
|
||||
# Local discovery on LAN - primary sync method
|
||||
localAnnounceEnabled = true;
|
||||
# Global discovery as fallback when not on same LAN
|
||||
globalAnnounceEnabled = true;
|
||||
# Relay servers as last resort (uses bandwidth)
|
||||
relaysEnabled = true;
|
||||
# NAT traversal for direct connections
|
||||
natEnabled = true;
|
||||
# Limit relay usage - prefer direct connections
|
||||
relayReconnectIntervalM = 30;
|
||||
# Automatically upgrade syncthing
|
||||
autoUpgradeIntervalH = 0; # Disabled - managed by Nix
|
||||
# Crash reporting
|
||||
crashReportingEnabled = true;
|
||||
# Start browser on startup
|
||||
startBrowser = false;
|
||||
};
|
||||
|
||||
# Devices to sync with
|
||||
devices = {
|
||||
"Pixel 10 Pro" = {
|
||||
id = deviceIds.pixel;
|
||||
# Auto-accept folders shared by this device
|
||||
autoAcceptFolders = false;
|
||||
# Addresses - let Syncthing discover automatically
|
||||
# Prefers local discovery, falls back to global/relay
|
||||
addresses = [ "dynamic" ];
|
||||
};
|
||||
"Steam Deck" = {
|
||||
id = deviceIds.steamdeck;
|
||||
autoAcceptFolders = false;
|
||||
addresses = [ "dynamic" ];
|
||||
};
|
||||
"Boox Go Color 7" = {
|
||||
id = deviceIds.boox;
|
||||
autoAcceptFolders = false;
|
||||
addresses = [ "dynamic" ];
|
||||
};
|
||||
};
|
||||
|
||||
# Folders to sync - matching restic backup paths
|
||||
folders = {
|
||||
# Documents folder - bidirectional sync with Pixel, send-only to Steam Deck
|
||||
"Documents" = {
|
||||
id = "docs";
|
||||
path = "/home/bogdan/Documents";
|
||||
devices = [ "Pixel 10 Pro" ];
|
||||
# Bidirectional sync (Steam Deck is configured as receive-only on its end)
|
||||
type = "sendreceive";
|
||||
# Versioning for 3-2-1 backup (local versions)
|
||||
versioning = versioningConfig;
|
||||
# Watch for changes using inotify
|
||||
fsWatcherEnabled = true;
|
||||
# Rescan interval in seconds (fallback if inotify misses something)
|
||||
rescanIntervalS = 3600; # 1 hour
|
||||
# Ignore permissions (mobile devices have different permission models)
|
||||
ignorePerms = true;
|
||||
};
|
||||
|
||||
# Passwords folder - sync across all devices
|
||||
"Passwords" = {
|
||||
id = "passwords";
|
||||
path = "/home/bogdan/Passwords";
|
||||
devices = [ "Pixel 10 Pro" "Steam Deck" ];
|
||||
type = "sendreceive";
|
||||
versioning = versioningConfig;
|
||||
fsWatcherEnabled = true;
|
||||
rescanIntervalS = 3600;
|
||||
ignorePerms = true;
|
||||
};
|
||||
|
||||
# Steam game recordings from Steam Deck - receive-only
|
||||
"Steam Recordings" = {
|
||||
id = "steam-recordings";
|
||||
path = "/home/bogdan/Videos/Steam Recordings";
|
||||
devices = [ "Steam Deck" ];
|
||||
# Receive-only: Framework13 receives recordings but doesn't modify them
|
||||
type = "receiveonly";
|
||||
| < | ||||