nixos config

This commit is contained in:
Nexoniarz 2026-08-30 01:26:52 +02:00
commit 5dc9fb020e
46 changed files with 6194 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
hardware-configuration.nix
.claude
result
result-*

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# nexoniarz-config-nixos
# nexoniarz-config-nixos
# nexoniarz-config-nixos

19
configuration.nix Normal file
View file

@ -0,0 +1,19 @@
# Main NixOS configuration file importing modular configurations.
{ config, pkgs, ... }:
{
imports = [
./hardware-configuration.nix # Generated hardware scan results
./modules/system.nix # Bootloader, networking, time, sound, hardening, nix-ld
./modules/drivers.nix # NVIDIA Open GPU kernel modules and graphics
./modules/desktop.nix # Hyprland compositor and desktop environment
./modules/theme.nix # Shared color palette; GTK dark+squared theming
./modules/users.nix # User accounts and global configurations
./modules/apps.nix # All installed packages: apps, CLI tools, dev toolchains
./modules/scripts.nix # Custom system scripts and helpers
];
# Do not change this value. Read documentation before updating.
system.stateVersion = "26.05";
}

60
dotfiles/kitty/kitty.conf Normal file
View file

@ -0,0 +1,60 @@
# Kitty terminal configuration
# Deployed to ~/.config/kitty/kitty.conf via systemd.tmpfiles in
# modules/desktop.nix.
# --- Shell: run ZSH directly, with shell integration ---
shell zsh
shell_integration enabled
# --- Font ---
font_family JetBrainsMono Nerd Font
bold_font auto
italic_font auto
bold_italic_font auto
font_size 11.0
# --- Squared, minimal window chrome ---
window_padding_width 8
window_margin_width 0
window_border_width 0
draw_minimal_borders yes
hide_window_decorations yes
confirm_os_window_close 0
# --- Tab bar: flat, square ---
tab_bar_edge top
tab_bar_style powerline
tab_powerline_style slanted
tab_bar_min_tabs 2
# --- Cursor ---
cursor_shape block
cursor_blink_interval 0
# --- Colors ---
# Base/accent colors (background, foreground, selection, borders, tabs)
# come from the Quickshell Theme picker (dotfiles/quickshell/Theme.qml),
# via kitty-theme-set — kitty has a built-in config-file watcher (see
# `kitten __watch_conf__` in its process list) that also watches included
# files, so this picks up changes live with no restart needed.
include theme.conf
# ANSI color0-15 palette (used for terminal output — ls, git diff, etc.)
# stays fixed regardless of accent/mode: these need to stay mutually
# distinguishable hues, not track a single accent color.
color0 #0d0d0d
color8 #444444
color1 #e06c75
color9 #e06c75
color2 #98c379
color10 #98c379
color3 #e5c07b
color11 #e5c07b
color4 #61afef
color12 #61afef
color5 #c678dd
color13 #c678dd
color6 #56b6c2
color14 #56b6c2
color7 #e0e0e0
color15 #ffffff

30
dotfiles/mako/config Normal file
View file

@ -0,0 +1,30 @@
# Mako notification daemon configuration
# Deployed to ~/.config/mako/config via systemd.tmpfiles in
# modules/desktop.nix. Squared, sharp, no rounded corners.
#
# Anchored top-center to match the custom polkit dialog (PolkitDialog.qml)
# so both "system popup" surfaces show up in the same place.
background-color=#0d0d0dee
text-color=#e0e0e0ff
border-color=#333333ff
border-size=2
border-radius=0
padding=10
margin=12
width=420
font=JetBrainsMono Nerd Font 10
default-timeout=5000
max-visible=5
layer=overlay
anchor=top-center
[urgency=low]
border-color=#333333ff
[urgency=normal]
border-color=#8fbcbbff
[urgency=critical]
border-color=#e06c75ff
default-timeout=0

View file

@ -0,0 +1,352 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import Quickshell.Widgets
PanelWindow {
id: root
required property var screen
required property var leftBar
required property var rightBar
// Right-click context menu state — set by whichever taskbar window
// entry was right-clicked, cleared after an action or a second
// right-click on the same entry. Anchored via anchor.window + a
// one-shot computed x (not anchor.item!) — anchoring a popup directly
// to a Repeater delegate crashed Quickshell (PopupAnchor::setItem /
// onItemWindowChanged segfaults when the delegate it's watching gets
// destroyed/recreated, which happens often since the window list
// rebuilds its model on every toplevel change). root itself is a
// stable, never-recreated window, so anchoring to it is safe — same
// pattern the Left/RightBar flyouts already use successfully.
property var ctxMenuToplevel: null
property real ctxMenuX: 0
function closeCtxMenu() {
root.ctxMenuToplevel = null;
}
anchors { left: true; right: true; bottom: true }
implicitHeight: 46
exclusiveZone: 46
color: Theme.bg
focusable: false
// Special workspaces (e.g. "special:showdesktop", used by the
// Super+D show-desktop toggle) are an implementation detail, not a
// real switchable workspace — Hyprland keeps their metadata around
// even once empty, so they'd otherwise show up here forever.
property var visibleWorkspaces: {
var out = [];
var all = Hyprland.workspaces.values;
for (var i = 0; i < all.length; i++) {
if (all[i].name.indexOf("special:") !== 0) out.push(all[i]);
}
return out;
}
// Only workspaces that actually exist (have windows, or are/were
// focused) get a pill — showing all 10 possible slots took up too
// much space. Always shown in numeric order (1, 2, 3, ...) regardless
// of creation/focus order, matching the Super+1..9,0 keybinds.
property var workspacePills: {
var out = [];
for (var i = 0; i < root.visibleWorkspaces.length; i++) {
var w = root.visibleWorkspaces[i];
out.push({ number: w.id, ws: w });
}
out.sort(function (a, b) { return a.number - b.number; });
return out;
}
// Window list ordered by workspace number (1, then 2, ...) instead of
// whatever arbitrary order the compositor reports toplevels in.
//
// The base list comes from ToplevelManager (the generic Wayland
// wlr-foreign-toplevel-management module), NOT Hyprland.toplevels —
// Hyprland.toplevels is built by parsing Hyprland's own IPC event
// stream, and apps that open/close many short-lived helper windows
// fast (Steam is the textbook case: steamwebhelper, dummy GL context
// windows, etc.) can desync it from reality, leaving phantom entries
// that `hyprctl clients` doesn't even show. ToplevelManager reflects
// live protocol state directly, so it can't go stale like that.
// Hyprland.toplevels is only used as a lookup to find each window's
// workspace id for sorting — a lookup miss just sorts that window last,
// it can't add a window that isn't really there.
property var orderedToplevels: {
var genList = ToplevelManager.toplevels.values;
var hyprList = Hyprland.toplevels.values;
var out = [];
for (var i = 0; i < genList.length; i++) {
var gt = genList[i];
var wsId = 999999;
for (var j = 0; j < hyprList.length; j++) {
if (hyprList[j].wayland === gt) {
wsId = (hyprList[j].workspace && hyprList[j].workspace.id > 0) ? hyprList[j].workspace.id : 999999;
break;
}
}
out.push({ tl: gt, wsId: wsId });
}
out.sort(function (a, b) { return a.wsId - b.wsId; });
return out;
}
Rectangle { anchors.top: parent.top; width: parent.width; height: 1; color: Theme.border }
// --- Left bar toggle ---------------------------------------------
Rectangle {
id: leftToggle
anchors.left: parent.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
width: 32
height: 32
color: ltMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: root.leftBar.expanded ? 2 : 1
border.color: root.leftBar.expanded ? Theme.accent : Theme.border
Text {
anchors.centerIn: parent
text: root.leftBar.expanded ? "<" : ">"
color: root.leftBar.expanded ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 14
font.bold: true
}
MouseArea {
id: ltMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.leftBar.expanded = !root.leftBar.expanded;
if (!root.leftBar.expanded) root.leftBar.activePanel = "";
}
}
}
Row {
anchors.left: leftToggle.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 8
spacing: 4
Repeater {
model: root.workspacePills
delegate: Rectangle {
property var ws: modelData.ws
property int number: modelData.number
width: 32
height: 28
color: ws.focused ? Theme.accent : (wsMa.containsMouse ? Theme.bgAlt : "transparent")
border.width: 1
border.color: ws.urgent ? Theme.danger : Theme.border
Text {
anchors.centerIn: parent
text: number
color: ws.focused ? Theme.bg : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
MouseArea {
id: wsMa
anchors.fill: parent
hoverEnabled: true
onClicked: ws.activate()
}
}
}
}
Rectangle {
anchors.left: parent.left
anchors.leftMargin: 8 + 32 + 8 + (root.workspacePills.length * 36) + 4
anchors.top: parent.top
anchors.topMargin: 6
width: 1
height: 28
color: Theme.border
}
// --- Right bar toggle ---------------------------------------------
Rectangle {
id: rightToggle
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
width: 32
height: 32
color: rtMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: root.rightBar.expanded ? 2 : 1
border.color: root.rightBar.expanded ? Theme.accent : Theme.border
Text {
anchors.centerIn: parent
text: root.rightBar.expanded ? ">" : "<"
color: root.rightBar.expanded ? Theme.accent : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 14
font.bold: true
}
MouseArea {
id: rtMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.rightBar.expanded = !root.rightBar.expanded;
if (!root.rightBar.expanded) root.rightBar.activePanel = "";
}
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: 8 + 32 + 8 + (root.workspacePills.length * 36) + 16
anchors.right: rightToggle.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
spacing: 4
clip: true
Repeater {
model: root.orderedToplevels
delegate: Rectangle {
id: tlDelegate
property var tl: modelData.tl
// Resolved via the app's .desktop entry (matched by Wayland
// app-id) rather than trusting a raw icon name/path from
// the toplevel itself, which isn't exposed by the protocol.
property string iconSrc: {
if (!tl.appId) return "";
var entry = DesktopEntries.heuristicLookup(tl.appId);
if (!entry || !entry.icon) return "";
return Quickshell.iconPath(entry.icon, "");
}
width: Math.min(220, label.implicitWidth + (iconSrc !== "" ? 46 : 24))
height: 28
color: tl.activated ? Theme.accent : (tlMa.containsMouse ? Theme.bgAlt : Theme.bgAlt)
border.width: 1
border.color: root.ctxMenuToplevel === tl ? Theme.accent : Theme.border
IconImage {
id: appIcon
visible: tlDelegate.iconSrc !== ""
implicitSize: 16
anchors.left: parent.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
source: tlDelegate.iconSrc
}
Text {
id: label
anchors.left: appIcon.visible ? appIcon.right : parent.left
anchors.leftMargin: appIcon.visible ? 6 : 8
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: tl.title || tl.appId || "?"
color: tl.activated ? Theme.bg : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
elide: Text.ElideRight
}
MouseArea {
id: tlMa
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
if (root.ctxMenuToplevel === tl) { root.closeCtxMenu(); return; }
var pos = tlDelegate.mapToItem(null, 0, 0);
root.ctxMenuX = pos.x;
root.ctxMenuToplevel = tl;
return;
}
if (mouse.button === Qt.MiddleButton) tl.close();
else tl.activate();
}
}
}
}
}
// --- Taskbar entry context menu ------------------------------------
// Window control options exposed by the wlr-foreign-toplevel-management
// protocol (fullscreen/maximized are writable properties on any
// toplevel — there's no per-app capability flag to check, so these are
// always offered as a best-effort "if the app honors it" set).
// Loaded lazily (not instantiated until first opened) so the popup
// and its anchor only exist while actually needed.
Loader {
active: root.ctxMenuToplevel !== null
sourceComponent: PopupWindow {
id: ctxMenu
visible: true
anchor.window: root
anchor.rect.x: root.ctxMenuX
anchor.rect.y: 0
anchor.edges: Edges.Top
anchor.gravity: Edges.Top
anchor.margins.bottom: 4
implicitWidth: 150
implicitHeight: ctxCol.implicitHeight + 12
color: "transparent"
Rectangle {
anchors.fill: parent
color: Theme.bg
border.width: 1
border.color: Theme.border
Column {
id: ctxCol
x: 6
y: 6
width: parent.width - 12
spacing: 2
Repeater {
model: {
var tl = root.ctxMenuToplevel;
if (!tl) return [];
return [
{ label: tl.fullscreen ? "Exit Fullscreen" : "Fullscreen", action: "fullscreen", danger: false },
{ label: tl.maximized ? "Restore" : "Maximize", action: "maximize", danger: false },
{ label: "Close", action: "close", danger: true }
];
}
delegate: Rectangle {
width: parent.width
height: 26
color: itemMa.containsMouse ? Theme.bgAlt : "transparent"
Text {
anchors.left: parent.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: modelData.label
color: modelData.danger ? Theme.danger : Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
}
MouseArea {
id: itemMa
anchors.fill: parent
hoverEnabled: true
onClicked: {
var tl = root.ctxMenuToplevel;
if (tl) {
if (modelData.action === "fullscreen") tl.fullscreen = !tl.fullscreen;
else if (modelData.action === "maximize") tl.maximized = !tl.maximized;
else if (modelData.action === "close") tl.close();
}
root.closeCtxMenu();
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
// Small close button using a plain "x" glyph, not a nerd-font icon — the
// left-bar icons taught us those can silently fail to render.
Rectangle {
id: root
signal clicked()
width: 24
height: 24
color: mouseArea.containsMouse ? Theme.borderDim : "transparent"
border.width: 1
border.color: Theme.border
Text {
anchors.centerIn: parent
text: "x"
color: Theme.fgDim2
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,36 @@
import QtQuick
// Small "x" corner button for deleting a grid tile (wallpapers) — only
// meant to be shown while the tile itself is hovered, matching the
// hover-to-reveal pattern used for clearing a single notification.
Rectangle {
id: root
signal clicked()
width: 18
height: 18
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 4
z: 1
color: mouseArea.containsMouse ? Theme.danger : Theme.bg
border.width: 1
border.color: Theme.danger
Text {
anchors.centerIn: parent
text: "×"
color: mouseArea.containsMouse ? Theme.bg : Theme.danger
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import Quickshell
// 24h time + "day monthname year" date, using the system locale (pl_PL) for
// the month name — matches how Poland actually writes dates.
Column {
id: root
spacing: 2
SystemClock {
id: clock
precision: SystemClock.Minutes
enabled: true
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: Qt.formatTime(clock.date, "HH:mm")
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 20
font.bold: true
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: Qt.formatDate(clock.date, "d MMMM yyyy")
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
}

View file

@ -0,0 +1,33 @@
import QtQuick
Rectangle {
id: root
property string glyph: ""
property bool active: false
property color fg: Theme.fg
property color accent: Theme.accent
signal clicked()
width: 48
height: 48
color: mouseArea.containsMouse ? Theme.bgAlt : "transparent"
border.width: active ? 2 : 1
border.color: active ? accent : Theme.borderDim
radius: 0
Text {
anchors.centerIn: parent
text: root.glyph
color: root.active ? root.accent : root.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 21
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,162 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
PanelWindow {
id: root
required property var screen
property string activePanel: ""
property bool expanded: false
anchors { top: true; left: true; bottom: true }
// Collapsed = zero reserved space; expand/collapse is triggered from
// the taskbar (BottomBar), not a button on this bar itself.
implicitWidth: expanded ? 220 : 0
exclusiveZone: expanded ? 220 : 0
color: Theme.bg
focusable: false
Process { id: launcher; command: [] }
function launch(cmd) { launcher.command = cmd; launcher.running = true; }
// Only "booru" needs the popup's keyboard grab (it's the only panel
// with a text field).
function needsGrab(panel) { return panel === "booru"; }
function toggle(panel) {
if (root.activePanel === panel) {
root.activePanel = "";
return;
}
if (root.activePanel !== "" && root.needsGrab(root.activePanel) !== root.needsGrab(panel)) {
// Switching directly between a grabbing and a non-grabbing
// panel without closing the popup first doesn't work — an
// xdg_popup's grab can only be established when the surface
// is first created, not toggled on one that's already
// mapped. Force a close, then reopen on the next tick so the
// popup actually gets torn down and recreated with the right
// grab state.
root.activePanel = "";
Qt.callLater(function () { root.activePanel = panel; });
return;
}
root.activePanel = panel;
}
// Resolves the real, locale-correct Pictures folder at click time
// (e.g. ~/Obrazy on pl_PL) instead of assuming the English name.
Process {
id: picturesLauncher
command: ["sh", "-c", "thunar \"$(xdg-user-dir PICTURES)\""]
}
Column {
visible: root.expanded
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 14
spacing: 14
width: parent.width - 24
Column {
width: parent.width
spacing: 6
Text {
text: "SHORTCUTS"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
font.bold: true
}
SidebarRow {
label: "Dotfiles / NixOS"
onClicked: root.launch(["thunar", "/etc/nixos"])
}
SidebarRow {
label: "Pictures"
onClicked: picturesLauncher.running = true
}
}
Column {
width: parent.width
spacing: 6
Text {
text: "APPEARANCE"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
font.bold: true
}
SidebarRow {
label: "Theme"
active: root.activePanel === "theme"
onClicked: root.toggle("theme")
}
SidebarRow {
label: "Display Settings"
active: root.activePanel === "settings"
onClicked: root.toggle("settings")
}
SidebarRow {
label: "Wallpapers"
active: root.activePanel === "wallpapers"
onClicked: root.toggle("wallpapers")
}
SidebarRow {
label: "Booru"
active: root.activePanel === "booru"
onClicked: root.toggle("booru")
}
SidebarRow {
label: "Cursor"
active: root.activePanel === "cursor"
onClicked: root.toggle("cursor")
}
}
}
PanelWindow {
id: flyout
screen: root.screen
anchors { top: true; left: true }
// No margin here on purpose: Hyprland already insets layer-shell
// surfaces past other surfaces' exclusive zones on the same edge
// (confirmed via `hyprctl layers` — BottomBar gets the same
// treatment for free, sitting at x=220 with zero margin of its
// own). A margin of 220 here was stacking on top of that
// automatic inset, pushing the flyout 220px further right than
// intended and leaving a ~220px gap between it and the sidebar.
visible: root.activePanel !== ""
implicitWidth: 320
implicitHeight: flyoutContent.implicitHeight
exclusiveZone: 0
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
// On-demand layer-shell focus, not an xdg_popup grab: this used to
// be a PopupWindow with grabFocus, but an xdg_popup grab gets
// dismissed by the compositor the instant anything else so much as
// touches focus/surface state elsewhere — mapping mpvpaper's
// layer-shell surface (or even hyprpaper's own "wallpaper" IPC call
// recreating its surface) was enough to trigger that, silently
// closing the whole flyout AND dropping keyboard input, which is
// what let keystrokes fall through to whatever window was focused
// before the flyout opened instead of reaching the text field.
// OnDemand only claims focus when something inside is actually
// clicked, and isn't torn down by unrelated surfaces. Panels with
// no text input don't need it at all.
WlrLayershell.keyboardFocus: root.needsGrab(root.activePanel)
? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
LeftFlyout {
id: flyoutContent
anchors.fill: parent
screen: root.screen
activePanel: root.activePanel
onRequestClose: root.activePanel = ""
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,176 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Polkit
// Custom-themed polkit authentication agent, replacing hyprpolkitagent's
// default (unthemed, light) dialog. PolkitAgent/AuthFlow handle the actual
// D-Bus/PAM plumbing internally — this file is purely the UI around it.
//
// Rendered as a slim layer-shell bar anchored to the top-center of the
// screen (like mako's notification popups) rather than a normal toplevel
// window — this sidesteps Hyprland window placement entirely, so there's
// no need for float/center window rules.
Item {
id: root
PolkitAgent {
id: agent
}
PanelWindow {
id: dialogWindow
visible: agent.flow !== null
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "polkit-dialog"
anchors { top: true }
margins.top: 12
exclusiveZone: 0
focusable: visible
color: "transparent"
implicitWidth: 420
implicitHeight: content.implicitHeight + 24
onVisibleChanged: if (visible) responseField.forceActiveFocus()
Rectangle {
anchors.fill: parent
color: Theme.bg
border.width: 2
border.color: Theme.accent
Row {
id: content
x: 12
y: 12
width: parent.width - 24
spacing: 10
Rectangle {
width: 4
height: infoCol.implicitHeight
color: Theme.accent
}
Column {
id: infoCol
width: content.width - 4 - 10 - actionRow.width - 10
spacing: 4
Text {
text: agent.flow ? agent.flow.message : ""
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: true
elide: Text.ElideRight
width: parent.width
}
Text {
visible: !!(agent.flow && agent.flow.supplementaryMessage)
text: agent.flow ? agent.flow.supplementaryMessage : ""
color: (agent.flow && agent.flow.supplementaryIsError) ? Theme.danger : Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
wrapMode: Text.WordWrap
width: parent.width
}
Rectangle {
visible: !!(agent.flow && agent.flow.isResponseRequired)
width: parent.width
height: 26
color: Theme.bgAlt
border.width: 1
border.color: Theme.border
Text {
visible: responseField.text.length === 0
anchors.verticalCenter: parent.verticalCenter
x: 8
text: agent.flow ? agent.flow.inputPrompt : ""
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
TextInput {
id: responseField
anchors.fill: parent
anchors.margins: 6
echoMode: (agent.flow && agent.flow.responseVisible) ? TextInput.Normal : TextInput.Password
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
focus: true
Keys.onReturnPressed: submitResponse()
Keys.onEnterPressed: submitResponse()
Keys.onEscapePressed: if (agent.flow) agent.flow.cancelAuthenticationRequest()
function submitResponse() {
if (agent.flow) {
agent.flow.submit(text);
text = "";
}
}
}
}
}
Row {
id: actionRow
anchors.verticalCenter: infoCol.verticalCenter
spacing: 6
Rectangle {
width: 90
height: 26
color: authMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 2
border.color: Theme.accent
Text {
anchors.centerIn: parent
text: "Authenticate"
color: Theme.accent
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
font.bold: true
}
MouseArea {
id: authMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: responseField.submitResponse()
}
}
Rectangle {
width: 70
height: 26
color: cancelMa.containsMouse ? Theme.bgAlt : "transparent"
border.width: 1
border.color: Theme.border
Text {
anchors.centerIn: parent
text: "Cancel"
color: Theme.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 10
}
MouseArea {
id: cancelMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: if (agent.flow) agent.flow.cancelAuthenticationRequest()
}
}
}
}
}
}
}

View file

@ -0,0 +1,218 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import Quickshell.Wayland
PanelWindow {
id: root
required property var screen
property string activePanel: ""
property bool expanded: false
// Keeps the default sink's PwNode subscribed so audio properties
// (volume/muted) actually receive live updates.
PwObjectTracker {
objects: Pipewire.defaultAudioSink ? [Pipewire.defaultAudioSink] : []
}
anchors { top: true; right: true; bottom: true }
// Collapsed = zero reserved space; expand/collapse is triggered from
// the taskbar (BottomBar), not a button on this bar itself.
implicitWidth: expanded ? 220 : 0
exclusiveZone: expanded ? 220 : 0
color: Theme.bg
// Never take keyboard focus — this bar has no text input, and leaving
// it focusable was stealing the keyboard grab from things like slurp's
// screenshot selection overlay.
focusable: false
// Only "network" needs the popup's keyboard grab (hotspot/wifi
// password fields).
function needsGrab(panel) { return panel === "network"; }
function toggle(panel) {
if (root.activePanel === panel) {
root.activePanel = "";
return;
}
if (root.activePanel !== "" && root.needsGrab(root.activePanel) !== root.needsGrab(panel)) {
// See LeftBar.qml's toggle() for why: an xdg_popup's grab
// can't be toggled on an already-mapped surface, only
// established fresh — so force a close+reopen when crossing
// that boundary instead of just changing the panel in place.
root.activePanel = "";
Qt.callLater(function () { root.activePanel = panel; });
return;
}
root.activePanel = panel;
}
Column {
visible: root.expanded
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 14
spacing: 14
width: parent.width - 24
DigitalClock {
anchors.horizontalCenter: parent.horizontalCenter
}
// System tray (StatusNotifierItem) — background/status apps like
// Steam, Discord, KDE Connect that register a tray icon. Sits
// right under the clock, separate from the categorized sections
// below the divider.
Row {
id: trayRow
visible: SystemTray.items.values.length > 0
anchors.horizontalCenter: parent.horizontalCenter
spacing: 12
Repeater {
model: SystemTray.items.values
delegate: IconImage {
id: trayIcon
implicitSize: 18
source: Quickshell.iconPath(modelData.icon, "")
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (mouse.button === Qt.RightButton) {
if (modelData.hasMenu) {
var pos = trayIcon.mapToItem(null, 0, trayIcon.height);
modelData.display(root, pos.x, pos.y);
} else {
modelData.secondaryActivate();
}
} else {
modelData.activate();
}
}
}
}
}
}
Rectangle { width: parent.width; height: 1; color: Theme.borderDim }
Column {
width: parent.width
spacing: 6
Text {
text: "CONNECTIVITY"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
font.bold: true
}
SidebarRow {
label: "Wi-Fi & Network"
active: root.activePanel === "network"
onClicked: root.toggle("network")
}
SidebarRow {
label: "Bluetooth"
active: root.activePanel === "bluetooth"
onClicked: root.toggle("bluetooth")
}
SidebarRow {
label: "Messages"
active: root.activePanel === "messages"
onClicked: root.toggle("messages")
}
}
Column {
width: parent.width
spacing: 6
Text {
text: "SYSTEM"
color: Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 9
font.bold: true
}
SidebarRow {
label: "Screenshot"
active: root.activePanel === "screenshot"
onClicked: root.toggle("screenshot")
}
SidebarRow {
label: (Pipewire.defaultAudioSink && Pipewire.defaultAudioSink.audio && Pipewire.defaultAudioSink.audio.muted)
? "Volume (muted)" : "Volume"
active: root.activePanel === "volume"
onClicked: root.toggle("volume")
}
SidebarRow {
label: "Brightness"
active: root.activePanel === "brightness"
onClicked: root.toggle("brightness")
}
SidebarRow {
label: "Devices"
active: root.activePanel === "devices"
onClicked: root.toggle("devices")
}
}
}
SidebarRow {
visible: root.expanded
anchors.bottom: parent.bottom
anchors.bottomMargin: 14
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 24
label: "Power"
fg: Theme.danger
active: root.activePanel === "power"
onClicked: root.toggle("power")
}
PanelWindow {
id: flyout
screen: root.screen
anchors { top: true; right: true }
// No margin here on purpose: Hyprland already insets layer-shell
// surfaces past other surfaces' exclusive zones on the same edge
// (confirmed via `hyprctl layers` — BottomBar gets the same
// treatment for free, spanning right up to x=1700 with zero
// margin of its own). A margin of 220 here was stacking on top
// of that automatic inset, pushing the flyout 220px further left
// than intended and leaving a ~220px gap between it and the
// sidebar.
visible: root.activePanel !== ""
implicitWidth: 320
implicitHeight: content.implicitHeight
exclusiveZone: 0
color: "transparent"
WlrLayershell.layer: WlrLayer.Overlay
// On-demand layer-shell focus, not an xdg_popup grab: this used to
// be a PopupWindow with grabFocus, but an xdg_popup grab gets
// dismissed by the compositor the instant anything else so much as
// touches focus/surface state elsewhere (confirmed: even an
// unrelated hyprpaper/mpvpaper surface change was enough), which
// silently closed the whole flyout AND dropped keyboard input —
// that's what let keystrokes fall through to whatever window was
// focused before the flyout opened instead of reaching the text
// field. OnDemand only claims focus when something inside is
// actually clicked, and isn't torn down by unrelated surfaces.
// Panels with no text input don't need it at all.
WlrLayershell.keyboardFocus: root.needsGrab(root.activePanel)
? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
RightFlyout {
id: content
anchors.fill: parent
activePanel: root.activePanel
onRequestClose: root.activePanel = ""
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,36 @@
import QtQuick
Rectangle {
id: root
property string label: ""
property bool active: false
property color fg: Theme.fg
property color accent: Theme.accent
signal clicked()
width: parent ? parent.width : 200
height: 40
color: mouseArea.containsMouse ? Theme.bgAlt : "transparent"
border.width: active ? 2 : 1
border.color: active ? accent : Theme.borderDim
radius: 0
Text {
anchors.left: parent.left
anchors.leftMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: root.label
color: root.active ? root.accent : root.fg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 12
font.bold: root.active
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,48 @@
import QtQuick
// Reusable 0..1 drag slider with a visible handle, used by the volume and
// brightness panels.
Item {
id: root
property real value: 0
signal moved(real value)
height: 28
Rectangle {
id: track
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 6
color: Theme.bgAlt
border.width: 1
border.color: Theme.border
Rectangle {
width: track.width * Math.max(0, Math.min(1, root.value))
height: parent.height
color: Theme.accent
}
}
Rectangle {
id: handle
width: 14
height: 20
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(track.width - width, track.width * Math.max(0, Math.min(1, root.value)) - width / 2))
color: Theme.fg
border.width: 1
border.color: Theme.border
}
MouseArea {
anchors.fill: parent
onPositionChanged: (mouse) => { if (pressed) update(mouse.x) }
onPressed: (mouse) => update(mouse.x)
function update(x) {
var v = Math.max(0, Math.min(1, x / width));
root.value = v;
root.moved(v);
}
}
}

View file

@ -0,0 +1,142 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Single source of truth for every color used across the shell — swapping
// accent color or dark/light mode here changes it everywhere, instead of
// hunting down hardcoded hex values in 14 different files (which is what
// this replaced). Persisted to disk (same pattern as the wallpaper/booru
// settings) since PersistentProperties doesn't survive a real restart.
//
// Plain strings, not `color`-typed properties — QML still accepts them
// fine anywhere a `color:` binding is expected (auto-converts at the
// point of use), but keeping them as strings means they can also be
// handed straight to hypr-theme-set/gtk-theme-set as clean "#rrggbb"
// text without a color-to-string round trip appending an alpha suffix.
Item {
id: theme
property bool dark: true
property string accentName: "teal"
readonly property var accentPalette: ({
teal: "#8fbcbb",
blue: "#61afef",
green: "#98c379",
purple: "#c678dd",
orange: "#d19a66",
pink: "#ff79c6",
yellow: "#e5c07b"
})
readonly property var accentNames: ["teal", "blue", "green", "purple", "orange", "pink", "yellow"]
readonly property string accent: theme.accentPalette[theme.accentName] || theme.accentPalette.teal
readonly property string danger: "#e06c75"
// Dark palette (the original, and still the default)
readonly property string bgDark: "#0d0d0d"
readonly property string bgAltDark: "#1a1a1a"
readonly property string bgAlt2Dark: "#131313"
readonly property string fgDark: "#e0e0e0"
readonly property string fgDimDark: "#666666"
readonly property string fgDim2Dark: "#999999"
readonly property string borderDark: "#333333"
readonly property string borderDimDark: "#262626"
// Light palette
readonly property string bgLight: "#f0f0f0"
readonly property string bgAltLight: "#e2e2e2"
readonly property string bgAlt2Light: "#d5d5d5"
readonly property string fgLight: "#1a1a1a"
readonly property string fgDimLight: "#666666"
readonly property string fgDim2Light: "#888888"
readonly property string borderLight: "#c4c4c4"
readonly property string borderDimLight: "#d8d8d8"
readonly property string bg: theme.dark ? theme.bgDark : theme.bgLight
readonly property string bgAlt: theme.dark ? theme.bgAltDark : theme.bgAltLight
readonly property string bgAlt2: theme.dark ? theme.bgAlt2Dark : theme.bgAlt2Light
readonly property string fg: theme.dark ? theme.fgDark : theme.fgLight
readonly property string fgDim: theme.dark ? theme.fgDimDark : theme.fgDimLight
readonly property string fgDim2: theme.dark ? theme.fgDim2Dark : theme.fgDim2Light
readonly property string border: theme.dark ? theme.borderDark : theme.borderLight
readonly property string borderDim: theme.dark ? theme.borderDimDark : theme.borderDimLight
property string stateFile: Quickshell.stateDir + "/theme-settings.json"
function setAccent(name) {
if (theme.accentPalette[name] === undefined) return;
theme.accentName = name;
theme.save();
theme.syncSystem();
}
function setDark(isDark) {
theme.dark = isDark;
theme.save();
theme.syncSystem();
}
function randomAccent() {
var names = theme.accentNames;
var pick = names[Math.floor(Math.random() * names.length)];
theme.setAccent(pick);
}
function save() {
var json = JSON.stringify({ dark: theme.dark, accentName: theme.accentName });
saveProc.command = ["sh", "-c",
"mkdir -p \"$(dirname \"$2\")\" && printf '%s' \"$1\" > \"$2\"",
"--", json, theme.stateFile];
saveProc.running = true;
}
// Pushes the current palette out to the two things that don't read
// this file directly: Hyprland's window border colors (hardcoded in
// hyprland.conf otherwise) and GTK apps like Thunar (which used to be
// a static Nix-rebuild-only CSS file).
function syncSystem() {
hyprSyncProc.command = ["hypr-theme-set",
theme.accent.replace("#", ""), theme.border.replace("#", "")];
hyprSyncProc.running = true;
gtkSyncProc.command = ["gtk-theme-set",
theme.accent, theme.bg, theme.bgAlt, theme.fg, theme.fgDim, theme.border,
theme.dark ? "1" : "0"];
gtkSyncProc.running = true;
kittySyncProc.command = ["kitty-theme-set",
theme.accent, theme.bg, theme.bgAlt, theme.fg, theme.fgDim, theme.border];
kittySyncProc.running = true;
rofiSyncProc.command = ["rofi-theme-set",
theme.accent, theme.bg, theme.bgAlt, theme.fg, theme.fgDim, theme.border,
theme.dark ? "1" : "0"];
rofiSyncProc.running = true;
}
Process { id: saveProc }
Process { id: hyprSyncProc }
Process { id: gtkSyncProc }
Process { id: kittySyncProc }
Process { id: rofiSyncProc }
Process {
id: loadProc
command: ["cat", theme.stateFile]
stdout: StdioCollector {
onStreamFinished: {
try {
var d = JSON.parse(text);
if (typeof d.dark === "boolean") theme.dark = d.dark;
if (d.accentName) theme.accentName = d.accentName;
} catch (e) { /* no state file yet — defaults stand */ }
// Always sync on load, even if the state file didn't
// exist — Hyprland's border colors and GTK's CSS are
// otherwise still whatever was last written (or the
// hardcoded hyprland.conf default), independent of
// whether Quickshell itself just restarted.
theme.syncSystem();
}
}
}
Component.onCompleted: loadProc.running = true
}

View file

@ -0,0 +1,41 @@
import QtQuick
Row {
id: root
property bool checked: false
property string onLabel: "On"
property string offLabel: "Off"
signal toggled()
spacing: 8
Rectangle {
width: 44
height: 22
anchors.verticalCenter: parent.verticalCenter
color: root.checked ? Theme.accent : Theme.bgAlt
border.width: 1
border.color: Theme.border
Rectangle {
width: 18
height: 18
y: 2
x: root.checked ? parent.width - width - 2 : 2
color: root.checked ? Theme.bg : Theme.fgDim
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.toggled()
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.checked ? root.onLabel : root.offLabel
color: root.checked ? Theme.accent : Theme.fgDim
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
}

View file

@ -0,0 +1,27 @@
//@ pragma UseQApplication
// Quickshell entry point.
// Deployed as the whole directory -> ~/.config/quickshell via
// systemd.tmpfiles in modules/desktop.nix (relative imports need the full
// directory present, not just this file).
// UseQApplication is required for SystemTray items' native right-click
// menu (item.display()) — without it, Quickshell logs "Cannot display
// PlatformMenuEntry ... was not started in QApplication mode" and the
// menu silently does nothing.
import QtQuick
import Quickshell
ShellRoot {
PolkitDialog {}
Variants {
model: Quickshell.screens
Item {
required property var modelData
LeftBar { id: leftBarInstance; screen: modelData }
RightBar { id: rightBarInstance; screen: modelData }
BottomBar { screen: modelData; leftBar: leftBarInstance; rightBar: rightBarInstance }
}
}
}

115
dotfiles/rofi/config.rasi Normal file
View file

@ -0,0 +1,115 @@
/* Rofi launcher configuration
* Deployed to ~/.config/rofi/config.rasi via systemd.tmpfiles in
* modules/desktop.nix. Squared, sharp, no rounded corners.
*
* Colors live in theme.rasi (imported below), regenerated live by
* rofi-theme-set from the Quickshell Theme picker (dotfiles/quickshell/
* Theme.qml) — same accent/bg/fg/border values Hyprland, GTK, and Kitty
* all sync to. Rofi has no config watcher, but it's launched on demand
* rather than staying resident, so the next open just picks up whatever
* is on disk — no reload step needed.
*/
@import "theme.rasi"
configuration {
modi: "drun,run,window";
show-icons: true;
/* icon-theme comes from theme.rasi (imported above), set live by
rofi-theme-set to track dark/light mode. */
display-drun: "";
display-run: "";
display-window: "";
drun-display-format: "{name}";
font: "JetBrainsMono Nerd Font 11";
}
* {
background-color: @bg;
text-color: @fg;
margin: 0px;
padding: 0px;
spacing: 0px;
/* Rofi's built-in "default" theme (always loaded first, see
`rofi -dump-theme`) defines its own named vars — background,
foreground, lightbg, lightfg, blue, red — and alternate/active/
urgent row states read THOSE, not our bg/fg/accent. Explicit
`element.normal.alternate{...}` overrides below turned out not
to reliably win against them, so redefine the base theme's own
variables directly instead (same fix shape as GTK's
@define-color trick) — this is what actually stopped every
other row rendering in stock Solarized cream. */
background: @bg;
foreground: @fg;
lightbg: @bg-alt;
lightfg: @fg;
blue: @accent;
red: @accent;
}
window {
width: 600px;
border: 2px;
border-color: @border-color;
border-radius: 0px;
background-color: @bg;
}
mainbox {
children: [ "inputbar", "listview" ];
}
inputbar {
padding: 12px;
background-color: @bg-alt;
border: 0px 0px 2px 0px;
border-color: @border-color;
border-radius: 0px;
children: [ "prompt", "entry" ];
}
prompt {
text-color: @accent;
padding: 0px 8px 0px 0px;
}
entry {
placeholder: "Search...";
placeholder-color: @placeholder;
text-color: @fg;
}
listview {
lines: 8;
scrollbar: false;
padding: 8px;
}
element {
padding: 8px 12px;
border-radius: 0px;
spacing: 10px;
}
/* State selectors are dot-joined with no space (element.normal.normal,
not "element normal.normal") — the latter silently matches nothing,
which is why unselected rows never actually picked up @bg/@fg before
and fell back to rofi's built-in cream default on every other row. */
element.normal.normal, element.normal.alternate {
background-color: @bg;
text-color: @fg;
}
element.selected.normal, element.selected.alternate {
background-color: @accent;
text-color: @bg;
}
element-icon {
size: 20px;
}
element-text {
text-color: inherit;
vertical-align: 0.5;
}

185
hyprland/hyprland.conf Normal file
View file

@ -0,0 +1,185 @@
# Hyprland configuration
# Deployed to ~/.config/hypr/hyprland.conf via systemd.tmpfiles in
# modules/desktop.nix. Edit this file, not the symlink target.
# --- Monitors -----------------------------------------------------------
# Wildcard fallback for any monitor without its own line below.
monitor = , preferred, auto, 1
# DP-1 (AOC 27G2G8) explicitly at its real 144Hz spec: its EDID lists a
# bogus "239.96Hz" mode, and "preferred" trusts the EDID's preferred-timing
# flag — which picked that bogus mode here, silently running the monitor
# at a refresh rate it doesn't actually support. display.conf below
# normally overrides this anyway, but this is the safe cold-boot default
# if that file is ever missing/reset.
monitor = DP-1, 1920x1080@144.00Hz, auto, 1
# User-editable cursor theme (set via the Quickshell cursor picker); empty
# by default, created by systemd.tmpfiles so this source never fails.
source = ~/.config/hypr/cursor.conf
# User-set display mode/scale/transform (set via the Quickshell display
# picker) — overrides the generic "preferred" fallback above for whatever
# monitors have an explicit line in here. Empty by default.
source = ~/.config/hypr/display.conf
# --- Autostart ------------------------------------------------------------
# Polkit auth is handled by our own themed PolkitDialog inside quickshell
# (dotfiles/quickshell/PolkitDialog.qml) — hyprpolkitagent stays installed
# but unstarted as a manual emergency fallback ("hyprpolkitagent &" from a
# TTY) since only one agent can be registered at a time.
exec-once = mako
exec-once = hyprpaper
exec-once = quickshell
# GTK/Qt apps native-Wayland where possible, X11 fallback otherwise
env = XDG_CURRENT_DESKTOP,Hyprland
env = XDG_SESSION_TYPE,wayland
env = XDG_SESSION_DESKTOP,Hyprland
env = GDK_BACKEND,wayland,x11
env = QT_QPA_PLATFORM,wayland;xcb
env = QT_QPA_PLATFORMTHEME,qt6ct
# --- Input ------------------------------------------------------------
input {
kb_layout = pl
follow_mouse = 1
touchpad {
natural_scroll = true
}
sensitivity = 0
}
# --- General: sharp, square, minimal -----------------------------------
general {
gaps_in = 0
gaps_out = 0
border_size = 2
col.active_border = rgba(8fbcbbff)
col.inactive_border = rgba(333333ff)
layout = dwindle
resize_on_border = true
}
# Border colors set via the Quickshell Theme picker (dotfiles/quickshell/
# Theme.qml) — overrides the general{} defaults above for whatever's been
# picked. Empty by default; hypr-theme-set writes to it live.
source = ~/.config/hypr/theme.conf
# Zero rounding, no shadows/blur: sharp squared look everywhere
decoration {
rounding = 0
shadow {
enabled = false
}
blur {
enabled = false
}
}
animations {
enabled = false
}
dwindle {
preserve_split = true
}
master {
new_status = master
}
# Don't warp the cursor to whatever window gets focused (e.g. clicking a
# taskbar entry) — only move it when the user actually moves the mouse.
cursor {
no_warps = true
}
# --- Window rules ---------------------------------------------------------
# (none needed currently — the polkit dialog is a layer-shell surface via
# Quickshell's PanelWindow, not a normal toplevel, so it doesn't need a
# float/center window rule at all.)
misc {
disable_hyprland_logo = true
disable_splash_rendering = true
force_default_wallpaper = 0
}
# --- Keybinds ------------------------------------------------------------
$mod = SUPER
$terminal = kitty
$fileManager = thunar
$launcher = rofi -show drun
$imageViewer = imv
bind = $mod, Return, exec, $terminal
bind = $mod, T, exec, $terminal
bind = $mod, E, exec, $fileManager
bind = $mod, A, exec, $launcher
bind = $mod, D, exec, show-desktop-toggle
bind = $mod, Q, killactive,
bind = $mod SHIFT, Q, exit,
bind = $mod, V, togglefloating,
bind = $mod, F, fullscreen,
bind = $mod, P, pseudo,
bind = $mod, S, layoutmsg, togglesplit
# Focus movement (vim-style, plus arrow keys as the same thing)
bind = $mod, H, movefocus, l
bind = $mod, L, movefocus, r
bind = $mod, K, movefocus, u
bind = $mod, J, movefocus, d
bind = $mod, Left, movefocus, l
bind = $mod, Right, movefocus, r
bind = $mod, Up, movefocus, u
bind = $mod, Down, movefocus, d
# Move windows
bind = $mod SHIFT, H, movewindow, l
bind = $mod SHIFT, L, movewindow, r
bind = $mod SHIFT, K, movewindow, u
bind = $mod SHIFT, J, movewindow, d
bind = $mod SHIFT, Left, movewindow, l
bind = $mod SHIFT, Right, movewindow, r
bind = $mod SHIFT, Up, movewindow, u
bind = $mod SHIFT, Down, movewindow, d
# Workspaces 1-10
bind = $mod, 1, workspace, 1
bind = $mod, 2, workspace, 2
bind = $mod, 3, workspace, 3
bind = $mod, 4, workspace, 4
bind = $mod, 5, workspace, 5
bind = $mod, 6, workspace, 6
bind = $mod, 7, workspace, 7
bind = $mod, 8, workspace, 8
bind = $mod, 9, workspace, 9
bind = $mod, 0, workspace, 10
bind = $mod SHIFT, 1, movetoworkspace, 1
bind = $mod SHIFT, 2, movetoworkspace, 2
bind = $mod SHIFT, 3, movetoworkspace, 3
bind = $mod SHIFT, 4, movetoworkspace, 4
bind = $mod SHIFT, 5, movetoworkspace, 5
bind = $mod SHIFT, 6, movetoworkspace, 6
bind = $mod SHIFT, 7, movetoworkspace, 7
bind = $mod SHIFT, 8, movetoworkspace, 8
bind = $mod SHIFT, 9, movetoworkspace, 9
bind = $mod SHIFT, 0, movetoworkspace, 10
# Mouse: drag/resize floating windows
bindm = $mod, mouse:272, movewindow
bindm = $mod, mouse:273, resizewindow
# Screenshots — the "screenshot" script (region|full|window, clipboard|file|both).
# Super+Shift+S is the primary bind (this keyboard has no physical Print key) —
# it saves a file AND copies to clipboard, so it's never a silent no-op.
bind = , Print, exec, screenshot full clipboard
bind = $mod, Print, exec, screenshot region clipboard
bind = $mod SHIFT, Print, exec, screenshot region both
bind = $mod SHIFT, S, exec, screenshot region both
# Image viewer quick-launch on the screenshots folder
bind = $mod, I, exec, $imageViewer ~/Screenshots

7
hyprland/hyprpaper.conf Normal file
View file

@ -0,0 +1,7 @@
# Hyprpaper wallpaper daemon config.
# Deployed to ~/.config/hypr/hyprpaper.conf via systemd.tmpfiles in
# modules/desktop.nix. No static preload list here on purpose — wallpapers
# are set at runtime via IPC (`wallpaper-set <path>`) from the Quickshell
# wallpaper picker (dotfiles/quickshell/LeftFlyout.qml).
splash = false
ipc = true

76
modules/apps.nix Normal file
View file

@ -0,0 +1,76 @@
{ config, pkgs, ... }:
let
# hyprpolkitagent only ships its binary under libexec/, not bin/, so it
# never lands on $PATH — wrap it so `exec-once = hyprpolkitagent` in
# hyprland.conf can find it.
hyprpolkitagentBin = pkgs.writeShellScriptBin "hyprpolkitagent"
"exec ${pkgs.hyprpolkitagent}/libexec/hyprpolkitagent";
in
{
# Every installed package — GUI apps, CLI tools, and dev toolchains alike.
# If it's "install X", it goes here.
users.users."nexoniarz".packages = with pkgs; [
# Hyprland desktop toolchain (Wayland-native, lightweight, squared theme)
kitty # Terminal
thunar # File manager
thunar-archive-plugin
thunar-volman
tumbler # Thumbnails (paired with services.tumbler.enable)
rofi # Launcher (Wayland support built in since 2.0)
quickshell # Bar / UI shell
imv # Image viewer
mako # Notification daemon
grim # Screenshot capture
slurp # Screenshot region select
wl-clipboard # Wayland clipboard
hyprpolkitagentBin # Polkit authentication agent (wrapped, see `let` above)
hyprlock # Screen locker (power menu)
brightnessctl # Backlight brightness (laptop panels)
ddcutil # DDC/CI brightness (external monitors)
lm_sensors # sensors-detect helps ddcutil find I2C buses
hyprpaper # Wallpaper daemon, driven by the Quickshell picker (static images)
mpvpaper # Wallpaper daemon for video-loop (animated) wallpapers
networkmanager # nmcli, for the Wi-Fi hotspot toggle
xdg-user-dirs # Resolves localized folder names (Obrazy, not Pictures, on pl_PL)
bibata-cursors # Cursor theme(s) for the cursor picker
xcur2png # Renders Xcursor files to PNG for the cursor picker's previews
imagemagick # `identify`, used to pick the right cursor-preview size
# Apps
blender
gimp
inkscape
vesktop
tor-browser
ffmpeg-full
vlc
filezilla
chromium
# Games
prismlauncher
pkgs.osu-lazer-bin
# Dev: compilers, runtimes, languages
glibc
musl
cmake
gnumake # Provides the 'make' command
gcc
python3
openjdk25
claude-code # AI Developer Assistant
];
# Global command-line utilities available everywhere
environment.systemPackages = with pkgs; [
git
vim
fastfetch
gparted
firejail
gnupg
lmstudio
];
}

95
modules/desktop.nix Normal file
View file

@ -0,0 +1,95 @@
{ config, pkgs, ... }:
{
# --- Hyprland: Wayland compositor ---
programs.hyprland = {
enable = true;
xwayland.enable = true;
};
# --- greetd + tuigreet: minimal TUI login manager, launches Hyprland directly ---
services.greetd = {
enable = true;
settings.default_session = {
command = "${pkgs.tuigreet}/bin/tuigreet --time --remember --remember-session --cmd Hyprland";
user = "greeter";
};
};
# --- XDG portals: screen share, file pickers, etc. ---
xdg.portal = {
enable = true;
extraPortals = [ pkgs.xdg-desktop-portal-hyprland pkgs.xdg-desktop-portal-gtk ];
config.common.default = [ "hyprland" "gtk" ];
};
security.polkit.enable = true;
# Keyboard layout for console/XWayland/greetd; Hyprland's own input block
# in hyprland/hyprland.conf mirrors this.
services.xserver.xkb = {
layout = "pl";
variant = "";
};
# Thunar: thumbnails, trash/volume mount integration, settings storage
services.tumbler.enable = true;
services.gvfs.enable = true;
programs.xfconf.enable = true;
# Native Wayland backend for Electron/Chromium-based apps
environment.sessionVariables.NIXOS_OZONE_WL = "1";
# --- Dotfiles: symlink tracked configs into the user's home ---
# Editing /etc/nixos/hyprland or /etc/nixos/dotfiles takes effect on the
# next reload of the respective app (no rebuild needed for config-only
# tweaks; a rebuild re-runs the symlink if the target path itself changes).
systemd.tmpfiles.rules = [
"d /home/nexoniarz/.config/hypr 0755 nexoniarz users -"
"L+ /home/nexoniarz/.config/hypr/hyprland.conf - - - - /etc/nixos/hyprland/hyprland.conf"
"L+ /home/nexoniarz/.config/hypr/hyprpaper.conf - - - - /etc/nixos/hyprland/hyprpaper.conf"
"d /home/nexoniarz/.config/kitty 0755 nexoniarz users -"
"L+ /home/nexoniarz/.config/kitty/kitty.conf - - - - /etc/nixos/dotfiles/kitty/kitty.conf"
# Colors set live via the Quickshell Theme picker — see kitty.conf's
# `include theme.conf`. Empty by default; kitty-theme-set writes to it.
"f /home/nexoniarz/.config/kitty/theme.conf 0644 nexoniarz users - -"
"d /home/nexoniarz/.config/rofi 0755 nexoniarz users -"
"L+ /home/nexoniarz/.config/rofi/config.rasi - - - - /etc/nixos/dotfiles/rofi/config.rasi"
# Colors set live via the Quickshell Theme picker — see config.rasi's
# `@import "theme.rasi"`. Empty by default; rofi-theme-set writes to it.
"f /home/nexoniarz/.config/rofi/theme.rasi 0644 nexoniarz users - -"
"d /home/nexoniarz/.config/mako 0755 nexoniarz users -"
"L+ /home/nexoniarz/.config/mako/config - - - - /etc/nixos/dotfiles/mako/config"
# Quickshell is multi-file with relative imports, so the whole directory
# is symlinked rather than a single file.
"L+ /home/nexoniarz/.config/quickshell - - - - /etc/nixos/dotfiles/quickshell"
# $HOME/Wallpapers, not $HOME/Pictures/Wallpapers — this system's locale
# (pl_PL) means the real XDG Pictures dir is ~/Obrazy, not ~/Pictures.
# Living directly under $HOME sidesteps that entirely.
"d /home/nexoniarz/Wallpapers 0755 nexoniarz users -"
"d /home/nexoniarz/Screenshots 0755 nexoniarz users -"
# User-editable cursor theme choice (set live via the Quickshell cursor
# picker) — created empty if missing, never overwritten if it already
# has content, since it's runtime state rather than tracked config.
"f /home/nexoniarz/.config/hypr/cursor.conf 0644 nexoniarz users - -"
# Same deal for display settings (mode/scale/transform per monitor,
# set via the Quickshell display picker) — without this file backing
# them, they only ever applied live via hyprctl and got silently
# reset by hyprland.conf's generic fallback monitor rule any time
# Hyprland re-evaluated monitor state.
"f /home/nexoniarz/.config/hypr/display.conf 0644 nexoniarz users - -"
# Same deal for window border colors (accent/border, set live via the
# Quickshell Theme picker) — without this file, they only ever applied
# via hyprctl and would fall back to hyprland.conf's own general{}
# defaults any time Hyprland re-evaluated its config.
"f /home/nexoniarz/.config/hypr/theme.conf 0644 nexoniarz users - -"
];
}

48
modules/drivers.nix Normal file
View file

@ -0,0 +1,48 @@
{ config, lib, pkgs, ... }:
{
# Enable graphics driver support and 32-bit acceleration (for Steam/gaming)
hardware.graphics = {
enable = true;
enable32Bit = true;
};
#OpenTabletDriver
hardware.opentabletdriver.enable = true;
hardware.uinput.enable = true;
boot.kernelModules = [ "uinput" "v4l2loopback" ];
# Virtual webcam device so ffmpeg (fed by the GoPro's stream) can appear
# as a normal camera to apps like Discord. exclusive_caps=1 is required
# for Chromium/WebRTC-based apps to recognize it as a real capture device.
boot.extraModulePackages = [ config.boot.kernelPackages.v4l2loopback ];
boot.extraModprobeConfig = ''
options v4l2loopback devices=1 video_nr=10 card_label="GoPro Webcam" exclusive_caps=1
# PS4/PS5 controllers (e.g. DualSense) connect over Bluetooth then
# immediately disconnect — a well-known Linux Bluetooth stack issue
# where many adapters' Enhanced Retransmission Mode implementation is
# broken. Disabling ERTM is the standard, widely-documented fix.
options bluetooth disable_ertm=1
'';
# I2C bus, for DDC/CI monitor control (brightness) via ddcutil — this
# machine has no laptop backlight, only external DisplayPort monitors.
hardware.i2c.enable = true;
# Load NVIDIA driver for X11 and Wayland
services.xserver.videoDrivers = [ "nvidia" ];
hardware.nvidia = {
# Required settings
modesetting.enable = true;
# Enable NVIDIA Open GPU Kernel Modules (Turing architecture or newer)
open = true;
# Enable the NVIDIA settings menu
nvidiaSettings = true;
# Use the stable driver package matching current kernel
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
}

45
modules/scripts.nix Normal file
View file

@ -0,0 +1,45 @@
{ config, pkgs, ... }:
let
# Wrap external shell scripts into system executables
vaultToggle = pkgs.writeShellScriptBin "vault-toggle" (builtins.readFile ./scripts/vault-toggle.sh);
stripMetadata = pkgs.writeShellScriptBin "strip-metadata" (builtins.readFile ./scripts/strip-metadata.sh);
brightnessCtl = pkgs.writeShellScriptBin "brightness-ctl" (builtins.readFile ./scripts/brightness-ctl.sh);
showDesktopToggle = pkgs.writeShellScriptBin "show-desktop-toggle" (builtins.readFile ./scripts/show-desktop-toggle.sh);
wallpaperSet = pkgs.writeShellScriptBin "wallpaper-set" (builtins.readFile ./scripts/wallpaper-set.sh);
wallpaperDelete = pkgs.writeShellScriptBin "wallpaper-delete" (builtins.readFile ./scripts/wallpaper-delete.sh);
displaySet = pkgs.writeShellScriptBin "display-set" (builtins.readFile ./scripts/display-set.sh);
hyprThemeSet = pkgs.writeShellScriptBin "hypr-theme-set" (builtins.readFile ./scripts/hypr-theme-set.sh);
gtkThemeSet = pkgs.writeShellScriptBin "gtk-theme-set" (builtins.readFile ./scripts/gtk-theme-set.sh);
kittyThemeSet = pkgs.writeShellScriptBin "kitty-theme-set" (builtins.readFile ./scripts/kitty-theme-set.sh);
rofiThemeSet = pkgs.writeShellScriptBin "rofi-theme-set" (builtins.readFile ./scripts/rofi-theme-set.sh);
listCursorThemes = pkgs.writeShellScriptBin "list-cursor-themes" (builtins.readFile ./scripts/list-cursor-themes.sh);
cursorPreviewAll = pkgs.writeShellScriptBin "cursor-preview-all" (builtins.readFile ./scripts/cursor-preview-all.sh);
cursorSet = pkgs.writeShellScriptBin "cursor-set" (builtins.readFile ./scripts/cursor-set.sh);
screenshot = pkgs.writeShellScriptBin "screenshot" (builtins.readFile ./scripts/screenshot.sh);
hotspotStart = pkgs.writeShellScriptBin "hotspot-start" (builtins.readFile ./scripts/hotspot-start.sh);
in
{
# Install CLI dependencies required by the scripts
environment.systemPackages = with pkgs; [
gocryptfs
exiftool
jq
vaultToggle
stripMetadata
brightnessCtl
showDesktopToggle
wallpaperSet
wallpaperDelete
displaySet
hyprThemeSet
gtkThemeSet
kittyThemeSet
rofiThemeSet
listCursorThemes
cursorPreviewAll
cursorSet
screenshot
hotspotStart
];
}

View file

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Brightness control abstraction: prefers the kernel backlight class (laptop
# panels, via brightnessctl); falls back to DDC/CI over I2C (external
# monitors, via ddcutil) when no backlight device exists. Keeps the rest of
# the config (Quickshell's brightness popup) hardware-agnostic.
set -euo pipefail
has_backlight() {
[ -n "$(ls -A /sys/class/backlight 2>/dev/null)" ]
}
case "${1:-get}" in
get)
if has_backlight; then
brightnessctl -m | cut -d, -f4 | tr -d '%'
else
ddcutil getvcp 10 --brief 2>/dev/null | awk '{print $4}'
fi
;;
set)
pct="${2:?usage: brightness-ctl set <0-100>}"
if has_backlight; then
brightnessctl set "${pct}%" > /dev/null
else
ddcutil setvcp 10 "$pct" > /dev/null
fi
;;
*)
echo "usage: brightness-ctl [get|set <0-100>]" >&2
exit 1
;;
esac

View file

@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Extracts a small preview PNG (closest to 32px) for every installed
# cursor theme's "left_ptr" cursor into a cache directory, one per theme
# named "<theme>.png". Xcursor files are a binary format QML's Image
# element can't decode directly, so this is a one-time render step —
# already-cached themes are skipped.
#
# usage: cursor-preview-all <cache-dir>
set -euo pipefail
cache_dir="${1:?usage: cursor-preview-all <cache-dir>}"
mkdir -p "$cache_dir"
dirs=(
"$HOME/.icons"
"$HOME/.local/share/icons"
"/run/current-system/sw/share/icons"
"/etc/profiles/per-user/$USER/share/icons"
)
render_one() {
local theme="$1"
local src="$2"
local out="$cache_dir/$theme.png"
[ -e "$out" ] && return 0
local tmp
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' RETURN
xcur2png -q -d "$tmp" -c "$tmp/conf" -i 1 "$src" > /dev/null 2>&1 || return 0
local best=""
local best_diff=999999
for png in "$tmp"/*.png; do
[ -e "$png" ] || continue
local w
w=$(identify -format "%w" "$png" 2>/dev/null || echo 0)
local diff=$(( w > 32 ? w - 32 : 32 - w ))
if [ "$diff" -lt "$best_diff" ]; then
best_diff=$diff
best="$png"
fi
done
[ -n "$best" ] && cp "$best" "$out"
}
for d in "${dirs[@]}"; do
[ -d "$d" ] || continue
for theme_dir in "$d"/*/; do
[ -f "${theme_dir}cursors/left_ptr" ] || continue
render_one "$(basename "$theme_dir")" "${theme_dir}cursors/left_ptr"
done
done

16
modules/scripts/cursor-set.sh Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Applies a cursor theme immediately (hyprctl) and persists it across
# restarts by writing env vars to a file hyprland.conf sources on startup.
set -euo pipefail
theme="${1:?usage: cursor-set <theme-name> [size]}"
size="${2:-24}"
hyprctl setcursor "$theme" "$size" > /dev/null
mkdir -p "$HOME/.config/hypr"
cat > "$HOME/.config/hypr/cursor.conf" <<EOF
env = HYPRCURSOR_THEME,$theme
env = XCURSOR_THEME,$theme
env = XCURSOR_SIZE,$size
EOF

View file

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Applies a monitor mode/position/scale/transform immediately (hyprctl)
# AND persists it into ~/.config/hypr/display.conf, which hyprland.conf
# sources — without that file, hyprland.conf's generic fallback rule
# ("monitor = , preferred, auto, 1") is the only thing on record, and
# ANYTHING that makes Hyprland re-evaluate monitor state (observed: even
# changing the cursor theme via `hyprctl setcursor` does this) reapplies
# that fallback, silently dropping back to "preferred" refresh rate
# instead of whatever the user actually picked.
set -euo pipefail
mon="${1:?usage: display-set <monitor> <mode> <x> <y> <scale> [transform]}"
mode="${2:?mode required}"
x="${3:?x required}"
y="${4:?y required}"
scale="${5:?scale required}"
transform="${6:-0}"
descriptor="$mon,$mode,${x}x${y},$scale"
[ "$transform" != "0" ] && descriptor="$descriptor,transform,$transform"
conf="$HOME/.config/hypr/display.conf"
mkdir -p "$(dirname "$conf")"
touch "$conf"
line="monitor = $descriptor"
if grep -q "^monitor = $mon," "$conf" 2>/dev/null; then
tmp=$(mktemp)
awk -v prefix="monitor = $mon," -v newline="$line" '
index($0, prefix) == 1 { print newline; next } { print }
' "$conf" > "$tmp"
mv "$tmp" "$conf"
else
echo "$line" >> "$conf"
fi
hyprctl keyword monitor "$descriptor" > /dev/null

View file

@ -0,0 +1,119 @@
#!/usr/bin/env bash
# Regenerates GTK3/4 CSS + settings.ini to match the current Quickshell
# theme (accent color + dark/light mode). These used to be static,
# Nix-rebuild-only files (symlinked straight into the store) — this makes
# them live-editable instead, the same way cursor.conf/display.conf work,
# so the Theme picker can actually reach GTK apps like Thunar.
set -euo pipefail
accent="${1:?usage: gtk-theme-set <accent> <bg> <bgAlt> <fg> <fgDim> <border> <dark:1|0>}"
bg="${2:?bg required}"
bg_alt="${3:?bgAlt required}"
fg="${4:?fg required}"
fg_dim="${5:?fgDim required}"
border="${6:?border required}"
dark="${7:?dark required}"
dir3="$HOME/.config/gtk-3.0"
dir4="$HOME/.config/gtk-4.0"
mkdir -p "$dir3" "$dir4"
css_file=$(mktemp)
cat > "$css_file" <<EOF
/* Squared corners + accent colors, generated by gtk-theme-set from the
Quickshell Theme picker — GTK's CSS is a subset of real CSS: no
!important, and zero still needs a unit.
The @define-color block matters more than it looks: plain selector
overrides (window, .background, .view, ...) only get you so far —
Thunar's icon-view content area and sidebar tree view turned out to
pull their actual fill color from these named theme variables
internally, not from the generic background-color cascade. Verified
live: without redefining theme_base_color specifically, the sidebar
and file grid stayed white no matter how broad the selector list got. */
@define-color theme_bg_color $bg;
@define-color theme_base_color $bg;
@define-color theme_fg_color $fg;
@define-color theme_text_color $fg;
@define-color theme_selected_bg_color $accent;
@define-color theme_selected_fg_color $bg;
@define-color borders $border;
* {
border-radius: 0px;
-gtk-outline-radius: 0px;
}
window, .background, .view, iconview, treeview, treeview.view, list, row,
notebook, notebook > stack, scrolledwindow, viewport {
background-color: $bg;
color: $fg;
}
iconview.cell, iconview text {
background-color: $bg;
color: $fg;
}
treeview.view:selected, row:selected, iconview:selected {
background-color: $accent;
color: $bg;
}
headerbar, .titlebar {
background-color: $bg_alt;
color: $fg;
border-color: $border;
box-shadow: none;
}
button {
border-color: $border;
}
button:hover {
background-color: $bg_alt;
}
selection, *:selected {
background-color: $accent;
color: $bg;
}
EOF
cp "$css_file" "$dir3/gtk.css"
cp "$css_file" "$dir4/gtk.css"
rm -f "$css_file"
prefer_dark="false"
theme_name="Adwaita"
icon_theme="Gruvbox-Plus-Light"
if [ "$dark" = "1" ]; then
prefer_dark="true"
theme_name="Adwaita-dark"
icon_theme="Gruvbox-Plus-Dark"
fi
ini_file=$(mktemp)
cat > "$ini_file" <<EOF
[Settings]
gtk-application-prefer-dark-theme=$prefer_dark
gtk-theme-name=$theme_name
gtk-icon-theme-name=$icon_theme
gtk-font-name=Noto Sans, 10
gtk-enable-animations=true
EOF
cp "$ini_file" "$dir3/settings.ini"
cp "$ini_file" "$dir4/settings.ini"
rm -f "$ini_file"
# settings.ini alone isn't enough: GTK3 binds gtk-theme-name/icon-theme-name
# to org.gnome.desktop.interface via GSettings whenever that schema and a
# dconf backend are present (they are here — dconf-service was already
# running), and that binding wins over settings.ini unconditionally. This
# machine's dconf database had hard values left over (gtk-theme='Breeze',
# icon-theme='breeze') that silently overrode every settings.ini edit —
# which is why neither accent/dark-mode nor the Gruvbox icon switch ever
# reached Thunar. Writing the same values to dconf keeps both sources in
# sync so it doesn't matter which one GTK actually reads from.
if command -v dconf >/dev/null 2>&1; then
color_scheme="default"
[ "$dark" = "1" ] && color_scheme="prefer-dark"
dconf write /org/gnome/desktop/interface/gtk-theme "'$theme_name'"
dconf write /org/gnome/desktop/interface/icon-theme "'$icon_theme'"
dconf write /org/gnome/desktop/interface/color-scheme "'$color_scheme'"
fi

View file

@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Starts a Wi-Fi hotspot with explicit security/band control — the simple
# `nmcli device wifi hotspot` one-liner doesn't expose those.
# hotspot-start <ssid> <password> <wpa2|wpa3> <auto|bg|a>
set -euo pipefail
ssid="${1:?usage: hotspot-start <ssid> <password> <wpa2|wpa3> <auto|bg|a>}"
password="${2:?password required}"
security="${3:-wpa2}"
band="${4:-auto}"
iface=$(nmcli -t -f DEVICE,TYPE device | awk -F: '$2=="wifi"{print $1; exit}')
if [ -z "$iface" ]; then
echo "No Wi-Fi device found" >&2
exit 1
fi
# Idempotent: drop any previous Hotspot profile from an earlier run first.
nmcli connection delete Hotspot >/dev/null 2>&1 || true
key_mgmt="wpa-psk"
[ "$security" = "wpa3" ] && key_mgmt="sae"
nmcli connection add type wifi ifname "$iface" con-name Hotspot autoconnect no ssid "$ssid" > /dev/null
nmcli connection modify Hotspot 802-11-wireless.mode ap
if [ "$band" != "auto" ]; then
nmcli connection modify Hotspot 802-11-wireless.band "$band"
fi
nmcli connection modify Hotspot wifi-sec.key-mgmt "$key_mgmt"
nmcli connection modify Hotspot wifi-sec.psk "$password"
nmcli connection modify Hotspot ipv4.method shared
nmcli connection up Hotspot

View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Applies Hyprland's active/inactive window border colors immediately AND
# persists them into ~/.config/hypr/display.conf's sibling, theme.conf
# (sourced by hyprland.conf after the static general{} block, so it
# overrides those hardcoded defaults) — without that file, a config
# reload/restart would silently revert to whatever's hardcoded in
# hyprland.conf, same class of bug as the display-settings one.
set -euo pipefail
accent="${1:?usage: hypr-theme-set <accent_hex_no_hash> <border_hex_no_hash>}"
border="${2:?border required}"
hyprctl keyword general:col.active_border "rgba(${accent}ff)" > /dev/null
hyprctl keyword general:col.inactive_border "rgba(${border}ff)" > /dev/null
conf="$HOME/.config/hypr/theme.conf"
mkdir -p "$(dirname "$conf")"
cat > "$conf" <<EOF
general {
col.active_border = rgba(${accent}ff)
col.inactive_border = rgba(${border}ff)
}
EOF

View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Regenerates ~/.config/kitty/theme.conf (included from kitty.conf) to
# match the current Quickshell theme. No explicit reload needed — kitty
# has a built-in config-file watcher (visible as `kitten __watch_conf__`
# in its process list) that also watches included files, so this takes
# effect live in already-open kitty windows.
set -euo pipefail
accent="${1:?usage: kitty-theme-set <accent> <bg> <bgAlt> <fg> <fgDim> <border>}"
bg="${2:?bg required}"
bg_alt="${3:?bgAlt required}"
fg="${4:?fg required}"
fg_dim="${5:?fgDim required}"
border="${6:?border required}"
conf="$HOME/.config/kitty/theme.conf"
mkdir -p "$(dirname "$conf")"
cat > "$conf" <<EOF
background $bg
foreground $fg
selection_background $border
selection_foreground $fg
cursor $fg
active_border_color $accent
inactive_border_color $border
active_tab_background $bg_alt
active_tab_foreground $fg
inactive_tab_background $bg
inactive_tab_foreground $fg_dim
EOF

View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Lists installed Xcursor themes by scanning the real XCURSOR_PATH locations
# for a "cursors" subdirectory — no hardcoded theme names.
set -euo pipefail
dirs=(
"$HOME/.icons"
"$HOME/.local/share/icons"
"/run/current-system/sw/share/icons"
# Packages installed via users.users.<name>.packages (as opposed to
# environment.systemPackages) land here, not under
# /run/current-system/sw — this is where bibata-cursors actually is.
"/etc/profiles/per-user/$USER/share/icons"
)
for d in "${dirs[@]}"; do
[ -d "$d" ] || continue
for theme in "$d"/*/; do
[ -d "${theme}cursors" ] || continue
basename "$theme"
done
done | sort -u

View file

@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Regenerates ~/.config/rofi/theme.rasi (imported by config.rasi) to match
# the current Quickshell theme. Rofi has no config-file watcher — unlike
# kitty, it's launched on demand rather than staying resident, so the next
# time it's opened it just reads whatever is on disk. No reload step needed.
set -euo pipefail
accent="${1:?usage: rofi-theme-set <accent> <bg> <bgAlt> <fg> <fgDim> <border> <dark:1|0>}"
bg="${2:?bg required}"
bg_alt="${3:?bgAlt required}"
fg="${4:?fg required}"
fg_dim="${5:?fgDim required}"
border="${6:?border required}"
dark="${7:?dark required}"
icon_theme="Gruvbox-Plus-Light"
if [ "$dark" = "1" ]; then
icon_theme="Gruvbox-Plus-Dark"
fi
conf="$HOME/.config/rofi/theme.rasi"
mkdir -p "$(dirname "$conf")"
cat > "$conf" <<EOF
* {
bg: ${bg}ee;
bg-alt: ${bg_alt}ee;
fg: ${fg}ff;
accent: ${accent}ff;
border-color: ${border}ff;
placeholder: ${fg_dim}ff;
}
configuration {
icon-theme: "$icon_theme";
}
EOF

51
modules/scripts/screenshot.sh Executable file
View file

@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Unified screenshot helper.
# screenshot <region|full|window> <clipboard|file|both>
set -euo pipefail
mode="${1:-region}"
action="${2:-both}"
geom=()
case "$mode" in
region)
sel="$(slurp)" || exit 0
[ -z "$sel" ] && exit 0
geom=(-g "$sel")
;;
window)
win="$(hyprctl activewindow -j)"
at_x=$(echo "$win" | jq -r '.at[0]')
at_y=$(echo "$win" | jq -r '.at[1]')
w=$(echo "$win" | jq -r '.size[0]')
h=$(echo "$win" | jq -r '.size[1]')
geom=(-g "${at_x},${at_y} ${w}x${h}")
;;
full)
geom=()
;;
*)
echo "unknown mode: $mode (expected region|full|window)" >&2
exit 1
;;
esac
mkdir -p "$HOME/Screenshots"
file="$HOME/Screenshots/$(date +%Y-%m-%d_%H-%M-%S).png"
case "$action" in
clipboard)
grim "${geom[@]}" - | wl-copy
;;
file)
grim "${geom[@]}" "$file"
;;
both)
grim "${geom[@]}" "$file"
wl-copy < "$file"
;;
*)
echo "unknown action: $action (expected clipboard|file|both)" >&2
exit 1
;;
esac

View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
# "Show desktop" for Hyprland: stashes every window on the active workspace
# into a hidden special workspace, and restores each one to the workspace
# it came from on the next press — regardless of which workspace you're on
# when you press it again (a single global state file, not one keyed by
# "current workspace", which broke the moment you switched workspaces
# in between).
set -euo pipefail
state_file="${XDG_RUNTIME_DIR:-/tmp}/hypr-show-desktop.json"
if [ -s "$state_file" ]; then
jq -c '.[]' "$state_file" | while IFS= read -r entry; do
addr=$(echo "$entry" | jq -r '.address')
ws=$(echo "$entry" | jq -r '.workspace')
hyprctl dispatch movetoworkspacesilent "$ws,address:$addr" > /dev/null
done
rm -f "$state_file"
else
ws_id=$(hyprctl activeworkspace -j | jq -r '.id')
hyprctl clients -j | jq -c --argjson ws "$ws_id" \
'[.[] | select(.workspace.id == $ws) | {address: .address, workspace: $ws}]' > "$state_file"
jq -r '.[].address' "$state_file" | while IFS= read -r addr; do
[ -n "$addr" ] && hyprctl dispatch movetoworkspacesilent "special:showdesktop,address:$addr" > /dev/null
done
fi

View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
if [ $# -eq 0 ]; then
echo "Usage: strip-metadata <file_or_directory>"
exit 1
fi
TARGET="$1"
echo "[*] Stripping metadata from: $TARGET"
# Remove all embedded metadata while keeping original filenames
if command -v exiftool &> /dev/null; then
exiftool -all= -overwrite_original -r "$TARGET"
echo "[+] Exif metadata removed."
else
echo "[!] Warning: exiftool is not installed."
fi
# Reset timestamps (atime and mtime) to Epoch (1970-01-01 00:00:00)
echo "[*] Resetting timestamps..."
find "$TARGET" -exec touch -t 197001010000.00 {} +
echo "[+] Operation completed successfully."

30
modules/scripts/vault-toggle.sh Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
CIPHER_DIR="/run/media/nexoniarz/Lukas/.vault_cipher"
MOUNT_DIR="$HOME/Vault"
mkdir -p "$CIPHER_DIR" "$MOUNT_DIR"
if mountpoint -q "$MOUNT_DIR"; then
echo "[*] Synchronizing buffers and unmounting Vault..."
sync
if fusermount -u "$MOUNT_DIR"; then
echo "[+] Vault unmounted successfully."
else
echo "[!] Failed to unmount Vault. Check if any application is using $MOUNT_DIR"
exit 1
fi
else
echo "[*] Mounting Vault..."
if [ ! -f "$CIPHER_DIR/gocryptfs.conf" ]; then
echo "[!] Container not initialized. Initializing new gocryptfs vault..."
gocryptfs -init "$CIPHER_DIR" || exit 1
fi
if gocryptfs -allow_other "$CIPHER_DIR" "$MOUNT_DIR"; then
echo "[+] Vault mounted successfully at $MOUNT_DIR"
else
echo "[!] Mount failed."
exit 1
fi
fi

View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Removes a wallpaper by moving it to the freedesktop.org trash
# (~/.local/share/Trash), the same mechanism Thunar's own delete uses —
# so it shows up in Thunar's Trash and can be restored from there,
# instead of `rm` permanently destroying it with no way back.
set -euo pipefail
path="${1:?usage: wallpaper-delete <path>}"
[ -e "$path" ] || exit 0
trash_dir="$HOME/.local/share/Trash"
mkdir -p "$trash_dir/files" "$trash_dir/info"
name="$(basename "$path")"
dest="$trash_dir/files/$name"
info="$trash_dir/info/$name.trashinfo"
n=1
while [ -e "$dest" ] || [ -e "$info" ]; do
dest="$trash_dir/files/${name}.$n"
info="$trash_dir/info/${name}.$n.trashinfo"
n=$((n + 1))
done
cat > "$info" <<EOF
[Trash Info]
Path=$path
DeletionDate=$(date +%Y-%m-%dT%H:%M:%S)
EOF
mv "$path" "$dest"

View file

@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Sets the desktop wallpaper — static images go through hyprpaper's IPC,
# video files loop via mpvpaper. Only one backend should ever be driving
# the background at a time, so switching to one always tears the other
# down first.
#
# usage: wallpaper-set <path> [fit_mode] [muted] [speed] [auto_pause]
# fit_mode: cover|contain (static images only, default cover)
# muted: 1|0 (animated only, default 1)
# speed: playback speed (animated only, default 1.0)
# auto_pause: 1|0 (animated only — mpvpaper -p, pauses
# playback while the wallpaper is hidden
# behind a fullscreen window, default 1)
set -euo pipefail
image="${1:?usage: wallpaper-set <path> [fit_mode] [muted] [speed] [auto_pause]}"
fit_mode="${2:-cover}"
muted="${3:-1}"
speed="${4:-1.0}"
auto_pause="${5:-1}"
case "${image,,}" in
*.mp4|*.webm|*.mkv|*.mov|*.gif)
# -9: plain SIGTERM doesn't reliably kill mpvpaper (verified live —
# it survives pkill -f without -9), which would leave the old
# instance still holding the layer-shell surface underneath a new
# one. The short sleep gives it a moment to actually release that
# surface before a replacement tries to claim it.
pkill -9 -f mpvpaper > /dev/null 2>&1 || true
sleep 0.3
mpv_opts="loop-playlist=inf speed=$speed"
[ "$muted" = "1" ] && mpv_opts="no-audio $mpv_opts"
mpvpaper_flags=()
[ "$auto_pause" = "1" ] && mpvpaper_flags+=("-p")
# setsid + disown so mpvpaper (which runs forever, looping the
# video) survives after this script exits.
setsid mpvpaper "${mpvpaper_flags[@]}" -o "$mpv_opts" '*' "$image" \
> /dev/null 2>&1 < /dev/null &
disown
;;
*)
# -9: plain SIGTERM doesn't reliably kill mpvpaper (verified live —
# it survives pkill -f without -9), which would leave the old
# instance still holding the layer-shell surface underneath a new
# one. The short sleep gives it a moment to actually release that
# surface before a replacement tries to claim it.
pkill -9 -f mpvpaper > /dev/null 2>&1 || true
sleep 0.3
# This hyprpaper version's IPC only exposes a single `wallpaper`
# request (confirmed via `hyprctl hyprpaper --help`) — it loads
# and swaps the image in one call. preload/unload/listloaded
# requests it used to need don't exist anymore and error out with
# "invalid hyprpaper request", which — combined with `set -e` —
# was aborting this script before it ever got here.
hyprctl hyprpaper wallpaper ",$image,$fit_mode" > /dev/null
;;
esac

204
modules/system.nix Normal file
View file

@ -0,0 +1,204 @@
{ config, lib, pkgs, ... }:
{
# Bootloader & Kernel
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.loader.systemd-boot.configurationLimit = 5;
boot.kernelPackages = pkgs.linuxPackages;
boot.supportedFilesystems = [ "fuse" ];
# Networking
networking.hostName = "nixos";
networking.networkmanager.enable = true;
# Firewall Configuration
networking.firewall = {
enable = true;
# Allow local ping diagnostic queries
allowPing = true;
allowedTCPPortRanges = [ { from = 1714; to = 1764; } ];
allowedUDPPortRanges = [ { from = 1714; to = 1764; } ];
allowedTCPPorts = [ 80 443 ];
# allowedUDPPorts = [ ];
};
# Time zone and Internationalization
time.timeZone = "Europe/Warsaw";
i18n.defaultLocale = "pl_PL.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "pl_PL.UTF-8";
LC_IDENTIFICATION = "pl_PL.UTF-8";
LC_MEASUREMENT = "pl_PL.UTF-8";
LC_MONETARY = "pl_PL.UTF-8";
LC_NAME = "pl_PL.UTF-8";
LC_NUMERIC = "pl_PL.UTF-8";
LC_PAPER = "pl_PL.UTF-8";
LC_TELEPHONE = "pl_PL.UTF-8";
LC_TIME = "pl_PL.UTF-8";
};
nix.settings.auto-optimise-store = true;
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
# Console keymap
console.keyMap = "pl2";
# Sound and Audio (Pipewire)
security.rtkit.enable = true;
services.pulseaudio.enable = false;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
hardware.bluetooth = {
enable = true;
powerOnBoot = true;
settings = {
General = {
# Shows battery charge of connected devices on supported
# Bluetooth adapters. Defaults to 'false'.
Experimental = true;
# When enabled other devices can connect faster to us, however
# the tradeoff is increased power consumption. Defaults to
# 'false'.
FastConnectable = true;
};
Policy = {
# Enable all controllers when they are found. This includes
# adapters present on start as well as adapters that are plugged
# in later on. Defaults to 'true'.
AutoEnable = true;
};
};
};
services.blueman.enable = true;
#Firejail
programs.firejail = {
enable = true;
};
#FUSE shit
programs.fuse.userAllowOther = true;
# Enable GnuPG Agent with GUI pinentry support
programs.gnupg.agent = {
enable = true;
enableSSHSupport = true;
pinentryPackage = pkgs.pinentry-rofi; # Reuses the Rofi theme, no extra toolkit
};
security.sudo.extraConfig = ''
Defaults pwfeedback
'';
# Printing services
services.printing.enable = true;
nixpkgs.config.allowUnfree = true;
fonts.packages = with pkgs; [
corefonts
dejavu_fonts
noto-fonts
noto-fonts-cjk-sans
noto-fonts-color-emoji
nerd-fonts.fira-code
nerd-fonts.jetbrains-mono
nerd-fonts.hack
inter
liberation_ttf
roboto
];
# --- Loader shim for running non-Nix-packaged binaries (e.g. the official
# Tor Browser bundle) that expect a standard FHS dynamic linker/libraries. ---
programs.nix-ld.enable = true;
programs.nix-ld.libraries = with pkgs; [
glib gtk3 gdk-pixbuf pango cairo atk
dbus dbus-glib fontconfig freetype
libx11 libxext libxrender libxtst
libxi libxcomposite libxdamage libxfixes
libxrandr libxcursor libxcb libxkbcommon
alsa-lib mesa zlib stdenv.cc.cc
];
environment.systemPackages = [ pkgs.file ];
# --- Hardening: swap, kernel sysctls, DNS, MAC randomization ---
# Swap: replace disk swap with compressed in-RAM swap. Nothing sensitive
# (decrypted vault contents, browser memory, GPG session data) ever gets
# written to disk in plaintext via swap. Also faster than disk swap under
# memory pressure.
zramSwap.enable = true;
swapDevices = lib.mkForce [ ];
# systemd's gpt-auto-generator independently re-discovers and activates any
# GPT partition tagged as Linux swap, regardless of the swapDevices list
# above — this disables that auto-discovery so the old disk swap partition
# actually stays off. Doesn't affect /, /home, /nix, /boot: those are
# already explicitly declared in hardware-configuration.nix, not relying on
# gpt-auto. Takes effect on next reboot.
boot.kernelParams = [ "systemd.gpt_auto=0" ];
# Kernel/system hardening sysctls
boot.kernel.sysctl = {
# Hide kernel pointers from unprivileged users (blocks a common
# local info-leak used in exploit chains).
"kernel.kptr_restrict" = 2;
# Restrict dmesg access to root.
"kernel.dmesg_restrict" = 1;
# Reverse-path filtering: drop packets with spoofed source addresses.
"net.ipv4.conf.all.rp_filter" = 1;
"net.ipv4.conf.default.rp_filter" = 1;
# Don't accept/send ICMP redirects (blocks a classic local MITM trick).
"net.ipv4.conf.all.accept_redirects" = false;
"net.ipv4.conf.default.accept_redirects" = false;
"net.ipv6.conf.all.accept_redirects" = false;
"net.ipv6.conf.default.accept_redirects" = false;
"net.ipv4.conf.all.send_redirects" = false;
"net.ipv4.conf.default.send_redirects" = false;
};
# Crash dumps can contain decrypted memory contents, passwords, keys —
# never write them to disk.
systemd.coredump.enable = false;
# Blocks kexec-based kernel image tampering.
security.protectKernelImage = true;
# DNS-over-TLS system-wide (Tor Browser already routes its own DNS
# through Tor, so this only affects everything else). Cloudflare + Quad9
# both support DoT; "allow-downgrade" avoids hard failures on domains
# with broken DNSSEC deployment.
networking.nameservers = [ "1.1.1.1#cloudflare-dns.com" "9.9.9.9#dns.quad9.net" ];
services.resolved = {
enable = true;
settings.Resolve = {
DNSSEC = "allow-downgrade";
DNSOverTLS = "true";
};
};
networking.networkmanager.dns = "systemd-resolved";
# MAC address randomization.
# WiFi: fully random per connection (you're on ethernet now, but this
# protects you if you ever use WiFi).
# Ethernet: "stable" — a consistent pseudonymous MAC per connection
# profile rather than your real hardware MAC, so it won't break any
# static DHCP reservation/MAC filtering on your OpenWRT router, while
# still not exposing your real hardware identity.
networking.networkmanager.wifi.macAddress = "random";
networking.networkmanager.wifi.scanRandMacAddress = true;
networking.networkmanager.ethernet.macAddress = "stable";
}

35
modules/theme.nix Normal file
View file

@ -0,0 +1,35 @@
{ config, pkgs, lib, ... }:
{
# Dark GTK theme (built into GTK3/4's Adwaita since 3.20) for Thunar and
# other GTK apps, as a starting point. The actual palette (accent color,
# dark/light) is no longer static here — it's generated at runtime by
# gtk-theme-set (modules/scripts/gtk-theme-set.sh), called from the
# Quickshell Theme picker (dotfiles/quickshell/Theme.qml) every time the
# theme changes, so switching accent/mode there actually reaches GTK
# apps instead of requiring a rebuild.
environment.systemPackages = with pkgs; [
gnome-themes-extra
adwaita-icon-theme
gruvbox-plus-icons
dconf
];
# No environment.sessionVariables.GTK_THEME here on purpose: GTK_THEME
# takes priority over settings.ini's gtk-theme-name for GTK3 apps, and
# as a session env var it's fixed at login — it can't be changed live.
# A leftover "Adwaita:dark" override here was silently overriding
# whatever gtk-theme-set wrote to settings.ini, permanently forcing
# dark mode and blocking the Theme picker from ever reaching Thunar.
systemd.tmpfiles.rules = [
"d /home/nexoniarz/.config/gtk-3.0 0755 nexoniarz users -"
"d /home/nexoniarz/.config/gtk-4.0 0755 nexoniarz users -"
# `f`, not `L+`: created empty only if missing, never overwritten on
# rebuild — gtk-theme-set owns the actual content from here on.
"f /home/nexoniarz/.config/gtk-3.0/gtk.css 0644 nexoniarz users - -"
"f /home/nexoniarz/.config/gtk-4.0/gtk.css 0644 nexoniarz users - -"
"f /home/nexoniarz/.config/gtk-3.0/settings.ini 0644 nexoniarz users - -"
"f /home/nexoniarz/.config/gtk-4.0/settings.ini 0644 nexoniarz users - -"
];
}

27
modules/users.nix Normal file
View file

@ -0,0 +1,27 @@
{ config, pkgs, ... }:
{
# Primary user account configuration (Access and settings only)
users.users."nexoniarz" = {
isNormalUser = true;
description = "Nexoniarz";
extraGroups = [ "networkmanager" "wheel" "fuse" "i2c" "video" ];
shell = pkgs.zsh;
};
# Default shell (ZSH)
programs.zsh.enable = true;
# Web browser configuration
programs.firefox.enable = true;
# Enable Steam with optimized system configurations
programs.steam = {
enable = true;
remotePlay.openFirewall = true;
dedicatedServer.openFirewall = true;
};
# Enable Flatpak support
services.flatpak.enable = true;
}