Compare commits
41 Commits
0d6b8ca13a
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
5e1b9332a4
|
|||
|
c9b976f0ae
|
|||
|
07a78b6012
|
|||
|
94411c0abe
|
|||
|
7415ea0b54
|
|||
|
184a2f5545
|
|||
|
b413c8af81
|
|||
|
5c4045f4a6
|
|||
|
e4a8b07117
|
|||
|
f533bbeb05
|
|||
|
031bad3238
|
|||
|
dc676b6f8c
|
|||
|
f64d68ac05
|
|||
|
4be1c4fb67
|
|||
|
7205841c4d
|
|||
|
0851515de0
|
|||
|
de6c3993cc
|
|||
|
98eb0f87ba
|
|||
|
d8d86aadcb
|
|||
|
c375a15561
|
|||
|
08a14d52f2
|
|||
|
eab578e699
|
|||
|
8f9eff1640
|
|||
|
3e093b02d1
|
|||
|
b907a39c41
|
|||
|
c1ec7401d1
|
|||
|
80c90c03ae
|
|||
|
53f079c242
|
|||
|
e6d96725ad
|
|||
|
f9c3dad584
|
|||
|
270ffa8208
|
|||
|
b6f9a9beec
|
|||
|
0c2ae04cdf
|
|||
|
eb8693e853
|
|||
|
7b44fe8bf1
|
|||
|
b2fabbc764
|
|||
|
5172ca4d18
|
|||
|
46cebeb8fd
|
|||
|
f35037387c
|
|||
|
d43381727d
|
|||
|
2cba23b804
|
@@ -15,6 +15,20 @@ one as a flake input. Deploy happens from the private repo, not here.
|
||||
- secrets (keys, tokens, passwords) → **neither** repo; runtime files or a secrets manager
|
||||
decrypting to a path. See the SSH section.
|
||||
|
||||
## Overrides = options (the consumer contract)
|
||||
|
||||
Private imports these modules **read-only** (flake input) — it can't patch a package list
|
||||
or a hard-coded value in place. So anything a consumer might reasonably want to change is
|
||||
exposed as an **option with a default**, never baked into config. That is what makes this
|
||||
repo a *baseline others extend* rather than a fork-and-patch.
|
||||
|
||||
Pattern (see `modules/texlive.nix`): option-bearing config lives in its own sub-module
|
||||
under the `local.*` namespace, `imports`-ed by `home.nix`. A module that declares `options`
|
||||
must use the split `{ options = …; config = …; }` form, so isolating it per file keeps
|
||||
`home.nix` bare. Override is then one line, e.g.
|
||||
`local.texlive.package = pkgs.texlive.withPackages (ps: [ ps.scheme-full ]);`. A plain value
|
||||
with nothing to vary goes straight in the bare package list — no option, don't over-abstract.
|
||||
|
||||
## Maxim: keep $HOME clean (XDG)
|
||||
|
||||
Standing principle for every addition: minimise dotfiles/dirs in `$HOME`. Route a tool's
|
||||
|
||||
@@ -24,19 +24,63 @@ you can tweak.
|
||||
|
||||
## Import the modules into your own flake
|
||||
|
||||
Add this repo as a flake input and import its modules from your own configuration:
|
||||
Add this repo as an input and assemble your machines in a private flake. Below is a
|
||||
**realistic example layout**. Structurally only `flake.nix` is required — every other path
|
||||
is just one way to organize your config; rename, merge, inline, or drop them freely. The one
|
||||
bit of *content* a bootable machine can't skip is its `hardware-configuration.nix`
|
||||
(`nixos-generate-config` output); the `hosts/<host>/` directory is only where this example
|
||||
keeps it.
|
||||
|
||||
```
|
||||
your-inventory/
|
||||
├── flake.nix # inputs (pub + follows) + nixosConfigurations.<host>
|
||||
├── flake.lock
|
||||
├── system/ # system-level modules
|
||||
│ ├── regional.nix # time zone / keymap / locale (see Overriding)
|
||||
│ ├── packages.nix # extra system packages
|
||||
│ └── tailscale.nix # VPN / network topology
|
||||
├── home/ # home-manager modules, one file per concern
|
||||
│ ├── identity.nix # git identity, name/email
|
||||
│ ├── ssh.nix # SSH hosts (topology — stays private)
|
||||
│ ├── syncthing.nix # Syncthing peers
|
||||
│ └── … # personal apps: browser, editor, chat, …
|
||||
└── hosts/
|
||||
└── myhost/
|
||||
├── default.nix # hostname, user account, host-specific system bits
|
||||
└── hardware-configuration.nix # `nixos-generate-config` output
|
||||
```
|
||||
|
||||
The `flake.nix` that wires it together:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs.config.url = "github:<owner>/<repo>";
|
||||
# …
|
||||
outputs = { nixpkgs, config, home-manager, ... }: {
|
||||
inputs = {
|
||||
pub.url = "github:<owner>/<repo>";
|
||||
# Reuse the public flake's pins so versions never drift between the two.
|
||||
nixpkgs.follows = "pub/nixpkgs";
|
||||
home-manager.follows = "pub/home-manager";
|
||||
};
|
||||
|
||||
outputs = inputs@{ self, nixpkgs, pub, home-manager, ... }: {
|
||||
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
config.nixosModules.default
|
||||
./hosts/myhost # your hardware + hostname
|
||||
pub.nixosModules.default # generic system baseline
|
||||
./system/regional.nix # your regional values (see below)
|
||||
./hosts/myhost # hardware + hostname
|
||||
home-manager.nixosModules.home-manager
|
||||
{ home-manager.users.me.imports = [ config.homeModules.default ]; }
|
||||
{
|
||||
home-manager.useGlobalPkgs = true;
|
||||
home-manager.useUserPackages = true;
|
||||
# Inject your own inputs into your home modules; the public ones ignore them.
|
||||
home-manager.extraSpecialArgs = { inherit inputs; };
|
||||
home-manager.users.me.imports = [
|
||||
pub.homeModules.default # generic home baseline
|
||||
./home/identity.nix # your git identity, name/email, …
|
||||
./home/ssh.nix # your SSH hosts (topology stays private)
|
||||
# … more personal home modules
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
@@ -46,6 +90,30 @@ Add this repo as a flake input and import its modules from your own configuratio
|
||||
Your machine list, hostnames, and any private specifics stay in *your* flake — you pull
|
||||
only the generic modules from here.
|
||||
|
||||
## Overriding the baseline
|
||||
|
||||
The public modules are a baseline you extend without forking. Three ways to shape them
|
||||
from your private flake, in rough order of how often you reach for them:
|
||||
|
||||
1. **Neutral default → your real value.** Region-agnostic settings ship as `mkDefault`
|
||||
placeholders (`console.keyMap = "us"`, `time.timeZone = "UTC"`, `en_US` locale). Set the
|
||||
real ones in a private module — no `mkForce` needed:
|
||||
|
||||
```nix
|
||||
# system/regional.nix
|
||||
{ time.timeZone = "Europe/Berlin"; console.keyMap = "de"; }
|
||||
```
|
||||
|
||||
2. **`local.*` options.** A module that exposes a knob puts it under the `local.*`
|
||||
namespace with a default. Override in one line, e.g. a fuller TeX Live:
|
||||
|
||||
```nix
|
||||
local.texlive.package = pkgs.texlive.withPackages (ps: [ ps.scheme-full ]);
|
||||
```
|
||||
|
||||
3. **Extra modules.** Anything with no public counterpart is just another module in the
|
||||
import lists above — personal apps, SSH/Syncthing topology, secrets wiring.
|
||||
|
||||
## Architecture: three layers
|
||||
|
||||
Kept apart on purpose. When adding config, decide which layer it belongs to:
|
||||
@@ -71,3 +139,7 @@ hosts/example/ # the template host (placeholder hardware-configuration.nix
|
||||
```
|
||||
|
||||
Shared `*.nix` / `*.lock` live at the top level; per-host files under `hosts/<host>/`.
|
||||
|
||||
Chasing a shutdown/boot hang? `modules/shutdown-debug.nix` exposes `local.shutdownDebug.*`
|
||||
(verbose systemd logging to kmsg, a tty9 debug shell, a shortened stop-timeout) — see its
|
||||
header comment for how to read the evidence.
|
||||
|
||||
Generated
+60
@@ -1,5 +1,26 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": [
|
||||
"nixvim",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1778716662,
|
||||
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
@@ -37,10 +58,34 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixvim": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1782919967,
|
||||
"narHash": "sha256-pRwjfB5HQJ3m8J8bOR43pPHtHI7VUJSqwLA3P06cOY0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixvim",
|
||||
"rev": "667c8471f4a0fb24d702d1a61af8609f1a5f1ba6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "nixos-26.05",
|
||||
"repo": "nixvim",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"home-manager": "home-manager",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixvim": "nixvim",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
@@ -63,6 +108,21 @@
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -17,9 +17,20 @@
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# nixvim: declarative Neovim as home-manager options — the whole editor
|
||||
# (plugins, LSP, keymaps) lives in modules/nvim.nix, pinned by this flake's
|
||||
# lock. Track the release branch matching nixpkgs (a mismatched branch fails
|
||||
# with `vimPlugins.<name> not found`). `follows` keeps one nixpkgs: both are
|
||||
# on the same stable channel, so the version skew nixvim warns about (only an
|
||||
# issue against unstable) does not arise here.
|
||||
nixvim = {
|
||||
url = "github:nix-community/nixvim/nixos-26.05";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs@{ self, nixpkgs, home-manager, rust-overlay, ... }:
|
||||
outputs = inputs@{ self, nixpkgs, home-manager, rust-overlay, nixvim, ... }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
in {
|
||||
@@ -28,7 +39,11 @@
|
||||
# imports = [ config.nixosModules.default ];
|
||||
# home-manager.users.<you>.imports = [ config.homeModules.default ];
|
||||
nixosModules.default = import ./modules/nixos.nix;
|
||||
homeModules.default = import ./modules/home.nix;
|
||||
# nixvim's home-manager module rides along so `modules/nvim.nix` can set
|
||||
# `programs.nixvim.*`; consumers get the editor by importing this one module.
|
||||
homeModules.default = {
|
||||
imports = [ nixvim.homeModules.nixvim ./modules/home.nix ];
|
||||
};
|
||||
|
||||
# ---- The example / template host -----------------------------------
|
||||
# Real and buildable; dogfoods the modules above. Clone, replace
|
||||
|
||||
@@ -14,4 +14,17 @@
|
||||
name = "Example User";
|
||||
email = "user@example.com";
|
||||
};
|
||||
|
||||
# Optional: give workspaces a home monitor (see modules/hyprland-workspaces.nix).
|
||||
# Bind by connector name (`hyprctl monitors`), not panel identity, so any
|
||||
# monitor on that port inherits the rules. `default:true` sets what a screen
|
||||
# shows on connect. Absent monitors fall back, so a single-screen session still
|
||||
# works. Left unset here — uncomment and adjust to your outputs.
|
||||
#
|
||||
# local.hyprland.workspaceRules = [
|
||||
# "1, monitor:HDMI-A-1, default:true" # external, workspaces 1-5
|
||||
# "2, monitor:HDMI-A-1"
|
||||
# "6, monitor:LVDS-1, default:true" # laptop, workspaces 6-9
|
||||
# "7, monitor:LVDS-1"
|
||||
# ];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
[Midnight-Commander]
|
||||
verbose=true
|
||||
shell_patterns=true
|
||||
auto_save_setup=false
|
||||
preallocate_space=false
|
||||
auto_menu=false
|
||||
use_internal_view=true
|
||||
use_internal_edit=false
|
||||
clear_before_exec=true
|
||||
confirm_delete=true
|
||||
confirm_overwrite=true
|
||||
confirm_execute=false
|
||||
confirm_history_cleanup=true
|
||||
confirm_exit=false
|
||||
confirm_directory_hotlist_delete=false
|
||||
confirm_view_dir=false
|
||||
safe_delete=false
|
||||
safe_overwrite=false
|
||||
use_8th_bit_as_meta=false
|
||||
mouse_move_pages_viewer=true
|
||||
mouse_close_dialog=false
|
||||
fast_refresh=false
|
||||
drop_menus=false
|
||||
wrap_mode=false
|
||||
old_esc_mode=true
|
||||
cd_symlinks=true
|
||||
show_all_if_ambiguous=true
|
||||
use_file_to_guess_type=true
|
||||
alternate_plus_minus=false
|
||||
only_leading_plus_minus=true
|
||||
show_output_starts_shell=false
|
||||
xtree_mode=false
|
||||
file_op_compute_totals=true
|
||||
classic_progressbar=true
|
||||
use_netrc=true
|
||||
ftpfs_always_use_proxy=false
|
||||
ftpfs_use_passive_connections=true
|
||||
ftpfs_use_passive_connections_over_proxy=false
|
||||
ftpfs_use_unix_list_options=true
|
||||
ftpfs_first_cd_then_ls=true
|
||||
ignore_ftp_chattr_errors=true
|
||||
editor_fill_tabs_with_spaces=false
|
||||
editor_return_does_auto_indent=true
|
||||
editor_backspace_through_tabs=false
|
||||
editor_fake_half_tabs=true
|
||||
editor_option_save_position=true
|
||||
editor_option_auto_para_formatting=false
|
||||
editor_option_typewriter_wrap=false
|
||||
editor_edit_confirm_save=true
|
||||
editor_syntax_highlighting=true
|
||||
editor_persistent_selections=true
|
||||
editor_drop_selection_on_copy=true
|
||||
editor_cursor_beyond_eol=false
|
||||
editor_cursor_after_inserted_block=false
|
||||
editor_visible_tabs=true
|
||||
editor_visible_spaces=true
|
||||
editor_line_state=false
|
||||
editor_simple_statusbar=false
|
||||
editor_check_new_line=false
|
||||
editor_show_right_margin=false
|
||||
editor_group_undo=false
|
||||
editor_state_full_filename=false
|
||||
editor_ask_filename_before_edit=false
|
||||
nice_rotating_dash=true
|
||||
mcview_remember_file_position=false
|
||||
auto_fill_mkdir_name=true
|
||||
copymove_persistent_attr=true
|
||||
pause_after_run=1
|
||||
mouse_repeat_rate=100
|
||||
double_click_speed=250
|
||||
old_esc_mode_timeout=1000000
|
||||
max_dirt_limit=10
|
||||
num_history_items_recorded=60
|
||||
vfs_timeout=60
|
||||
ftpfs_directory_timeout=900
|
||||
ftpfs_retry_seconds=30
|
||||
fish_directory_timeout=900
|
||||
editor_tab_spacing=8
|
||||
editor_word_wrap_line_length=72
|
||||
editor_option_save_mode=0
|
||||
editor_backup_extension=~
|
||||
editor_filesize_threshold=64M
|
||||
editor_stop_format_chars=-+*\\,.;:&>
|
||||
mcview_eof=
|
||||
skin=yadt256-defbg
|
||||
|
||||
shadows=true
|
||||
|
||||
[Layout]
|
||||
output_lines=0
|
||||
left_panel_size=115
|
||||
top_panel_size=0
|
||||
message_visible=true
|
||||
keybar_visible=true
|
||||
xterm_title=true
|
||||
command_prompt=true
|
||||
menubar_visible=true
|
||||
free_space=true
|
||||
horizontal_split=false
|
||||
vertical_equal=true
|
||||
horizontal_equal=true
|
||||
|
||||
[Misc]
|
||||
timeformat_recent=%d. %b %H:%M
|
||||
timeformat_old=%d. %b %Y
|
||||
ftp_proxy_host=gate
|
||||
ftpfs_password=anonymous@
|
||||
display_codepage=UTF-8
|
||||
source_codepage=Other_8_bit
|
||||
autodetect_codeset=
|
||||
clipboard_store=
|
||||
clipboard_paste=
|
||||
|
||||
[Colors]
|
||||
base_color=
|
||||
st-256color=
|
||||
color_terminals=
|
||||
|
||||
[Panels]
|
||||
show_mini_info=true
|
||||
kilobyte_si=true
|
||||
mix_all_files=false
|
||||
show_backups=true
|
||||
show_dot_files=true
|
||||
fast_reload=false
|
||||
fast_reload_msg_shown=false
|
||||
mark_moves_down=true
|
||||
reverse_files_only=true
|
||||
auto_save_setup_panels=false
|
||||
navigate_with_arrows=true
|
||||
panel_scroll_pages=true
|
||||
panel_scroll_center=false
|
||||
mouse_move_pages=true
|
||||
filetype_mode=true
|
||||
permission_mode=false
|
||||
torben_fj_mode=false
|
||||
quick_search_mode=2
|
||||
select_flags=6
|
||||
|
||||
simple_swap=false
|
||||
|
||||
[FindFile]
|
||||
file_case_sens=true
|
||||
file_shell_pattern=true
|
||||
file_find_recurs=true
|
||||
file_skip_hidden=false
|
||||
file_all_charsets=false
|
||||
content_case_sens=true
|
||||
content_regexp=false
|
||||
content_first_hit=false
|
||||
content_whole_words=false
|
||||
content_all_charsets=false
|
||||
ignore_dirs_enable=true
|
||||
ignore_dirs=
|
||||
|
||||
[Panelize]
|
||||
Geänderte Git-Dateien=git ls-files --modified
|
||||
Nach dem Patchen nach Ablehnungen suchen=find . -name \\*.rej -print
|
||||
Suche SUID- und SGID-Programme=find . \\( \\( -perm -04000 -a -perm /011 \\) -o \\( -perm -02000 -a -perm /01 \\) \\) -print
|
||||
Suche nach *.orig nach dem Patchen=find . -name \\*.orig -print
|
||||
@@ -0,0 +1,86 @@
|
||||
# Midnight Commander file associations (public baseline).
|
||||
#
|
||||
# mc reads a SINGLE mc.ext.ini; this user copy fully replaces the system one
|
||||
# (they are not merged), so it must also carry the archive entries we keep.
|
||||
#
|
||||
# Policy: Enter (Open) on an archive mounts it as a virtual filesystem (mc
|
||||
# built-in, no external helper). Every other file falls through to [Default],
|
||||
# which runs `xdg-open` -- so the app is whatever the XDG mimeapps default
|
||||
# resolves to (set those in the private inventory's xdg.mimeApps). One source of
|
||||
# truth, shared with every other launcher; nothing app-specific baked in here.
|
||||
#
|
||||
# Note: office documents (.docx/.odt/.xlsx/...) are ZIP containers, so they are
|
||||
# deliberately absent from the [zip] entry below -- otherwise Enter would mount
|
||||
# them as archives instead of handing them to LibreOffice via xdg-open.
|
||||
#
|
||||
# Override contract (this is a types.lines option shipped as mkBefore): append a
|
||||
# same-named section to override one key -- mc merges same-named sections and
|
||||
# the last key wins -- e.g. add a [Default] with a different Open to change the
|
||||
# fallback, or add [pdf] Type=^PDF / Open=mupdf %f & to force one type to a
|
||||
# specific app. lib.mkForce "..." replaces this baseline wholesale.
|
||||
|
||||
[mc.ext.ini]
|
||||
Version=4.0
|
||||
|
||||
### Archives: Enter mounts them as a virtual filesystem (F3/F4 unaffected) ###
|
||||
|
||||
[tar]
|
||||
Regex=\\.(tar(\\.(gz|bz2|xz|zst|lz|lz4|lzo|Z))?|t(gz|az|bz2?|xz|zst|lz))$
|
||||
Open=%cd %p/utar://
|
||||
|
||||
[zip]
|
||||
Regex=\\.(zip|jar|xpi|whl|egg|apk|war|ear)$
|
||||
Open=%cd %p/uzip://
|
||||
|
||||
[7z]
|
||||
Shell=.7z
|
||||
Open=%cd %p/u7z://
|
||||
|
||||
[rar]
|
||||
Regex=\\.r(ar|[0-9][0-9])$
|
||||
Open=%cd %p/urar://
|
||||
|
||||
[arj]
|
||||
Regex=\\.a(rj|[0-9][0-9])$
|
||||
Open=%cd %p/uarj://
|
||||
|
||||
[cab]
|
||||
Shell=.cab
|
||||
Open=%cd %p/ucab://
|
||||
|
||||
[cpio]
|
||||
Regex=\\.cpio(\\.(gz|xz|bz2|zst|lz|lz4|lzo|Z))?$
|
||||
Open=%cd %p/ucpio://
|
||||
|
||||
[deb]
|
||||
Regex=\\.u?deb$
|
||||
Open=%cd %p/deb://
|
||||
|
||||
[rpm]
|
||||
Regex=\\.(rpm|src\\.rpm|spm)$
|
||||
Open=%cd %p/rpm://
|
||||
|
||||
[iso]
|
||||
Shell=.iso
|
||||
Open=%cd %p/iso9660://
|
||||
|
||||
[patch]
|
||||
Regex=\\.(diff|patch)(\\.(gz|bz2|xz|zst|Z))?$
|
||||
Open=%cd %p/patchfs://
|
||||
|
||||
### Everything else: hand off to the desktop's configured default app ###
|
||||
|
||||
# `&` backgrounds xdg-open so mc's shell returns and the launched GUI is orphaned
|
||||
# to init. Hyprland only swallows the terminal when the new window's parent-PID
|
||||
# chain still has a foot ancestor (getSwallower in Hyprland's Window.cpp), so an
|
||||
# orphaned app opens as its own tile instead of swallowing mc (zathura, imv, ...).
|
||||
# %f MUST stay unquoted: mc already backslash-escapes the path, so quoting turns
|
||||
# those backslashes literal and breaks filenames with spaces.
|
||||
# Redirect stdout/stderr to /dev/null: `&` backgrounds but leaves the app's fds on
|
||||
# mc's terminal, so a chatty app paints over the mc pane and looks like a broken
|
||||
# swallow. Tradeoff: this also discards any real diagnostics when an app fails to
|
||||
# open a file. To keep a breadcrumb, swap /dev/null for a log file, e.g.
|
||||
# `>>$XDG_STATE_HOME/mc-open.log 2>&1`.
|
||||
|
||||
[Default]
|
||||
Open=xdg-open %f >/dev/null 2>&1 &
|
||||
@@ -0,0 +1,30 @@
|
||||
shell_patterns=0
|
||||
|
||||
# F2 user menu. Format macros: %f current file · %d current dir · %s tagged
|
||||
# files else current · %t tagged files · %D other panel's dir. Full list: mc(1).
|
||||
|
||||
c Pack selected into an archive (ouch — format from the name's extension)
|
||||
NAME=%{Archive name, e.g. stuff.tar.zst}
|
||||
ouch compress %s "$NAME"
|
||||
|
||||
x Unpack archive here (ouch)
|
||||
ouch decompress %f
|
||||
|
||||
i Open file with image viewer (imv)
|
||||
imv %f
|
||||
|
||||
I Open current directory with image viewer (imv)
|
||||
imv %d
|
||||
|
||||
p Open file with document viewer (zathura)
|
||||
zathura %f
|
||||
|
||||
v Play file with mpv
|
||||
mpv %f
|
||||
|
||||
V Play current directory with mpv
|
||||
mpv %d
|
||||
|
||||
m View a man page
|
||||
MAN=%{Enter manual name}
|
||||
%view{ascii,nroff} MANROFFOPT='-c -Tlatin1' MAN_KEEP_FORMATTING=1 man -P cat "$MAN"
|
||||
+411
-28
@@ -3,15 +3,130 @@
|
||||
# user.name/email), host topology (SSH hosts, Syncthing peers) and personal app
|
||||
# choices (grayjay, Claude Code, …) live in the inventory, not in this module.
|
||||
|
||||
{ config, pkgs, ... }:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
terminal = "foot";
|
||||
browser = "librewolf";
|
||||
editor = "nvim";
|
||||
|
||||
# SSH_ASKPASS helper that drives the GPG pinentry-qt: ssh passphrase/PIN
|
||||
# prompts reuse the same Qt dialog as commit signing, adding no extra GUI
|
||||
# toolkit (a packaged askpass would drag in KDE/GTK — hundreds of MB).
|
||||
# ssh signals the prompt kind via $SSH_ASKPASS_PROMPT (OpenSSH ≥ 8.4):
|
||||
# empty real passphrase/PIN entry — collect input, echo to stdout
|
||||
# confirm/none informational, e.g. "Confirm user presence" (touch request)
|
||||
# A FIDO touch can't be faked by software, so those messages need no dialog:
|
||||
# succeed silently instead of popping a spurious PIN box (the token blinks).
|
||||
# pinentry runs under `systemd-run --user`, not directly, so it is reparented
|
||||
# onto the user service manager instead of this askpass shell. ssh spawns the
|
||||
# askpass from inside the foot terminal, and Hyprland's window swallowing walks
|
||||
# a new window's parent-PID chain for a `foot` ancestor (swallow_regex) — a
|
||||
# direct pinentry would match and hide the terminal behind the PIN dialog.
|
||||
# Detaching breaks that chain (parent becomes systemd), exactly like the
|
||||
# commit-signing pinentry that gpg-agent already spawns off-tree. --pipe wires
|
||||
# stdin/stdout through so the Assuan exchange still works; --wait propagates
|
||||
# the exit status; --collect reaps the transient unit.
|
||||
sshAskpass = pkgs.writeShellScript "ssh-askpass-pinentry" ''
|
||||
[ -n "''${SSH_ASKPASS_PROMPT:-}" ] && exit 0
|
||||
desc=$(printf '%s' "''${1:-Passphrase:}" | sed 's/%/%25/g')
|
||||
printf 'SETTITLE ssh\nSETPROMPT PIN:\nSETDESC %s\nGETPIN\nBYE\n' "$desc" \
|
||||
| ${pkgs.systemd}/bin/systemd-run --user --pipe --wait --collect --quiet \
|
||||
${pkgs.pinentry-qt}/bin/pinentry-qt \
|
||||
| sed -n 's/^D //p'
|
||||
'';
|
||||
|
||||
# Screenshot helper for the Print-key binds below. Two axes, chosen by args:
|
||||
# scope ($1 = full | region, the latter an interactive slurp selection) and
|
||||
# destination ($2 = file | clip). Files are timestamped PNGs under
|
||||
# ~/Pictures/Screenshots; clip copies to the Wayland clipboard. A mako toast
|
||||
# confirms each grab. slurp exits non-zero on Escape, which aborts here.
|
||||
screenshot = pkgs.writeShellScript "screenshot" ''
|
||||
set -eu
|
||||
if [ "$1" = region ]; then
|
||||
geom=$(${pkgs.slurp}/bin/slurp) || exit 1
|
||||
grab() { ${pkgs.grim}/bin/grim -g "$geom" "$@"; }
|
||||
else
|
||||
grab() { ${pkgs.grim}/bin/grim "$@"; }
|
||||
fi
|
||||
if [ "$2" = clip ]; then
|
||||
grab - | ${pkgs.wl-clipboard}/bin/wl-copy
|
||||
${pkgs.libnotify}/bin/notify-send "Screenshot" "Copied to clipboard"
|
||||
else
|
||||
dir="${config.xdg.userDirs.pictures}/Screenshots"
|
||||
mkdir -p "$dir"
|
||||
file="$dir/$(date +%Y-%m-%d-%H%M%S).png"
|
||||
grab "$file"
|
||||
${pkgs.libnotify}/bin/notify-send "Screenshot" "Saved $file"
|
||||
fi
|
||||
'';
|
||||
|
||||
# Session/power menu: a fuzzel --dmenu picker (not app-list entries), bound to
|
||||
# $mod+Backspace. --index returns the chosen row number, so dispatch matches on
|
||||
# position, not label text. Icons use the Papirus names via fuzzel's dmenu
|
||||
# icon protocol (NUL + 0x1f after each label). Escape/no pick → fuzzel exits
|
||||
# non-zero, command substitution is empty, case falls through → no-op.
|
||||
#
|
||||
# Icon names deliberately picked from Papirus's apps/ (colored) set that have
|
||||
# NO actions/ variant: system-{reboot,shutdown,log-out} exist in both, and at
|
||||
# dmenu line-height fuzzel resolves the monochrome actions/ copy. gnome-logout
|
||||
# / system-restart / gnome-session-halt are apps-only, so they stay colored.
|
||||
powerMenu = pkgs.writeShellScript "power-menu" ''
|
||||
case $(printf 'Log Out\0icon\x1fgnome-logout\nReboot\0icon\x1fsystem-restart\nShut Down\0icon\x1fgnome-session-halt\n' \
|
||||
| ${pkgs.fuzzel}/bin/fuzzel --dmenu --index --prompt 'power ') in
|
||||
0) ${config.wayland.windowManager.hyprland.finalPackage}/bin/hyprctl dispatch exit ;;
|
||||
1) ${pkgs.systemd}/bin/systemctl reboot ;;
|
||||
2) ${pkgs.systemd}/bin/systemctl poweroff ;;
|
||||
esac
|
||||
'';
|
||||
|
||||
# Swap the physical left/right placement of a two-monitor setup at runtime.
|
||||
# Reflows the pair adjacent from x=0 (left one at origin, right one butted
|
||||
# against its logical width) after reversing their order — so it stays
|
||||
# gap-free regardless of differing widths, unlike a naive x-origin exchange
|
||||
# (equal widths only). Machine-agnostic: reads whichever two connectors are
|
||||
# live from `hyprctl -j`, no connector names. Ephemeral (hyprctl keyword) —
|
||||
# the per-host default layout is restored on the next reload/relogin. No-op
|
||||
# unless exactly two monitors are connected.
|
||||
#
|
||||
# Hyprland doesn't reliably re-notify layer-shell clients when an output is
|
||||
# reconfigured at runtime (a known limitation — same class as the mirror/unplug
|
||||
# cases where waybar needs a restart). Observed here: after the move, waybar and
|
||||
# the wallpaper keep drawing at the pre-swap offsets, so the moved monitor's
|
||||
# bar/bg land off-screen. Re-init both against the new geometry: waybar rebuilds
|
||||
# its bars on SIGUSR2, and awww only paints outputs present when `awww img` last
|
||||
# ran, so re-push the wallpaper symlink. Both are idempotent — harmless when a
|
||||
# surface happened not to lag (which surface stales seems to race).
|
||||
swapMonitors = pkgs.writeShellScript "hypr-swap-monitors" ''
|
||||
set -eu
|
||||
hc=${config.wayland.windowManager.hyprland.finalPackage}/bin/hyprctl
|
||||
jq=${pkgs.jq}/bin/jq
|
||||
data=$("$hc" monitors -j)
|
||||
n=$(printf '%s' "$data" | "$jq" length)
|
||||
if [ "$n" -ne 2 ]; then
|
||||
${pkgs.libnotify}/bin/notify-send "Monitor swap" "need exactly 2 monitors ($n connected)"
|
||||
exit 0
|
||||
fi
|
||||
# sort by current x → [left, right]; the new left is the old right.
|
||||
cmd=$(printf '%s' "$data" | "$jq" -r '
|
||||
sort_by(.x)
|
||||
| .[1] as $l | .[0] as $r
|
||||
| (($l.width / $l.scale) | floor) as $off
|
||||
| "keyword monitor \($l.name),preferred,0x0,\($l.scale)"
|
||||
+ " ; keyword monitor \($r.name),preferred,\($off)x0,\($r.scale)"')
|
||||
"$hc" --batch "$cmd"
|
||||
# re-anchor the layer-shell clients onto the new geometry (see comment above)
|
||||
${pkgs.procps}/bin/pkill -USR2 -x .waybar-wrapped || true
|
||||
bg="$HOME/.local/share/bg"
|
||||
[ -e "$bg" ] && ${pkgs.awww}/bin/awww img "$bg" || true
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports = [ ./gpg.nix ./media.nix ];
|
||||
imports = [ ./gpg.nix ./media.nix ./texlive.nix ./librewolf.nix ./mc.nix ./mime.nix ./hyprland-workspaces.nix ./nvim.nix ];
|
||||
|
||||
# Put ~/.local/bin (the XDG user-bin dir) on PATH so hand-dropped scripts run
|
||||
# by name without wiring each through nix.
|
||||
home.sessionPath = [ "${config.home.homeDirectory}/.local/bin" ];
|
||||
|
||||
home.sessionVariables = {
|
||||
TERMINAL = terminal;
|
||||
@@ -25,6 +140,10 @@ in
|
||||
PYTHONIOENCODING = "UTF-8";
|
||||
PYTHONSTARTUP = "${config.xdg.configHome}/python/startup";
|
||||
PYTHON_HISTFILE = "${config.xdg.cacheHome}/python/history";
|
||||
# Default interpreter uv picks for `uv run`/`uv venv` without an explicit
|
||||
# -p. Pins throwaway REPLs and new projects to one version across machines
|
||||
# (further steered by uv.toml below). Fetched on demand by uv, not Nix.
|
||||
UV_PYTHON = "3.14";
|
||||
# psql / wget / readline configs, XDG-routed (the rc files are declared
|
||||
# via xdg.configFile below).
|
||||
PSQLRC = "${config.xdg.configHome}/psql/rc";
|
||||
@@ -36,30 +155,69 @@ in
|
||||
# Mute the harmless "Wayland does not support QWindow::requestActivate()"
|
||||
# spam Qt apps (keepassxc) print; scoped to that one log category only.
|
||||
QT_LOGGING_RULES = "qt.qpa.wayland=false";
|
||||
# GUI helper for ssh passphrase/PIN prompts. Not forced: with a tty ssh still
|
||||
# prompts inline; the helper is only used when a caller sets
|
||||
# SSH_ASKPASS_REQUIRE (e.g. a headless/agent context).
|
||||
SSH_ASKPASS = "${pkgs.ksshaskpass}/bin/ksshaskpass";
|
||||
# Run Chromium/Electron apps (brave, signal, grayjay's webview) natively on
|
||||
# Wayland instead of XWayland: nixpkgs' chromium/electron wrappers read this
|
||||
# and inject the Ozone Wayland flags. This is a Wayland-only (Hyprland)
|
||||
# session, so XWayland is pure downside (blurry fractional scaling, extra
|
||||
# hop). Override per-app with --ozone-platform=x11 if one misbehaves.
|
||||
NIXOS_OZONE_WL = "1";
|
||||
# GUI helper for ssh passphrase/PIN prompts (see sshAskpass above). Not
|
||||
# forced: with a tty ssh still prompts inline; the helper is used only when a
|
||||
# caller sets SSH_ASKPASS_REQUIRE (e.g. a headless/agent context).
|
||||
SSH_ASKPASS = "${sshAskpass}";
|
||||
};
|
||||
# Sets XDG_{CONFIG,DATA,CACHE}_HOME and enables xdg.configFile management.
|
||||
xdg.enable = true;
|
||||
|
||||
home.packages = with pkgs; [
|
||||
librewolf
|
||||
# librewolf is installed by ./librewolf.nix (via local.librewolf.package) so
|
||||
# the wrapped package stays overridable — do NOT also list it here.
|
||||
keepassxc # config in ~/.config/keepassxc
|
||||
papirus-icon-theme # fuzzel icon-theme; ships non-symbolic power icons
|
||||
# (Adwaita only has them as symbolic, which fuzzel skips)
|
||||
# claude-code is installed by programs.claude-code below (its wrapped
|
||||
# finalPackage) — do NOT also list it here, or buildEnv conflicts on the
|
||||
# wrapper binary.
|
||||
nodejs
|
||||
uv # Python: versions, venvs, deps, lockfile. Runs uv-managed
|
||||
# standalone CPython via nix-ld (see modules/nixos.nix).
|
||||
htop # process viewer; config in ~/.config/htop (XDG-native)
|
||||
highlight # `ccat` alias: syntax-highlighted cat
|
||||
file # identify file type by content
|
||||
ripgrep # `rg`: fast recursive grep
|
||||
fd # fast, ergonomic find
|
||||
tree # recursive directory listing
|
||||
unzip # extract .zip (tools that shell out to it expect the binary)
|
||||
jq # JSON on the command line (scripts, ad-hoc inspection)
|
||||
pv # pipe progress meter (throughput, ETA on streamed data)
|
||||
libreoffice
|
||||
ksshaskpass # Qt GUI prompt for ssh passphrase/PIN (see SSH_ASKPASS above)
|
||||
grim # screenshot capture (also used by the Print-key binds)
|
||||
slurp # interactive region selector for grim
|
||||
brightnessctl # backlight control for the XF86MonBrightness binds; writes
|
||||
# via the active logind session, so no udev rule needed
|
||||
];
|
||||
|
||||
# Canonical XDG user dirs. Only Pictures is actually used (screenshot target,
|
||||
# file-picker default). createDirectories stays off so the rest aren't
|
||||
# littered into $HOME — they're merely declared in ~/.config/user-dirs.dirs,
|
||||
# created lazily by whatever app wants them.
|
||||
xdg.userDirs = {
|
||||
enable = true;
|
||||
createDirectories = false;
|
||||
pictures = "${config.home.homeDirectory}/Pictures";
|
||||
};
|
||||
|
||||
# ---- Per-tool config files, routed into XDG dirs ---------------------
|
||||
xdg.configFile = {
|
||||
"python/startup".source = ./assets/python-startup.py;
|
||||
|
||||
# uv global config. "managed" = always use uv's own CPython, never a
|
||||
# system/Nix python — keeps behaviour reproducible regardless of what else
|
||||
# is on PATH. Default version comes from UV_PYTHON above.
|
||||
"uv/uv.toml".text = ''
|
||||
python-preference = "managed"
|
||||
'';
|
||||
|
||||
# psql: unlimited, per-database history file (kept out of $HOME).
|
||||
"psql/rc".text = ''
|
||||
\set HISTFILE ${config.xdg.cacheHome}/psql/history-:DBNAME
|
||||
@@ -87,6 +245,14 @@ in
|
||||
|
||||
programs.home-manager.enable = true;
|
||||
|
||||
# Document converter (md/html/docx/…). PDF output shells out to a LaTeX engine
|
||||
# (texlive.nix): default it to xelatex, which is Unicode-native — pdflatex
|
||||
# (pandoc's own default) fails on combining marks, e.g. U+0308 in decomposed ä.
|
||||
programs.pandoc = {
|
||||
enable = true;
|
||||
defaults.pdf-engine = "xelatex";
|
||||
};
|
||||
|
||||
programs.git = {
|
||||
enable = true;
|
||||
settings = {
|
||||
@@ -104,10 +270,7 @@ in
|
||||
merge.log = true;
|
||||
push = { default = "simple"; followTags = true; };
|
||||
submodule.recurse = true;
|
||||
|
||||
# github: / gh: / gist: remote shorthands.
|
||||
url."git@github.com:".insteadOf = "gh:";
|
||||
url."git://github.com/".insteadOf = "github:";
|
||||
fetch.recurseSubmodules = "on-demand";
|
||||
|
||||
alias = {
|
||||
l = "log --pretty=oneline -n 20 --graph --abbrev-commit";
|
||||
@@ -147,7 +310,56 @@ in
|
||||
# so their stderr goes to the journal, not whatever terminal you're in.
|
||||
programs.fuzzel = {
|
||||
enable = true;
|
||||
settings.main.font = "JetBrainsMono Nerd Font:size=11";
|
||||
settings = {
|
||||
main.font = "JetBrainsMono Nerd Font:size=11";
|
||||
# Papirus for colored power icons in the $mod+Backspace dmenu (Adwaita
|
||||
# ships those only as symbolic, and fuzzel skips symbolic dirs). It
|
||||
# inherits hicolor, so app icons still resolve (recolored to Papirus).
|
||||
main.icon-theme = "Papirus";
|
||||
# Fuzzel ignores the portal color-scheme, and its built-in palette is
|
||||
# light — dark has to be set here. Matches waybar (#222/#bbb + One Dark
|
||||
# accents); flat, thin border, no rounding per the minimal look.
|
||||
border = { width = 1; radius = 0; };
|
||||
colors = {
|
||||
background = "222222f2";
|
||||
text = "bbbbbbff";
|
||||
match = "e5c07bff";
|
||||
selection = "333333ff";
|
||||
selection-text = "ffffffff";
|
||||
selection-match = "e5c07bff";
|
||||
border = "444444ff";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Curate what fuzzel's app-list mode shows. Fuzzel just enumerates XDG
|
||||
# `.desktop` files, so we shape that set here. Shadow a package's entry by
|
||||
# redefining its id with noDisplay = true: our ~/.local/share/applications
|
||||
# copy wins over the profile's. noDisplay only hides from launchers/menus —
|
||||
# the app stays wired up for xdg-open and "open with", so opening a file
|
||||
# *with* mpv/zathura is unaffected. The attrset merges with anything a private
|
||||
# host adds. noDisplay is mkDefault so a host can un-hide one (e.g. re-expose
|
||||
# mpv) with a plain override, no conflict. (Power/session actions are not
|
||||
# entries here — see the $mod+Backspace dmenu power menu above.)
|
||||
xdg.desktopEntries = let
|
||||
hide = exec: {
|
||||
inherit exec;
|
||||
name = "hidden";
|
||||
noDisplay = lib.mkDefault true;
|
||||
};
|
||||
in {
|
||||
# foot's three entries: launched via $mod+Return, never from a menu.
|
||||
foot = hide "foot";
|
||||
foot-server = hide "foot --server";
|
||||
footclient = hide "footclient";
|
||||
# mpv/zathura are file handlers, not things to launch bare from a menu.
|
||||
mpv = hide "mpv %U";
|
||||
umpv = hide "umpv %U";
|
||||
"org.pwmt.zathura" = hide "zathura %U";
|
||||
"org.pwmt.zathura-djvu" = hide "zathura %U";
|
||||
"org.pwmt.zathura-cb" = hide "zathura %U";
|
||||
# nvim is $EDITOR / opened from a terminal; no place in a GUI launcher.
|
||||
nvim = hide "nvim %F";
|
||||
};
|
||||
|
||||
programs.fzf.enable = true; # provides the ^f "cd into fzf dir" binding
|
||||
@@ -192,10 +404,11 @@ in
|
||||
grep = "grep --color=auto";
|
||||
diff = "diff --color=auto";
|
||||
ccat = "highlight --out-format=ansi";
|
||||
ka = "killall";
|
||||
g = "git";
|
||||
e = editor;
|
||||
v = editor;
|
||||
# Throwaway REPL/scripts as if `python` were global; resolves to UV_PYTHON.
|
||||
python = "uv run python";
|
||||
};
|
||||
|
||||
# Interactive bits with no first-class HM option: prompt, autocd,
|
||||
@@ -265,6 +478,47 @@ in
|
||||
# Keep the GTK2 rc out of $HOME (default ~/.gtkrc-2.0). Setting this also
|
||||
# makes home-manager export GTK2_RC_FILES so GTK2 apps still find it.
|
||||
gtk2.configLocation = "${config.xdg.configHome}/gtk-2.0/gtkrc";
|
||||
# The theme *name* alone isn't enough: the qgtk3 Qt bridge and apps' dark
|
||||
# detection (e.g. keepassxc's "auto" theme) read this flag, not the name.
|
||||
# Set it so both GTK apps and the Qt-via-gtk3 apps resolve to dark.
|
||||
gtk3.extraConfig.gtk-application-prefer-dark-theme = true;
|
||||
gtk4.extraConfig.gtk-application-prefer-dark-theme = true;
|
||||
};
|
||||
|
||||
# The canonical dark/light signal on Wayland: apps query the xdg-desktop-portal
|
||||
# Settings interface for org.freedesktop.appearance/color-scheme over D-Bus, and
|
||||
# the gtk portal serves that from this dconf key. keepassxc's "auto" theme reads
|
||||
# it (NixUtils::isDarkMode) and ignores the GTK theme name / Qt style entirely,
|
||||
# so without this the portal reports 0 ("no preference") and keepass stays light.
|
||||
# prefer-dark makes the portal report dark to every color-scheme-aware app.
|
||||
dconf.settings."org/gnome/desktop/interface".color-scheme = "prefer-dark";
|
||||
|
||||
# Match the Qt apps (keepassxc, pinentry-qt, ssh askpass) to the GTK
|
||||
# Adwaita-dark above, else they render the default light Fusion style. Use the
|
||||
# gtk3 platform theme, not an adwaita *style* override: nixpkgs wraps Qt apps
|
||||
# with a baked QT_PLUGIN_PATH pointing only at their build deps, so an external
|
||||
# style plugin (adwaita-qt) is unreachable and QT_STYLE_OVERRIDE is silently
|
||||
# ignored. The gtk3 plugin (libqgtk3) ships inside qtbase itself, so it's
|
||||
# always on that path, and it reads the GTK theme (Adwaita-dark) directly.
|
||||
qt = {
|
||||
enable = true;
|
||||
platformTheme.name = "gtk3";
|
||||
};
|
||||
|
||||
# The home-manager Hyprland module below enables xdg.portal and pins the
|
||||
# portal search dir (NIX_XDG_DESKTOP_PORTAL_DIR) to this user profile, adding
|
||||
# only the Hyprland backend — which implements Screenshot/ScreenCast/
|
||||
# GlobalShortcuts but not FileChooser. Add the GTK portal here (system-level
|
||||
# extraPortals aren't in the pinned dir, so they're never seen) and route
|
||||
# FileChooser to it, else file dialogs in Chromium/Electron apps (signal,
|
||||
# brave) fail with a DBus "No such interface
|
||||
# org.freedesktop.portal.FileChooser" error.
|
||||
xdg.portal = {
|
||||
extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
|
||||
config.common = {
|
||||
default = [ "hyprland" "gtk" ];
|
||||
"org.freedesktop.impl.portal.FileChooser" = [ "gtk" ];
|
||||
};
|
||||
};
|
||||
|
||||
wayland.windowManager.hyprland = {
|
||||
@@ -272,7 +526,19 @@ in
|
||||
configType = "hyprlang";
|
||||
|
||||
settings = {
|
||||
monitor = ",preferred,auto,1";
|
||||
# Fallback for hosts that don't pin a layout; a two-monitor host overrides
|
||||
# this with explicit positions (see the private inventory). mkDefault so
|
||||
# that override is a plain value, not a mkForce fight.
|
||||
monitor = lib.mkDefault ",preferred,auto,1";
|
||||
|
||||
# The qt module sets QT_QPA_PLATFORMTHEME=gtk3 (→ Qt apps read the GTK
|
||||
# Adwaita-dark theme), but only into hm-session-vars.sh + the systemd user
|
||||
# env. gpg-agent (a user service) inherits it, so commit-signing pinentry
|
||||
# is dark — but Hyprland doesn't source that file, so its children (foot →
|
||||
# shell → ssh askpass → pinentry) launch without it and came up light.
|
||||
# Verified the single var flips pinentry-qt dark; export it here so every
|
||||
# session child gets it too.
|
||||
env = [ "QT_QPA_PLATFORMTHEME,gtk3" ];
|
||||
|
||||
general = {
|
||||
layout = "master";
|
||||
@@ -291,9 +557,47 @@ in
|
||||
enabled = false;
|
||||
};
|
||||
|
||||
# Suppress Hyprland's built-in default wallpaper. The wallpaper here is
|
||||
# driven by a userspace daemon (awww, see modules/media.nix) that only
|
||||
# starts — and pushes the chosen image — once the compositor is up. Until
|
||||
# then Hyprland paints its bundled splash, so on login it briefly shows
|
||||
# before the real wallpaper replaces it. Disabling it leaves the plain
|
||||
# background colour in that gap instead of a flash of unrelated art.
|
||||
misc = {
|
||||
force_default_wallpaper = 0;
|
||||
disable_hyprland_logo = true;
|
||||
# Window swallowing: a GUI spawned from a foot shell (e.g. zathura/imv
|
||||
# from mc) hides the terminal and takes its place, restoring it on close.
|
||||
# Matches on the *parent* terminal's class. Needs standalone foot: in
|
||||
# server/client mode all windows share one process, so PID ancestry can't
|
||||
# tell them apart and swallowing breaks — that's why $mod+Return runs
|
||||
# `foot`, not `footclient`. Swallow is decided by getSwallower() walking
|
||||
# the new window's parent-PID chain (up to 25 hops) for a window whose
|
||||
# class matches swallow_regex; swallow_exception_regex, if set, matches the
|
||||
# *terminal's title*, not the new window's class (a common misreading).
|
||||
#
|
||||
# Do NOT confuse swallowing with an app opening fullscreen/maximized — they
|
||||
# look identical (the launching terminal vanishes) but are different things:
|
||||
# - real swallow removes the terminal from the layout and restores it only
|
||||
# when the swallower closes; you cannot un-swallow a live window.
|
||||
# - a fullscreen/maximized app merely covers the workspace; the launcher
|
||||
# is still a tile underneath and $mod+f / $mod SHIFT f toggles it back.
|
||||
# `hyprctl clients` is decisive: a real swallow shows the terminal
|
||||
# hidden=true and the new window swallowing!=0x0. LibreOffice trips this —
|
||||
# it restores its own maximized state on open (from mc or a shell), so a
|
||||
# document appears to "swallow" mc when it is only fullscreen. And being
|
||||
# single-instance (one soffice.bin owns every window via IPC handoff), when
|
||||
# it genuinely does swallow, that is fixed by whatever launched its daemon,
|
||||
# not per-open — so it can't do the tile-from-mc / swallow-from-shell split
|
||||
# that single-process handlers like zathura/imv get for free.
|
||||
enable_swallow = true;
|
||||
swallow_regex = "^(foot)$";
|
||||
};
|
||||
|
||||
input = {
|
||||
kb_layout = "de";
|
||||
follow_mouse = 1;
|
||||
numlock_by_default = true;
|
||||
};
|
||||
|
||||
"$mod" = "SUPER";
|
||||
@@ -307,19 +611,30 @@ in
|
||||
];
|
||||
|
||||
bind = [
|
||||
"$mod, Return, exec, footclient"
|
||||
"$mod, Return, exec, foot"
|
||||
"$mod, q, killactive"
|
||||
"$mod SHIFT, q, exit"
|
||||
"$mod, f, fullscreen, 1"
|
||||
"$mod SHIFT, f, fullscreen, 0"
|
||||
# fullscreen arg: 0 = true fullscreen, 1 = maximize (keeps bar/gaps).
|
||||
"$mod, f, fullscreen, 0"
|
||||
"$mod SHIFT, f, fullscreen, 1"
|
||||
"$mod, w, exec, ${browser}"
|
||||
"$mod, d, exec, fuzzel"
|
||||
"$mod, Backspace, exec, ${powerMenu}"
|
||||
"$mod, j, layoutmsg, cyclenext"
|
||||
"$mod SHIFT, j, layoutmsg, swapnext"
|
||||
"$mod, k, layoutmsg, cycleprev"
|
||||
"$mod SHIFT, k, layoutmsg, swapprev"
|
||||
"$mod, h, layoutmsg, splitratio -0.05"
|
||||
"$mod, l, layoutmsg, splitratio +0.05"
|
||||
# swap focused window with the master (dwm-style "zoom").
|
||||
"$mod, space, layoutmsg, swapwithmaster"
|
||||
# master area width. Master layout uses `mfact`, not dwindle's `splitratio`.
|
||||
"$mod, h, layoutmsg, mfact -0.05"
|
||||
"$mod, l, layoutmsg, mfact +0.05"
|
||||
# Move focus / the current workspace to the other monitor ("o" = other).
|
||||
# With two monitors the relative +1 just toggles between them.
|
||||
"$mod, o, focusmonitor, +1"
|
||||
"$mod SHIFT, o, movecurrentworkspacetomonitor, +1"
|
||||
# Swap the two monitors' left/right placement (see swapMonitors above).
|
||||
"$mod CTRL, o, exec, ${swapMonitors}"
|
||||
] ++ (
|
||||
# Dynamic definitions for viewing workspaces 1 to 9 and moving windows between them.
|
||||
# References physical key codes for robustness with the SHIFT key, e.g. `code:10` instead of `1`.
|
||||
@@ -337,20 +652,23 @@ in
|
||||
", XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
|
||||
", XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 3%+"
|
||||
", XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 3%-"
|
||||
# Screenshots. $mod = pick a region (slurp), SHIFT = to clipboard;
|
||||
# bare Print grabs the whole screen to a file (the common case).
|
||||
", Print, exec, ${screenshot} full file"
|
||||
"$mod, Print, exec, ${screenshot} region file"
|
||||
"SHIFT, Print, exec, ${screenshot} full clip"
|
||||
"$mod SHIFT, Print, exec, ${screenshot} region clip"
|
||||
];
|
||||
|
||||
exec-once = [
|
||||
"waybar"
|
||||
# foot in server/daemon mode: $mod+Return runs `footclient`, which
|
||||
# attaches to this one process → faster new-window startup, shared RAM.
|
||||
# Uses foot's default socket, which footclient also defaults to.
|
||||
# Tradeoff: if the server dies, all foot windows die with it.
|
||||
"foot --server"
|
||||
# wallpaper (see modules/media.nix): start the awww daemon, then restore
|
||||
# the persisted choice. awww keeps no state across restarts, so re-push
|
||||
# the ~/.local/share/bg symlink if it exists.
|
||||
"awww-daemon"
|
||||
"sh -c '[ -e \"$HOME/.local/share/bg\" ] && awww img \"$HOME/.local/share/bg\"'"
|
||||
# re-push the wallpaper onto monitors connected after login (media.nix)
|
||||
"wallpaper-hotplug"
|
||||
];
|
||||
};
|
||||
};
|
||||
@@ -362,6 +680,23 @@ in
|
||||
# session has none otherwise; apps using libsecret/qtkeychain need it.
|
||||
services.gnome-keyring.enable = true;
|
||||
|
||||
# ssh-agent user service; sets SSH_AUTH_SOCK so `ssh-add` works in any shell.
|
||||
# gnome-keyring above no longer ships an ssh component (dropped upstream), and
|
||||
# gpg's ssh support is off, so this is the only agent. Keys are added on
|
||||
# demand — nothing here presumes a particular ssh config.
|
||||
services.ssh-agent.enable = true;
|
||||
# The agent, not the ssh client, collects the FIDO PIN when signing with a
|
||||
# verify-required sk-ed25519 key it holds — so the *agent's* environment needs
|
||||
# the askpass helper. The systemd user service starts with a minimal env that
|
||||
# lacks it, so such signatures fail with the misleading "incorrect passphrase
|
||||
# supplied to decrypt private key". REQUIRE=force drives the prompt with no
|
||||
# DISPLAY (a daemon has none); sshAskpass exits 0 for the touch step, so only
|
||||
# the PIN pops a dialog.
|
||||
systemd.user.services.ssh-agent.Service.Environment = [
|
||||
"SSH_ASKPASS=${sshAskpass}"
|
||||
"SSH_ASKPASS_REQUIRE=force"
|
||||
];
|
||||
|
||||
services.mako = {
|
||||
enable = true;
|
||||
settings = {
|
||||
@@ -379,15 +714,37 @@ in
|
||||
position = "top";
|
||||
height = 28;
|
||||
modules-left = [ "hyprland/workspaces" ];
|
||||
modules-center = [ "clock" ];
|
||||
modules-right = [ "pulseaudio" "network" "battery" ];
|
||||
modules-center = [ "hyprland/window" ];
|
||||
modules-right = [ "pulseaudio" "network" "battery" "clock" ];
|
||||
|
||||
"hyprland/workspaces" = {
|
||||
format = "{id}";
|
||||
};
|
||||
|
||||
"hyprland/window" = {
|
||||
format = "{title}";
|
||||
max-length = 100;
|
||||
separate-outputs = true;
|
||||
};
|
||||
|
||||
clock = {
|
||||
format = "{:%H:%M %a %d %b}";
|
||||
tooltip-format = "<tt>{calendar}</tt>";
|
||||
calendar = {
|
||||
mode = "month";
|
||||
mode-mon-col = 3;
|
||||
weeks-pos = "left";
|
||||
format = {
|
||||
months = "<span color='#e5c07b'><b>{}</b></span>";
|
||||
weekdays = "<span color='#c678dd'><b>{}</b></span>";
|
||||
days = "<span color='#bbbbbb'>{}</span>";
|
||||
today = "<span color='#61afef'><b><u>{}</u></b></span>";
|
||||
weeks = "<span color='#5c6370'>W{:%V}</span>";
|
||||
};
|
||||
};
|
||||
actions = {
|
||||
on-click-right = "mode";
|
||||
};
|
||||
};
|
||||
|
||||
pulseaudio = {
|
||||
@@ -427,9 +784,23 @@ in
|
||||
background: #222222;
|
||||
color: #bbbbbb;
|
||||
}
|
||||
#pulseaudio, #network, #battery {
|
||||
#pulseaudio, #network, #battery, #clock {
|
||||
padding: 0 12px;
|
||||
}
|
||||
#window {
|
||||
color: #ffffff;
|
||||
}
|
||||
tooltip {
|
||||
background: #222222;
|
||||
border: 1px solid #333333;
|
||||
}
|
||||
tooltip label {
|
||||
font-size: 15px;
|
||||
padding: 4px;
|
||||
}
|
||||
window#waybar.empty #window {
|
||||
background: transparent;
|
||||
}
|
||||
#battery.warning {
|
||||
color: #e5c07b;
|
||||
}
|
||||
@@ -439,6 +810,18 @@ in
|
||||
#battery.charging {
|
||||
color: #98c379;
|
||||
}
|
||||
#workspaces button {
|
||||
padding: 0 8px;
|
||||
color: #bbbbbb;
|
||||
}
|
||||
#workspaces button.active {
|
||||
color: #ffffff;
|
||||
background: #333333;
|
||||
box-shadow: inset 0 -2px #61afef;
|
||||
}
|
||||
#workspaces button.urgent {
|
||||
color: #e06c75;
|
||||
}
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Per-monitor "home" for workspaces. Each rule pins a workspace to a monitor
|
||||
# connector, so it opens there and Hyprland moves it back when that monitor
|
||||
# reappears after an unplug. The connector names are host hardware, so the rule
|
||||
# list is an option a consumer sets from its own host config (see the example in
|
||||
# `hosts/example/home.nix`), not baked in here; empty by default, so the example
|
||||
# host binds nothing.
|
||||
#
|
||||
# Soft binding: when a bound monitor is absent the workspace just falls back to
|
||||
# an active one — every workspace stays reachable, so a laptop-only (no external)
|
||||
# session works unchanged. The `default` keyword marks the workspace a monitor
|
||||
# shows on connect.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
options.local.hyprland.workspaceRules = lib.mkOption {
|
||||
type = with lib.types; listOf str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"1, monitor:HDMI-A-1, default:true"
|
||||
"6, monitor:LVDS-1, default:true"
|
||||
];
|
||||
description = ''
|
||||
Hyprland `workspace` rules binding workspaces to monitor connectors, so
|
||||
each workspace has a home screen. Connector names are host-specific — set
|
||||
this from the inventory rather than here.
|
||||
'';
|
||||
};
|
||||
|
||||
config.wayland.windowManager.hyprland.settings = {
|
||||
workspace = config.local.hyprland.workspaceRules;
|
||||
|
||||
# With a support monitor whose `default` workspace is high (e.g. a laptop
|
||||
# holding 6-9), a solo session with that monitor as the only screen opens on
|
||||
# that high workspace instead of 1. When rules are in effect, nudge a
|
||||
# single-monitor startup back to workspace 1. No-op with no rules (1 is
|
||||
# already the default). jq is pinned by store path so a consumer needn't
|
||||
# have it installed; hyprctl is bare (a Hyprland session always has it, like
|
||||
# the wallpaper exec-once assumes awww).
|
||||
exec-once = lib.optional (config.local.hyprland.workspaceRules != [ ])
|
||||
"sh -c '[ \"$(hyprctl monitors -j | ${pkgs.jq}/bin/jq length)\" -eq 1 ] && hyprctl dispatch workspace 1'";
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# LibreWolf browser. The package is an option so a consumer can swap the wrapped
|
||||
# package without editing this module: the base is plain pkgs.librewolf, but the
|
||||
# wrapper (wrapFirefox) accepts `extraPolicies` and `extraPrefs`, and overriding
|
||||
# those is the clean way to manage LibreWolf declaratively.
|
||||
#
|
||||
# The useful case is sharing state across LibreWolf profiles that otherwise stay
|
||||
# separate (each `-P <name>` has its own history/logins/cookies). Enterprise
|
||||
# policy `ExtensionSettings` force- or normal-installs the same add-ons into
|
||||
# every profile, and `extraPrefs` (autoconfig `defaultPref`/`lockPref`) seeds a
|
||||
# shared about:config baseline — both applied per-launch, neither touching
|
||||
# per-profile user data. Override, e.g.:
|
||||
#
|
||||
# local.librewolf.package = pkgs.librewolf.override {
|
||||
# extraPolicies.ExtensionSettings."uBlock0@raymondhill.net" = {
|
||||
# installation_mode = "normal_installed";
|
||||
# install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi";
|
||||
# };
|
||||
# extraPrefs = ''defaultPref("browser.startup.page", 3);'';
|
||||
# };
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
options.local.librewolf.package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.librewolf;
|
||||
defaultText = lib.literalExpression "pkgs.librewolf";
|
||||
description = ''
|
||||
LibreWolf package. Override with
|
||||
`pkgs.librewolf.override { extraPolicies = …; extraPrefs = …; }`
|
||||
to share extensions/settings across profiles.
|
||||
'';
|
||||
};
|
||||
|
||||
config.home.packages = [ config.local.librewolf.package ];
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
# Midnight Commander: TUI file manager. Its config (ini, panels, F2 user menu,
|
||||
# mc.ext.ini file associations) is opinionated but generic, so it lives in the
|
||||
# public baseline alongside the other desktop tooling. The only personal value
|
||||
# is the right-panel start dir, exposed as an option defaulting to the
|
||||
# consumer's own home. mc.ext.ini keeps archive browsing but routes every other
|
||||
# type to xdg-open, so the app choices live in xdg.mimeApps (baseline: mime.nix).
|
||||
#
|
||||
# ouch backs the F2 pack/unpack entries (one tool, archive format inferred from
|
||||
# the extension), so it ships with mc to keep the menu self-contained.
|
||||
#
|
||||
# mc honours XDG natively: config in $XDG_CONFIG_HOME/mc (the files below),
|
||||
# mutable runtime state (history, cursor positions) in $XDG_DATA_HOME/mc — no
|
||||
# env vars needed. `ini` sets auto_save_setup=false so mc never rewrites these
|
||||
# read-only store symlinks on exit.
|
||||
#
|
||||
# The F2 menu is a `types.lines` option, not a baked source file, so consumers
|
||||
# can extend or replace it. The shipped menu is a `mkBefore` definition (stays
|
||||
# first, ahead of appended entries), NOT a `default` — a `default` is dropped
|
||||
# the moment a consumer adds a definition, which would lose the base. As a real
|
||||
# definition it merges: `local.mc.menu = "…";` appends after the base entries;
|
||||
# `lib.mkForce "…"` replaces it wholesale.
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
options.local.mc = {
|
||||
otherDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = config.home.homeDirectory;
|
||||
defaultText = lib.literalExpression "config.home.homeDirectory";
|
||||
description = "Startup directory for mc's other (right) panel.";
|
||||
};
|
||||
|
||||
menu = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
description = ''
|
||||
Contents of mc's F2 user menu. The shipped baseline is a mkBefore
|
||||
definition, so a plain assignment appends entries after it; use
|
||||
lib.mkForce to replace it entirely.
|
||||
'';
|
||||
};
|
||||
|
||||
extIni = lib.mkOption {
|
||||
type = lib.types.lines;
|
||||
description = ''
|
||||
Contents of mc's mc.ext.ini (file-type associations driving Enter/F3/F4).
|
||||
The shipped baseline keeps mc's archive browsing and delegates every
|
||||
other type to xdg-open, so the actual app is the XDG mimeapps default.
|
||||
Like `menu`, it is a mkBefore definition: append a same-named section to
|
||||
override one key (mc merges same-named sections, last key wins), or use
|
||||
lib.mkForce to replace it entirely.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
home.packages = [ pkgs.mc pkgs.ouch ];
|
||||
|
||||
local.mc.menu = lib.mkBefore (builtins.readFile ./assets/mc/menu);
|
||||
local.mc.extIni = lib.mkBefore (builtins.readFile ./assets/mc/mc.ext.ini);
|
||||
|
||||
xdg.configFile = {
|
||||
"mc/ini".source = ./assets/mc/ini;
|
||||
"mc/menu".text = config.local.mc.menu;
|
||||
"mc/mc.ext.ini".text = config.local.mc.extIni;
|
||||
|
||||
# Generated (not a static asset) so other_dir tracks the option instead of
|
||||
# baking in a personal path.
|
||||
"mc/panels.ini".text = ''
|
||||
[New Left Panel]
|
||||
display=listing
|
||||
reverse=false
|
||||
case_sensitive=true
|
||||
exec_first=false
|
||||
sort_order=name
|
||||
list_mode=full
|
||||
brief_cols=2
|
||||
user_format=half type name | size | perm | mtime
|
||||
user_status0=half type name | size | perm
|
||||
user_status1=half type name | size | perm
|
||||
user_status2=half type name | size | perm
|
||||
user_status3=half type name | owner | group | ctime
|
||||
user_mini_status=true
|
||||
list_format=user
|
||||
|
||||
[New Right Panel]
|
||||
display=listing
|
||||
reverse=false
|
||||
case_sensitive=true
|
||||
exec_first=false
|
||||
sort_order=name
|
||||
list_mode=full
|
||||
brief_cols=2
|
||||
user_format=half type name | size | perm | mtime
|
||||
user_status0=half type name | size | perm
|
||||
user_status1=half type name | size | perm
|
||||
user_status2=half type name | size | perm
|
||||
user_status3=half type name | owner | group | ctime
|
||||
user_mini_status=true
|
||||
list_format=user
|
||||
|
||||
[Dirs]
|
||||
current_is_left=true
|
||||
other_dir=${config.local.mc.otherDir}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,22 @@ let
|
||||
[ -n "$1" ] && ln -sf "$(readlink -f "$1")" "$bgloc"
|
||||
${pkgs.awww}/bin/awww img "$bgloc"
|
||||
'';
|
||||
|
||||
# awww-daemon paints only the outputs present when `awww img` last ran and does
|
||||
# not repaint a monitor hotplugged afterwards — it comes up on the fill color
|
||||
# (black), and a transparent foot on it then shows that black, not the
|
||||
# wallpaper. Watch Hyprland's event socket and re-push the wallpaper (setbg
|
||||
# with no arg → all current outputs) on each monitor-add. Hyprland emits both
|
||||
# `monitoradded` and `monitoraddedv2`; match only the former so setbg fires
|
||||
# once. Runs as a Hyprland exec-once (see home.nix).
|
||||
wallpaperHotplug = pkgs.writeShellScriptBin "wallpaper-hotplug" ''
|
||||
sock="''${XDG_RUNTIME_DIR}/hypr/''${HYPRLAND_INSTANCE_SIGNATURE}/.socket2.sock"
|
||||
${pkgs.socat}/bin/socat -u UNIX-CONNECT:"$sock" - | while IFS= read -r line; do
|
||||
case "$line" in
|
||||
"monitoradded>>"*) setbg ;;
|
||||
esac
|
||||
done
|
||||
'';
|
||||
in
|
||||
{
|
||||
programs.mpv = {
|
||||
@@ -50,6 +66,7 @@ in
|
||||
|
||||
home.packages = with pkgs; [
|
||||
setbg
|
||||
wallpaperHotplug # re-push wallpaper to a hotplugged monitor (see comment above)
|
||||
awww # wayland wallpaper daemon (setbg backend)
|
||||
wl-clipboard # wl-copy, for the yank binds
|
||||
imagemagick # `magick`: rotate/flip binds
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Default applications per file type -- the single source of truth for "open
|
||||
# with" across the desktop: xdg-open, file pickers, and Enter in mc (whose
|
||||
# mc.ext.ini delegates every non-archive type to xdg-open; see mc.nix).
|
||||
#
|
||||
# These belong in the public baseline because the handlers do too: zathura, imv
|
||||
# and mpv come from media.nix, libreoffice from home.nix. So "PDF opens in
|
||||
# zathura", "images in imv", etc. are the natural defaults of this baseline, not
|
||||
# personal choices -- every entry is mkDefault, so a consumer overrides one type
|
||||
# by setting the same key (their definition wins), or flips enable off entirely.
|
||||
#
|
||||
# Personal handlers -- web browser, mail -- are NOT here; they map to a specific
|
||||
# browser profile / mail client and live in the private inventory, merging into
|
||||
# the same defaultApplications attrset.
|
||||
|
||||
{ lib, ... }:
|
||||
|
||||
let
|
||||
# Map every mime in the list to one .desktop id, at mkDefault priority.
|
||||
forAll = desktop: mimes: lib.genAttrs mimes (_: lib.mkDefault desktop);
|
||||
|
||||
zathura = "org.pwmt.zathura.desktop";
|
||||
imv = "imv.desktop";
|
||||
mpv = "mpv.desktop";
|
||||
writer = "writer.desktop";
|
||||
calc = "calc.desktop";
|
||||
impress = "impress.desktop";
|
||||
in {
|
||||
xdg.mimeApps.enable = lib.mkDefault true;
|
||||
|
||||
xdg.mimeApps.defaultApplications =
|
||||
(forAll zathura [
|
||||
"application/pdf"
|
||||
"application/epub+zip"
|
||||
"application/oxps"
|
||||
"application/postscript"
|
||||
])
|
||||
// (forAll imv [
|
||||
"image/png"
|
||||
"image/jpeg"
|
||||
"image/gif"
|
||||
"image/webp"
|
||||
"image/bmp"
|
||||
"image/tiff"
|
||||
])
|
||||
// (forAll mpv [
|
||||
"video/mp4"
|
||||
"video/x-matroska"
|
||||
"video/webm"
|
||||
"video/x-msvideo"
|
||||
"video/quicktime"
|
||||
"video/mpeg"
|
||||
"audio/mpeg"
|
||||
"audio/flac"
|
||||
"audio/ogg"
|
||||
"audio/x-wav"
|
||||
"audio/mp4"
|
||||
"audio/x-opus+ogg"
|
||||
])
|
||||
// (forAll writer [
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
"application/msword"
|
||||
"application/vnd.oasis.opendocument.text"
|
||||
"application/rtf"
|
||||
])
|
||||
// (forAll calc [
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
"application/vnd.ms-excel"
|
||||
"application/vnd.oasis.opendocument.spreadsheet"
|
||||
])
|
||||
// (forAll impress [
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
"application/vnd.ms-powerpoint"
|
||||
"application/vnd.oasis.opendocument.presentation"
|
||||
]);
|
||||
}
|
||||
+30
-8
@@ -5,6 +5,8 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
imports = [ ./shutdown-debug.nix ];
|
||||
|
||||
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
||||
# Deduplicate identical store paths via hardlinks.
|
||||
nix.settings.auto-optimise-store = true;
|
||||
@@ -39,14 +41,14 @@
|
||||
];
|
||||
};
|
||||
|
||||
# Time zone. Template default — override per host/region.
|
||||
time.timeZone = "Europe/Berlin";
|
||||
|
||||
# Internationalisation properties. Template defaults — override per region.
|
||||
i18n.defaultLocale = "en_US.UTF-8";
|
||||
# Neutral template defaults for regional/localized settings. All are
|
||||
# mkDefault: set the real values (time zone, keymap, locale categories) in your
|
||||
# own config so this public baseline stays region-agnostic.
|
||||
time.timeZone = lib.mkDefault "UTC";
|
||||
i18n.defaultLocale = lib.mkDefault "en_US.UTF-8";
|
||||
console = {
|
||||
font = "Lat2-Terminus16";
|
||||
keyMap = "de";
|
||||
font = lib.mkDefault "Lat2-Terminus16";
|
||||
keyMap = lib.mkDefault "us";
|
||||
};
|
||||
|
||||
# Programs.
|
||||
@@ -54,6 +56,23 @@
|
||||
programs.hyprland.enable = true;
|
||||
programs.dconf.enable = true;
|
||||
|
||||
# nix-ld: install a stub dynamic linker at the standard FHS path
|
||||
# (/lib64/ld-linux-*.so), which NixOS otherwise lacks. Lets unpatched
|
||||
# prebuilt binaries run — notably the standalone CPython builds uv downloads
|
||||
# and pip wheels with compiled extensions (numpy, pydantic-core, ...). The
|
||||
# libraries list is exposed as NIX_LD_LIBRARY_PATH so those binaries find
|
||||
# common shared libs; extend it when an import fails with "libFOO.so.N: cannot
|
||||
# open shared object file".
|
||||
programs.nix-ld = {
|
||||
enable = true;
|
||||
libraries = with pkgs; [
|
||||
stdenv.cc.cc.lib # libstdc++, libgcc_s, libgomp
|
||||
zlib # libz
|
||||
zstd # libzstd (pyarrow, polars)
|
||||
openssl # libssl, libcrypto
|
||||
];
|
||||
};
|
||||
|
||||
# Packages installed in system profile.
|
||||
environment.systemPackages = with pkgs; [
|
||||
curl
|
||||
@@ -96,7 +115,10 @@
|
||||
# so the Secret Service is open without a second passphrase.
|
||||
security.pam.services.greetd.enableGnomeKeyring = true;
|
||||
|
||||
# XDG portals (screen sharing, file pickers, and so on).
|
||||
# XDG portals (screen sharing, file pickers, and so on). The GTK portal (for
|
||||
# file dialogs) is added at the home-manager layer, not here: home-manager's
|
||||
# Hyprland module pins the portal search dir to the user profile, so a portal
|
||||
# listed in this system-level extraPortals is never seen. See modules/home.nix.
|
||||
xdg.portal = {
|
||||
enable = true;
|
||||
extraPortals = [ pkgs.xdg-desktop-portal-hyprland ];
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
# Neovim, configured declaratively via nixvim. The editor — plugins, LSP,
|
||||
# completion, keymaps — is defined here and pinned by the flake lock; nothing is
|
||||
# downloaded at runtime, unlike an imperative distro (NvChad/LazyVim). The nixvim
|
||||
# home-manager module that provides `programs.nixvim` is imported for consumers by
|
||||
# `homeModules.default` in flake.nix, so this file only sets options.
|
||||
#
|
||||
# Consumer knobs (the option-with-default contract): `local.nvim.enable` to opt
|
||||
# out wholesale, `local.nvim.colorscheme` to swap theme in one line, and
|
||||
# `local.nvim.lspServers` to reshape the language set. Everything else is
|
||||
# opinionated — but since nixvim exposes every plugin as an option, a consumer can
|
||||
# still override any `programs.nixvim.plugins.*` directly from their own config.
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.local.nvim;
|
||||
in
|
||||
{
|
||||
options.local.nvim = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Whether to enable the nixvim-based Neovim setup.";
|
||||
};
|
||||
|
||||
colorscheme = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "catppuccin";
|
||||
example = "tokyonight";
|
||||
description = ''
|
||||
nixvim colorscheme module to enable (the attr under `colorschemes.*`,
|
||||
e.g. "catppuccin", "tokyonight", "gruvbox"). Catppuccin is tuned below
|
||||
(mocha flavour); for another scheme set its `colorschemes.<name>.settings`
|
||||
from your own config if you want to tweak it.
|
||||
'';
|
||||
};
|
||||
|
||||
lspServers = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [
|
||||
"nixd" # Nix — this repo's own language, so a sensible public baseline
|
||||
"bashls" # Shell — likewise generic enough to default on
|
||||
];
|
||||
example = [ "nixd" "bashls" "rust_analyzer" "basedpyright" ];
|
||||
description = ''
|
||||
lspconfig server names to enable under `plugins.lsp.servers.<name>`.
|
||||
Each must exist in nixvim's server list and its binary in nixpkgs. The
|
||||
public default is deliberately minimal (Nix + shell); a consumer sets its
|
||||
own language set here — and any per-server tuning a pick needs (e.g.
|
||||
rust_analyzer's `installRustc`) via `programs.nixvim.plugins.lsp.servers`.
|
||||
Note: standalone Lua wants "lua_ls", but Lua embedded in Nix strings is
|
||||
invisible to any LSP, so it is not needed just to edit this config.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
programs.nixvim = {
|
||||
enable = true;
|
||||
|
||||
# Point nixvim at our (followed) nixpkgs instead of the rev it pins. Both
|
||||
# track nixos-26.05, so the version skew nixvim warns about does not arise,
|
||||
# and this keeps a single nixpkgs in the closure. Setting it explicitly is
|
||||
# also what silences that warning.
|
||||
nixpkgs.source = pkgs.path;
|
||||
|
||||
# System clipboard on Wayland: yanks go to / puts read from wl-clipboard,
|
||||
# so copy/paste crosses the nvim boundary. Enabling the provider pulls the
|
||||
# wl-clipboard binaries in as a dependency.
|
||||
clipboard = {
|
||||
register = "unnamedplus";
|
||||
providers.wl-copy.enable = true;
|
||||
};
|
||||
|
||||
globals = {
|
||||
mapleader = " ";
|
||||
maplocalleader = " ";
|
||||
};
|
||||
|
||||
opts = {
|
||||
number = true;
|
||||
relativenumber = true; # relative line numbers for quick j/k motions
|
||||
signcolumn = "yes"; # always show the gutter so text doesn't jump
|
||||
cursorline = true;
|
||||
termguicolors = true; # 24-bit colour, required by catppuccin & co.
|
||||
scrolloff = 8; # keep 8 lines of context around the cursor
|
||||
|
||||
expandtab = true; # spaces, not tabs
|
||||
shiftwidth = 2;
|
||||
tabstop = 2;
|
||||
smartindent = true;
|
||||
|
||||
ignorecase = true; # case-insensitive search…
|
||||
smartcase = true; # …unless the query has a capital
|
||||
undofile = true; # persistent undo across sessions
|
||||
splitright = true;
|
||||
splitbelow = true;
|
||||
|
||||
# What :mksession stores (persistence.nvim). Drops "blank" and "terminal"
|
||||
# from nvim's default so empty/plugin windows (neo-tree, toggleterm) aren't
|
||||
# serialised — they restore as broken empty splits otherwise. Adds
|
||||
# "globals", which some plugins rely on. Pairs with the PersistenceSavePre
|
||||
# neo-tree close below.
|
||||
sessionoptions = "buffers,curdir,folds,globals,tabpages,winsize";
|
||||
};
|
||||
|
||||
# Theme. Swap via local.nvim.colorscheme; catppuccin gets the mocha flavour.
|
||||
# mkMerge keeps the computed `${cfg.colorscheme}` key from colliding with the
|
||||
# literal `catppuccin` one when they happen to be the same attr.
|
||||
colorschemes = lib.mkMerge [
|
||||
{ ${cfg.colorscheme}.enable = true; }
|
||||
(lib.mkIf (cfg.colorscheme == "catppuccin") {
|
||||
catppuccin.settings.flavour = "mocha";
|
||||
})
|
||||
];
|
||||
|
||||
plugins = {
|
||||
# Shared icon set for the tree, statusline, completion menu.
|
||||
web-devicons.enable = true;
|
||||
|
||||
# Syntax via tree-sitter. The default grammar set is broad, so most
|
||||
# languages get highlighting without an LSP or any per-language wiring.
|
||||
treesitter = {
|
||||
enable = true;
|
||||
settings = {
|
||||
highlight.enable = true;
|
||||
indent.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
# LSP. Servers come from local.nvim.lspServers; blink-cmp feeds its
|
||||
# completion capabilities in automatically (setupLspCapabilities).
|
||||
lsp = {
|
||||
enable = true;
|
||||
servers = lib.genAttrs cfg.lspServers (_: { enable = true; });
|
||||
keymaps = {
|
||||
silent = true;
|
||||
lspBuf = {
|
||||
gd = "definition";
|
||||
gD = "declaration";
|
||||
K = "hover";
|
||||
"<leader>rn" = "rename";
|
||||
"<leader>ca" = "code_action";
|
||||
};
|
||||
# gr/gi (references, implementations) are wired to Telescope pickers in
|
||||
# the global keymaps below instead — fuzzy + preview browsing beats the
|
||||
# quickfix list that vim.lsp.buf.references would populate. gd stays a
|
||||
# direct jump here (usually one target; Ctrl-o returns).
|
||||
diagnostic = {
|
||||
# Idiomatic vim bracket pair for stepping a list, kept as the public
|
||||
# default. A consumer whose layout makes brackets awkward (they need
|
||||
# AltGr on QWERTZ) can add comfortable aliases from their own config:
|
||||
# this option is attrsOf, so extra keys merge in without conflict.
|
||||
"[d" = "goto_prev";
|
||||
"]d" = "goto_next";
|
||||
"<leader>ds" = "open_float"; # show the diagnostic under the cursor
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Completion. blink.cmp — faster and lighter to configure than nvim-cmp.
|
||||
blink-cmp = {
|
||||
enable = true;
|
||||
settings = {
|
||||
keymap.preset = "default"; # C-space open, C-y confirm, C-n/C-p move
|
||||
appearance.nerd_font_variant = "mono";
|
||||
sources.default = [ "lsp" "path" "snippets" "buffer" ];
|
||||
completion.documentation.auto_show = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Fuzzy finder — files, live grep, open buffers (buffer switching lives
|
||||
# here rather than a bufferline; see the S-h/S-l binds below too).
|
||||
telescope.enable = true;
|
||||
|
||||
# File tree.
|
||||
neo-tree.enable = true;
|
||||
|
||||
# Statusline, NvChad-ish blocks: powerline separators, one global bar,
|
||||
# theme following the colorscheme.
|
||||
lualine = {
|
||||
enable = true;
|
||||
settings.options = {
|
||||
theme = "auto";
|
||||
globalstatus = true;
|
||||
section_separators = { left = ""; right = ""; };
|
||||
component_separators = { left = ""; right = ""; };
|
||||
};
|
||||
};
|
||||
|
||||
# Inline colour swatches for #rrggbb / colour names.
|
||||
colorizer.enable = true;
|
||||
|
||||
# Git signs in the gutter.
|
||||
gitsigns.enable = true;
|
||||
|
||||
# Session persistence: auto-saves a session per project directory into XDG
|
||||
# state, restorable later. <leader>S restores the current dir's session; a
|
||||
# consumer wanting auto-restore on a bare `nvim` adds a VimEnter autocmd.
|
||||
persistence.enable = true;
|
||||
|
||||
# Floating terminal toggled with Ctrl-\ (shares nvim's cwd). Try it; if
|
||||
# a tiled foot next to nvim serves you better, drop this block.
|
||||
toggleterm = {
|
||||
enable = true;
|
||||
settings = {
|
||||
open_mapping = { __raw = "[[<c-\\>]]"; };
|
||||
direction = "float";
|
||||
};
|
||||
};
|
||||
|
||||
# Popup of the pending keybinds after leader — a live, always-accurate
|
||||
# cheatsheet. The group labels name each leader subtree so the popup reads
|
||||
# by category; <leader>? opens the full list on demand (keymaps below).
|
||||
which-key = {
|
||||
enable = true;
|
||||
settings.spec = [
|
||||
{ __unkeyed-1 = "<leader>f"; group = "Find (Telescope)"; }
|
||||
{ __unkeyed-1 = "<leader>d"; group = "Diagnostics"; }
|
||||
{ __unkeyed-1 = "<leader>b"; group = "Buffer"; }
|
||||
{ __unkeyed-1 = "<leader>c"; group = "Code"; }
|
||||
{ __unkeyed-1 = "<leader>r"; group = "Refactor"; }
|
||||
{ __unkeyed-1 = "<leader>s"; group = "Search"; }
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Global (non-LSP) keymaps. Deliberately small — a starting point to grow.
|
||||
keymaps = [
|
||||
{ mode = "n"; key = "<leader>w"; action = "<cmd>w<cr>"; options.desc = "Save"; }
|
||||
{ mode = "n"; key = "<leader>q"; action = "<cmd>q<cr>"; options.desc = "Quit window"; }
|
||||
{ mode = "n"; key = "<leader>Q"; action = "<cmd>qa<cr>"; options.desc = "Quit all (exit nvim)"; }
|
||||
# Restore the saved session for the current directory (persistence.nvim).
|
||||
{ mode = "n"; key = "<leader>S"; action.__raw = "function() require('persistence').load() end"; options.desc = "Restore session"; }
|
||||
{ mode = "n"; key = "<leader>e"; action = "<cmd>Neotree toggle<cr>"; options.desc = "File tree"; }
|
||||
{ mode = "n"; key = "<Esc>"; action = "<cmd>nohlsearch<cr>"; }
|
||||
|
||||
# Telescope
|
||||
{ mode = "n"; key = "<leader>ff"; action = "<cmd>Telescope find_files<cr>"; options.desc = "Find files"; }
|
||||
{ mode = "n"; key = "<leader>fg"; action = "<cmd>Telescope live_grep<cr>"; options.desc = "Live grep"; }
|
||||
{ mode = "n"; key = "<leader>fb"; action = "<cmd>Telescope buffers<cr>"; options.desc = "Buffers"; }
|
||||
{ mode = "n"; key = "<leader>fh"; action = "<cmd>Telescope help_tags<cr>"; options.desc = "Help tags"; }
|
||||
|
||||
# LSP navigation via Telescope pickers (fuzzy + preview) for the
|
||||
# many-result lookups; gd/gD stay direct jumps (plugins.lsp.keymaps above).
|
||||
{ mode = "n"; key = "gr"; action = "<cmd>Telescope lsp_references<cr>"; options.desc = "References"; }
|
||||
{ mode = "n"; key = "gi"; action = "<cmd>Telescope lsp_implementations<cr>"; options.desc = "Implementations"; }
|
||||
|
||||
# Buffer navigation (no bufferline: cycle with Shift-h/l, list via <leader>fb)
|
||||
{ mode = "n"; key = "<S-l>"; action = "<cmd>bnext<cr>"; options.desc = "Next buffer"; }
|
||||
{ mode = "n"; key = "<S-h>"; action = "<cmd>bprevious<cr>"; options.desc = "Prev buffer"; }
|
||||
{ mode = "n"; key = "<leader>bd"; action = "<cmd>bdelete<cr>"; options.desc = "Close buffer"; }
|
||||
|
||||
# Cheatsheet: <leader>? pops the full which-key list; <leader>sk fuzzy-
|
||||
# searches every active mapping via Telescope (catches non-leader binds too).
|
||||
{ mode = "n"; key = "<leader>?"; action = "<cmd>WhichKey<cr>"; options.desc = "All keybinds (which-key)"; }
|
||||
{ mode = "n"; key = "<leader>sk"; action = "<cmd>Telescope keymaps<cr>"; options.desc = "Search keymaps"; }
|
||||
];
|
||||
|
||||
# persistence saves the window layout via :mksession, but neo-tree's special
|
||||
# buffer doesn't serialise — a saved tree window restores as a blank split.
|
||||
# Close neo-tree just before a session is written so the layout stays clean.
|
||||
autoCmd = [
|
||||
{
|
||||
event = "User";
|
||||
pattern = "PersistenceSavePre";
|
||||
callback.__raw = ''function() pcall(vim.cmd, "Neotree close") end'';
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
# Shutdown/boot hang diagnostics.
|
||||
#
|
||||
# Shutdown stalls are awkward to debug because journald is torn down mid-way,
|
||||
# so anything that wedges *after* it dies never reaches /var/log/journal — the
|
||||
# tail just stops with no explanation. These knobs make the next hang leave a
|
||||
# trail instead. All are opt-in except the shortened stop-timeout, which is a
|
||||
# baseline preset (overridable / disable with null).
|
||||
#
|
||||
# Reading the evidence after a hang:
|
||||
# - On screen while it hangs: watch for "A stop job is running for <unit>
|
||||
# (Xs / 1min 30s)". That line names the culprit; note whether its counter
|
||||
# advances (named unit = easy) or the screen is frozen with no counter
|
||||
# (late systemd-shutdown phase = needs pstore, below).
|
||||
# - After a *clean* next boot: `journalctl -b -1 -e` shows the previous
|
||||
# boot's shutdown tail (NixOS journald is persistent by default).
|
||||
# - After a *forced* power-off: journald never flushed, so check the kernel
|
||||
# ring buffer preserved across reboot in `/sys/fs/pstore/` (needs pstore
|
||||
# backing — ACPI ERST on most laptops, else ramoops). `verboseLogging`
|
||||
# below routes systemd's own messages to kmsg so they land there too.
|
||||
# - With `debugShell` on: switch to tty9 (Ctrl+Alt+F9) while hung and run
|
||||
# `systemctl list-jobs` to see which job is still pending.
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.local.shutdownDebug;
|
||||
in
|
||||
{
|
||||
options.local.shutdownDebug = {
|
||||
stopTimeout = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = "20s";
|
||||
example = null;
|
||||
description = ''
|
||||
DefaultTimeoutStopSec for both the system and per-user managers. The
|
||||
default shortens systemd's 90s so a hung unit is killed — and logged as
|
||||
`timed out` — in seconds rather than spinning at a black screen. Set to
|
||||
null to leave systemd's default untouched. Any systemd time span, e.g.
|
||||
"20s", "45s", "1min".
|
||||
'';
|
||||
};
|
||||
|
||||
verboseLogging = lib.mkEnableOption ''
|
||||
verbose systemd shutdown logging. Adds the kernel params
|
||||
`systemd.log_level=debug systemd.log_target=kmsg`: bumps systemd to debug
|
||||
verbosity and routes its messages into the kernel ring buffer, which
|
||||
outlives journald and can be recovered from /sys/fs/pstore after a forced
|
||||
power-off. Noisy on every boot and mildly slows it — enable only while
|
||||
actively hunting a hang'';
|
||||
|
||||
debugShell = lib.mkEnableOption ''
|
||||
an unauthenticated root shell on tty9 (Ctrl+Alt+F9), available through
|
||||
shutdown, for running `systemctl list-jobs` while the system is wedged.
|
||||
SECURITY: anyone with physical console access gets root — leave off
|
||||
except while investigating'';
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf (cfg.stopTimeout != null) {
|
||||
systemd.settings.Manager.DefaultTimeoutStopSec = cfg.stopTimeout;
|
||||
systemd.user.extraConfig = "DefaultTimeoutStopSec=${cfg.stopTimeout}";
|
||||
})
|
||||
|
||||
(lib.mkIf cfg.verboseLogging {
|
||||
boot.kernelParams = [ "systemd.log_level=debug" "systemd.log_target=kmsg" ];
|
||||
})
|
||||
|
||||
# Uniquely named rather than enabling the upstream (static) debug-shell.service,
|
||||
# whose enablement via systemd.services would generate a shadowing unit.
|
||||
(lib.mkIf cfg.debugShell {
|
||||
systemd.services.shutdown-debug-shell = {
|
||||
description = "Root shell on tty9 for boot/shutdown debugging";
|
||||
unitConfig = {
|
||||
DefaultDependencies = false;
|
||||
ConditionPathExists = "/dev/tty9";
|
||||
};
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Environment = "TERM=linux";
|
||||
ExecStart = "${pkgs.bashInteractive}/bin/bash";
|
||||
Restart = "always";
|
||||
RestartSec = 0;
|
||||
StandardInput = "tty";
|
||||
TTYPath = "/dev/tty9";
|
||||
TTYReset = true;
|
||||
TTYVHangup = true;
|
||||
KillMode = "process";
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# TeX Live for pandoc PDF output: pandoc shells out to a LaTeX engine, so PDF
|
||||
# conversion needs a TeX distribution (other formats — html, docx, md — do not).
|
||||
# The package set is an option so a consumer can swap the scheme without editing
|
||||
# this module: scheme-medium covers pandoc's default LaTeX template; override
|
||||
# for a larger set (missing .sty files) or a smaller one.
|
||||
#
|
||||
# TeX Live's font cache and config trees default to ~/.texlive<year> and ~/texmf
|
||||
# (written on the first PDF build). The TEXMF* vars route them to XDG dirs.
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
options.local.texlive.package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.texlive.withPackages (ps: [ ps.scheme-medium ]);
|
||||
defaultText = lib.literalExpression
|
||||
"pkgs.texlive.withPackages (ps: [ ps.scheme-medium ])";
|
||||
description = "TeX Live package set. Override for a larger/smaller scheme.";
|
||||
};
|
||||
|
||||
config = {
|
||||
home.packages = [ config.local.texlive.package ];
|
||||
|
||||
# texmf-var is regenerable font/format cache → cacheHome; the other two hold
|
||||
# user config/data trees.
|
||||
home.sessionVariables = {
|
||||
TEXMFHOME = "${config.xdg.dataHome}/texmf";
|
||||
TEXMFVAR = "${config.xdg.cacheHome}/texlive/texmf-var";
|
||||
TEXMFCONFIG = "${config.xdg.configHome}/texlive/texmf-config";
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user