diff --git a/files/darwin-settings/vorssaint_settings.plist b/files/darwin-settings/vorssaint_settings.plist
new file mode 100644
index 0000000..d4dfed4
--- /dev/null
+++ b/files/darwin-settings/vorssaint_settings.plist
@@ -0,0 +1,789 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>settings</key>
+ <dict>
+ <key>appAppearance</key>
+ <string>system</string>
+ <key>appUpdatesCheckFrequency</key>
+ <string>off</string>
+ <key>appUpdatesIncludeAppStore</key>
+ <true/>
+ <key>appUpdatesNotify</key>
+ <true/>
+ <key>autoCheckUpdates</key>
+ <true/>
+ <key>autoQuitEnabled</key>
+ <true/>
+ <key>autoQuitExceptions</key>
+ <array>
+ <string>com.apple.finder</string>
+ </array>
+ <key>batteryLimitPercent</key>
+ <integer>10</integer>
+ <key>brightnessControlEnabled</key>
+ <false/>
+ <key>brightnessKeysEnabled</key>
+ <false/>
+ <key>brightnessOSDEnabled</key>
+ <false/>
+ <key>cameraPreviewShortcut</key>
+ <string>control+option+command:13</string>
+ <key>cameraPreviewShortcutEnabled</key>
+ <false/>
+ <key>clamshellPreferred</key>
+ <false/>
+ <key>cleanerScheduleFrequency</key>
+ <string>off</string>
+ <key>cleanerScheduleHour</key>
+ <integer>9</integer>
+ <key>cleanerScheduleMinute</key>
+ <integer>0</integer>
+ <key>cleanerScheduleNotify</key>
+ <true/>
+ <key>cleanerScheduleWeekday</key>
+ <integer>2</integer>
+ <key>clipboardHistoryEnabled</key>
+ <false/>
+ <key>clipboardHistoryIncludeImagesFiles</key>
+ <true/>
+ <key>clipboardHistoryLimit</key>
+ <integer>50</integer>
+ <key>clipboardHistoryShortcut</key>
+ <string>control+option+command:9</string>
+ <key>clipboardHistoryShortcutEnabled</key>
+ <true/>
+ <key>clipboardHistorySkipSensitive</key>
+ <true/>
+ <key>colorPickerBareHex</key>
+ <false/>
+ <key>colorPickerFormat</key>
+ <string>hex</string>
+ <key>colorPickerShortcut</key>
+ <string>control+option+command:8</string>
+ <key>colorPickerShortcutEnabled</key>
+ <false/>
+ <key>commandBarAliases</key>
+ <string></string>
+ <key>commandBarDisabledSources</key>
+ <string></string>
+ <key>commandBarHidden</key>
+ <string></string>
+ <key>commandBarPins</key>
+ <string></string>
+ <key>commandBarShortcut</key>
+ <string>option:49</string>
+ <key>commandBarShortcutEnabled</key>
+ <false/>
+ <key>defaultDurationMinutes</key>
+ <integer>0</integer>
+ <key>dockClickCycleWindows</key>
+ <false/>
+ <key>dockClickMinimize</key>
+ <true/>
+ <key>dockPreviewEnabled</key>
+ <true/>
+ <key>extraBrightnessEnabled</key>
+ <false/>
+ <key>extraBrightnessLevel</key>
+ <integer>100</integer>
+ <key>featureAvailable.appUpdates</key>
+ <false/>
+ <key>featureAvailable.autoQuit</key>
+ <true/>
+ <key>featureAvailable.brightness</key>
+ <false/>
+ <key>featureAvailable.cameraPreview</key>
+ <true/>
+ <key>featureAvailable.cleaner</key>
+ <false/>
+ <key>featureAvailable.cleaningMode</key>
+ <false/>
+ <key>featureAvailable.clipboardHistory</key>
+ <false/>
+ <key>featureAvailable.colorPicker</key>
+ <true/>
+ <key>featureAvailable.commandBar</key>
+ <false/>
+ <key>featureAvailable.dockClick</key>
+ <false/>
+ <key>featureAvailable.dockPreview</key>
+ <false/>
+ <key>featureAvailable.extraBrightness</key>
+ <false/>
+ <key>featureAvailable.finderCutPaste</key>
+ <true/>
+ <key>featureAvailable.homebrew</key>
+ <false/>
+ <key>featureAvailable.keepAwake</key>
+ <false/>
+ <key>featureAvailable.keyboardDebounce</key>
+ <false/>
+ <key>featureAvailable.mediaTools</key>
+ <false/>
+ <key>featureAvailable.micMute</key>
+ <false/>
+ <key>featureAvailable.middleClick</key>
+ <false/>
+ <key>featureAvailable.mixer</key>
+ <true/>
+ <key>featureAvailable.monitorCPU</key>
+ <true/>
+ <key>featureAvailable.monitorDisk</key>
+ <true/>
+ <key>featureAvailable.monitorGPU</key>
+ <true/>
+ <key>featureAvailable.monitorMemory</key>
+ <true/>
+ <key>featureAvailable.monitorNetwork</key>
+ <true/>
+ <key>featureAvailable.monitorPower</key>
+ <true/>
+ <key>featureAvailable.mouseButtonShortcuts</key>
+ <false/>
+ <key>featureAvailable.mouseNavigation</key>
+ <false/>
+ <key>featureAvailable.musicBlock</key>
+ <false/>
+ <key>featureAvailable.pastePlain</key>
+ <true/>
+ <key>featureAvailable.quickLauncher</key>
+ <true/>
+ <key>featureAvailable.quickToggles</key>
+ <false/>
+ <key>featureAvailable.radialMenu</key>
+ <true/>
+ <key>featureAvailable.scratchpad</key>
+ <false/>
+ <key>featureAvailable.screenOCR</key>
+ <false/>
+ <key>featureAvailable.screenshot</key>
+ <true/>
+ <key>featureAvailable.scrollInverter</key>
+ <true/>
+ <key>featureAvailable.shelf</key>
+ <true/>
+ <key>featureAvailable.smoothScroll</key>
+ <false/>
+ <key>featureAvailable.soundOutputSwitcher</key>
+ <false/>
+ <key>featureAvailable.superKey</key>
+ <false/>
+ <key>featureAvailable.switcher</key>
+ <true/>
+ <key>featureAvailable.textSnippets</key>
+ <false/>
+ <key>featureAvailable.uninstaller</key>
+ <false/>
+ <key>featureAvailable.urlCleaner</key>
+ <false/>
+ <key>featureAvailable.windowLayout</key>
+ <false/>
+ <key>featureAvailable.windowMaximizer</key>
+ <false/>
+ <key>featuresOnboardingVersion</key>
+ <integer>4</integer>
+ <key>finderCutPasteEnabled</key>
+ <true/>
+ <key>hasOnboarded</key>
+ <true/>
+ <key>hotkeyEnabled</key>
+ <true/>
+ <key>keepAwakeActiveIcon</key>
+ <string>vorssaint</string>
+ <key>keepAwakeAutoStart</key>
+ <false/>
+ <key>keepAwakeConnectedToPower</key>
+ <false/>
+ <key>keepAwakeExternalDisplay</key>
+ <false/>
+ <key>keepAwakeIconTint</key>
+ <string>orange</string>
+ <key>keepAwakeMouseJiggleEnabled</key>
+ <false/>
+ <key>keepAwakeMouseJiggleIntervalMinutes</key>
+ <integer>5</integer>
+ <key>keepAwakeShortcut</key>
+ <string>control+option+command:40</string>
+ <key>keyboardDebounceEnabled</key>
+ <false/>
+ <key>keyboardDebounceKeyWindows</key>
+ <string></string>
+ <key>keyboardDebounceWindowMs</key>
+ <integer>5</integer>
+ <key>lastUpdateIntroVersion</key>
+ <string>3.2.0</string>
+ <key>launchAtLoginWanted</key>
+ <true/>
+ <key>mediaGIFEnd</key>
+ <real>0.0</real>
+ <key>mediaGIFFPS</key>
+ <real>12</real>
+ <key>mediaGIFLoops</key>
+ <true/>
+ <key>mediaGIFQuality</key>
+ <real>0.73999999999999999</real>
+ <key>mediaGIFStart</key>
+ <real>0.0</real>
+ <key>mediaGIFWidth</key>
+ <integer>720</integer>
+ <key>mediaImageFormat</key>
+ <string>jpeg</string>
+ <key>mediaImageMaxDimension</key>
+ <integer>1600</integer>
+ <key>mediaImageQuality</key>
+ <real>0.71999999999999997</real>
+ <key>mediaImageStripMetadata</key>
+ <true/>
+ <key>mediaLastTool</key>
+ <string>videoCompressor</string>
+ <key>mediaTextAccurate</key>
+ <true/>
+ <key>mediaTextLanguageCorrection</key>
+ <true/>
+ <key>mediaVideoCodec</key>
+ <string>h264</string>
+ <key>mediaVideoEnd</key>
+ <real>0.0</real>
+ <key>mediaVideoFPS</key>
+ <real>30</real>
+ <key>mediaVideoKeepAudio</key>
+ <true/>
+ <key>mediaVideoMaxDimension</key>
+ <integer>1280</integer>
+ <key>mediaVideoQuality</key>
+ <real>0.68000000000000005</real>
+ <key>mediaVideoStart</key>
+ <real>0.0</real>
+ <key>menuBarBatteryTemperature</key>
+ <false/>
+ <key>menuBarBatteryTime</key>
+ <false/>
+ <key>menuBarCPU</key>
+ <false/>
+ <key>menuBarCPUTemperature</key>
+ <false/>
+ <key>menuBarCombineTemperatures</key>
+ <true/>
+ <key>menuBarDiskActivity</key>
+ <false/>
+ <key>menuBarDiskUsage</key>
+ <false/>
+ <key>menuBarGPU</key>
+ <false/>
+ <key>menuBarGPUTemperature</key>
+ <false/>
+ <key>menuBarHideIconWithMetrics</key>
+ <false/>
+ <key>menuBarLabelStyle</key>
+ <string>compact</string>
+ <key>menuBarMemoryStyle</key>
+ <string>percent</string>
+ <key>menuBarMetricAppearance</key>
+ <string>values</string>
+ <key>menuBarMetricOrder</key>
+ <string>cpu,cpuTemperature,gpu,gpuTemperature,memory,battery,batteryTime,batteryTemperature,peripheralBattery,network,diskUsage,diskActivity,power</string>
+ <key>menuBarMetricSpacing</key>
+ <string>compact</string>
+ <key>menuBarNetworkUploadFirst</key>
+ <false/>
+ <key>menuBarPeripheralBattery</key>
+ <false/>
+ <key>menuBarPreset</key>
+ <string>dense</string>
+ <key>menuBarSeparateMetrics</key>
+ <false/>
+ <key>menuBarUsageBarCriticalColor</key>
+ <string>#FF453A</string>
+ <key>menuBarUsageBarElevatedColor</key>
+ <string>#FFD60A</string>
+ <key>menuBarUsageBarHighThreshold</key>
+ <integer>90</integer>
+ <key>menuBarUsageBarMediumThreshold</key>
+ <integer>70</integer>
+ <key>menuBarUsageBarNormalColor</key>
+ <string>#64D2FF</string>
+ <key>micMuteMenuBarIndicator</key>
+ <true/>
+ <key>micMuteShortcut</key>
+ <string>control+option+command:46</string>
+ <key>micMuteShortcutEnabled</key>
+ <false/>
+ <key>middleClickEnabled</key>
+ <false/>
+ <key>middleClickExceptions</key>
+ <array/>
+ <key>middleClickTapFingers</key>
+ <integer>0</integer>
+ <key>mixerHeadphonesDisconnectVolumePercent</key>
+ <integer>25</integer>
+ <key>mixerLowerVolumeOnHeadphonesDisconnect</key>
+ <false/>
+ <key>mixerShowFinder</key>
+ <true/>
+ <key>monitorAlertBattery</key>
+ <false/>
+ <key>monitorAlertBatteryPercent</key>
+ <integer>15</integer>
+ <key>monitorAlertCPU</key>
+ <false/>
+ <key>monitorAlertCPUTemperature</key>
+ <false/>
+ <key>monitorAlertCPUTemperatureThreshold</key>
+ <integer>90</integer>
+ <key>monitorAlertCPUThreshold</key>
+ <integer>90</integer>
+ <key>monitorAlertCooldownMinutes</key>
+ <integer>15</integer>
+ <key>monitorAlertDisk</key>
+ <false/>
+ <key>monitorAlertDiskFreePercent</key>
+ <integer>10</integer>
+ <key>monitorAlertMemory</key>
+ <false/>
+ <key>monitorDiskActivity</key>
+ <true/>
+ <key>monitorDiskProtection</key>
+ <true/>
+ <key>monitorDiskSMART</key>
+ <true/>
+ <key>monitorDiskTools</key>
+ <true/>
+ <key>monitorDiskUsage</key>
+ <true/>
+ <key>monitorGraphBattery</key>
+ <true/>
+ <key>monitorGraphCPU</key>
+ <true/>
+ <key>monitorGraphDisk</key>
+ <true/>
+ <key>monitorGraphGPU</key>
+ <true/>
+ <key>monitorGraphMemory</key>
+ <true/>
+ <key>monitorGraphNetwork</key>
+ <true/>
+ <key>monitorGraphPower</key>
+ <true/>
+ <key>monitorIntervalSeconds</key>
+ <integer>2</integer>
+ <key>monitorNetApps</key>
+ <true/>
+ <key>monitorNetSpeed</key>
+ <true/>
+ <key>monitorNetTest</key>
+ <true/>
+ <key>monitorNetTotals</key>
+ <true/>
+ <key>monitorPwrAdapter</key>
+ <true/>
+ <key>monitorPwrBattery</key>
+ <true/>
+ <key>monitorPwrHealth</key>
+ <true/>
+ <key>monitorPwrSystem</key>
+ <true/>
+ <key>monitorPwrTimeRemaining</key>
+ <true/>
+ <key>monitorShowDisk</key>
+ <true/>
+ <key>monitorShowFanControlBeta</key>
+ <false/>
+ <key>monitorShowMixer</key>
+ <true/>
+ <key>monitorShowNetwork</key>
+ <true/>
+ <key>monitorShowPower</key>
+ <true/>
+ <key>monitorShowSystem</key>
+ <true/>
+ <key>monitorSysAlerts</key>
+ <true/>
+ <key>monitorSysBattery</key>
+ <true/>
+ <key>monitorSysCPU</key>
+ <true/>
+ <key>monitorSysGPU</key>
+ <true/>
+ <key>monitorSysMemory</key>
+ <true/>
+ <key>monitorSysTemps</key>
+ <true/>
+ <key>monitorSysUptime</key>
+ <true/>
+ <key>mouseButtonExceptions</key>
+ <array/>
+ <key>mouseButtonShortcuts</key>
+ <dict/>
+ <key>mouseButtonShortcutsEnabled</key>
+ <false/>
+ <key>mouseNavigationEnabled</key>
+ <false/>
+ <key>mouseNavigationExceptions</key>
+ <array/>
+ <key>musicBlockEnabled</key>
+ <false/>
+ <key>musicBlockReplacementPath</key>
+ <string></string>
+ <key>onboardingStep</key>
+ <integer>0</integer>
+ <key>panelCollapsedResetVersion</key>
+ <string>2.15.1</string>
+ <key>panelControlAutoQuit</key>
+ <true/>
+ <key>panelControlCutPaste</key>
+ <true/>
+ <key>panelControlDockClick</key>
+ <true/>
+ <key>panelControlDockClickCycle</key>
+ <true/>
+ <key>panelControlDockPreview</key>
+ <true/>
+ <key>panelControlFilesExpanded</key>
+ <false/>
+ <key>panelControlInputExpanded</key>
+ <false/>
+ <key>panelControlKeyDebounce</key>
+ <true/>
+ <key>panelControlMiddleClick</key>
+ <true/>
+ <key>panelControlMouseButtonShortcuts</key>
+ <true/>
+ <key>panelControlMouseNavigation</key>
+ <true/>
+ <key>panelControlMouseScroll</key>
+ <true/>
+ <key>panelControlRadialMenu</key>
+ <true/>
+ <key>panelControlShelf</key>
+ <true/>
+ <key>panelControlSuperKey</key>
+ <true/>
+ <key>panelControlSwitcher</key>
+ <true/>
+ <key>panelControlTextSnippets</key>
+ <true/>
+ <key>panelControlWindowMaximize</key>
+ <true/>
+ <key>panelControlWindowsExpanded</key>
+ <false/>
+ <key>panelNavigationEnabled</key>
+ <true/>
+ <key>panelPowerOrder</key>
+ <string>system,adapter,battery,remaining,health</string>
+ <key>panelSectionOrder</key>
+ <string>keepAwake,brightness,system,mixer,power,disk,network,fanControl,utilities,controls,toggles</string>
+ <key>panelShowBrightness</key>
+ <true/>
+ <key>panelShowControls</key>
+ <true/>
+ <key>panelShowKeepAwake</key>
+ <true/>
+ <key>panelShowToggles</key>
+ <true/>
+ <key>panelShowUtilities</key>
+ <true/>
+ <key>panelToggleDarkMode</key>
+ <true/>
+ <key>panelToggleDesktopIcons</key>
+ <true/>
+ <key>panelToggleDisplayOff</key>
+ <true/>
+ <key>panelToggleEjectDisks</key>
+ <true/>
+ <key>panelToggleEmptyTrash</key>
+ <true/>
+ <key>panelToggleHiddenFiles</key>
+ <true/>
+ <key>panelToggleLockScreen</key>
+ <true/>
+ <key>panelToggleScreenSaver</key>
+ <true/>
+ <key>panelUtilityAppUpdates</key>
+ <true/>
+ <key>panelUtilityCameraPreview</key>
+ <true/>
+ <key>panelUtilityCleaner</key>
+ <true/>
+ <key>panelUtilityCleaning</key>
+ <true/>
+ <key>panelUtilityClipboard</key>
+ <true/>
+ <key>panelUtilityColorPicker</key>
+ <true/>
+ <key>panelUtilityCommandBar</key>
+ <true/>
+ <key>panelUtilityHomebrew</key>
+ <true/>
+ <key>panelUtilityMedia</key>
+ <true/>
+ <key>panelUtilityMicMute</key>
+ <true/>
+ <key>panelUtilityQuickLauncher</key>
+ <true/>
+ <key>panelUtilityScratchpad</key>
+ <true/>
+ <key>panelUtilityScreenOCR</key>
+ <true/>
+ <key>panelUtilityScreenshot</key>
+ <true/>
+ <key>panelUtilityURLCleaner</key>
+ <true/>
+ <key>panelUtilityUninstaller</key>
+ <true/>
+ <key>panelUtilityWindowLayout</key>
+ <true/>
+ <key>pastePlainEnabled</key>
+ <true/>
+ <key>pastePlainShortcut</key>
+ <string>shift+command:9</string>
+ <key>previewSize</key>
+ <string>normal</string>
+ <key>quickLauncherHiddenItems</key>
+ <string></string>
+ <key>quickLauncherShortcut</key>
+ <string>option+shift:9</string>
+ <key>quickLauncherShortcutEnabled</key>
+ <true/>
+ <key>radialMenuActivationMode</key>
+ <string>pressOrHold</string>
+ <key>radialMenuAtPointer</key>
+ <true/>
+ <key>radialMenuEnabled</key>
+ <false/>
+ <key>radialMenuItems</key>
+ <data>
+ W3sicGF5bG9hZCI6InNjcmVlbnNob3QiLCJjaGlsZHJlbiI6W10sIm5hbWUi
+ OiIiLCJpZCI6IjU1N0M5QjY0LURBRUQtNEY0OS1CMjAyLUE2RTRFMjg3QkE3
+ MSIsImtpbmQiOiJ0b29sIiwic3ltYm9sTmFtZSI6IiJ9LHsicGF5bG9hZCI6
+ ImNvbG9yUGlja2VyIiwiY2hpbGRyZW4iOltdLCJuYW1lIjoiIiwiaWQiOiI4
+ NDc1RTg2OS0yNjFELTQ2OEUtODJEOC1BMjFGM0VFN0VGQjgiLCJraW5kIjoi
+ dG9vbCIsInN5bWJvbE5hbWUiOiIifSx7InBheWxvYWQiOiJjYW1lcmFQcmV2
+ aWV3IiwiY2hpbGRyZW4iOltdLCJuYW1lIjoiIiwiaWQiOiIzRTNCRUU3MS05
+ QzgxLTQ5QjMtOTEzNS03OTA5RkY5MDc4QUQiLCJraW5kIjoidG9vbCIsInN5
+ bWJvbE5hbWUiOiIifV0=
+ </data>
+ <key>radialMenuMouseButton</key>
+ <string>off</string>
+ <key>radialMenuShortcut</key>
+ <string>control+option+command:49</string>
+ <key>releaseNotesOnUpdate</key>
+ <true/>
+ <key>scratchpadCloseOnClickOutside</key>
+ <true/>
+ <key>scratchpadRetention</key>
+ <string>never</string>
+ <key>scratchpadShortcut</key>
+ <string>control+option+command:45</string>
+ <key>scratchpadShortcutEnabled</key>
+ <false/>
+ <key>screenOCRDetectQRCodes</key>
+ <true/>
+ <key>screenOCRShortcut</key>
+ <string>control+option+command:17</string>
+ <key>screenOCRShortcutEnabled</key>
+ <false/>
+ <key>screenshotAnnotationShadows</key>
+ <false/>
+ <key>screenshotBackdropPresets</key>
+ <string>[]</string>
+ <key>screenshotBackdropStyle</key>
+ <string></string>
+ <key>screenshotCopyToClipboard</key>
+ <true/>
+ <key>screenshotDefaultAction</key>
+ <string></string>
+ <key>screenshotDelay</key>
+ <integer>0</integer>
+ <key>screenshotDownscale</key>
+ <false/>
+ <key>screenshotFileNamePattern</key>
+ <string></string>
+ <key>screenshotFileNumberNext</key>
+ <integer>1</integer>
+ <key>screenshotFileNumberStart</key>
+ <integer>1</integer>
+ <key>screenshotFreeze</key>
+ <true/>
+ <key>screenshotIncludePointer</key>
+ <false/>
+ <key>screenshotLastColor</key>
+ <string>red</string>
+ <key>screenshotLastSticker</key>
+ <string>check</string>
+ <key>screenshotLastStroke</key>
+ <string>medium</string>
+ <key>screenshotLastTool</key>
+ <string>select</string>
+ <key>screenshotOpenEditorDirectly</key>
+ <false/>
+ <key>screenshotSaveFolder</key>
+ <string></string>
+ <key>screenshotSaveSubfolder</key>
+ <string></string>
+ <key>screenshotShortcut</key>
+ <string>control+option+command:21</string>
+ <key>screenshotShortcutEnabled</key>
+ <true/>
+ <key>screenshotShowLastRegion</key>
+ <true/>
+ <key>screenshotToolOrder</key>
+ <string>select,arrow,pixelate,crop,text,sticker,rect,highlight,freehand,line,ellipse,counter,redact</string>
+ <key>screenshotToolShortcutsEnabled</key>
+ <true/>
+ <key>scrollInverterEnabled</key>
+ <true/>
+ <key>scrollInverterExceptions</key>
+ <array/>
+ <key>shelfAutomaticExclusions</key>
+ <array/>
+ <key>shelfCloseAfterDrop</key>
+ <false/>
+ <key>shelfDropZoneEnabled</key>
+ <true/>
+ <key>shelfEnabled</key>
+ <true/>
+ <key>shelfRemoveAfterDrop</key>
+ <true/>
+ <key>shelfShakeToOpen</key>
+ <true/>
+ <key>shelfShortcut</key>
+ <string>control+option+command:2</string>
+ <key>shelfShortcutEnabled</key>
+ <true/>
+ <key>showCountdownInMenuBar</key>
+ <false/>
+ <key>smoothScrollEnabled</key>
+ <false/>
+ <key>smoothScrollExceptions</key>
+ <array/>
+ <key>smoothScrollStep</key>
+ <integer>40</integer>
+ <key>snippetLibraryEnabled</key>
+ <false/>
+ <key>snippetLibraryShortcut</key>
+ <string>control+option+command:37</string>
+ <key>soundOutputSwitcherEnabled</key>
+ <false/>
+ <key>soundOutputSwitcherShortcut</key>
+ <string>control+option+command:1</string>
+ <key>superKeyEnabled</key>
+ <false/>
+ <key>superKeySoloAction</key>
+ <string>none</string>
+ <key>supportUpdateIntroVersion</key>
+ <string>3.1.13</string>
+ <key>switcherCurrentSpaceOnly</key>
+ <false/>
+ <key>switcherEnabled</key>
+ <true/>
+ <key>switcherIconRowMode</key>
+ <false/>
+ <key>switcherMergeTabs</key>
+ <false/>
+ <key>switcherShortcut</key>
+ <string>command:48</string>
+ <key>switcherShowWindowlessFinder</key>
+ <true/>
+ <key>switcherSimpleMode</key>
+ <false/>
+ <key>switcherWindowShortcut</key>
+ <string>command:50</string>
+ <key>switcherWindowlessApps</key>
+ <string>finder</string>
+ <key>temperatureUnit</key>
+ <string>celsius</string>
+ <key>textSnippetsEnabled</key>
+ <false/>
+ <key>updateHighlightsSeenVersion</key>
+ <string>3.2.0</string>
+ <key>urlCleanerEnabled</key>
+ <false/>
+ <key>whatsAppDownloadsAutomaticEnabled</key>
+ <false/>
+ <key>whatsAppDownloadsCategories</key>
+ <string>image,video,audio</string>
+ <key>whatsAppDownloadsIncludeExisting</key>
+ <false/>
+ <key>whatsAppDownloadsNotify</key>
+ <true/>
+ <key>whatsAppDownloadsRetentionDays</key>
+ <integer>7</integer>
+ <key>whatsAppOrganizerCategories</key>
+ <string>image,video,audio,document,archive,other</string>
+ <key>whatsAppOrganizerDelayMinutes</key>
+ <integer>5</integer>
+ <key>whatsAppOrganizerDuplicateAction</key>
+ <string>trashNew</string>
+ <key>whatsAppOrganizerEnabled</key>
+ <false/>
+ <key>whatsAppOrganizerLayout</key>
+ <string>flat</string>
+ <key>windowGestureEnabled</key>
+ <false/>
+ <key>windowGestureModifiers</key>
+ <string>control+command</string>
+ <key>windowGestureRaiseWindow</key>
+ <false/>
+ <key>windowLayoutHiddenActions</key>
+ <string></string>
+ <key>windowLayoutShortcutBottom</key>
+ <string>control+option:125</string>
+ <key>windowLayoutShortcutBottomCenterSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutBottomLeft</key>
+ <string>control+option:38</string>
+ <key>windowLayoutShortcutBottomLeftSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutBottomRight</key>
+ <string>control+option:40</string>
+ <key>windowLayoutShortcutBottomRightSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutCenter</key>
+ <string>control+option:8</string>
+ <key>windowLayoutShortcutCenterThird</key>
+ <string>control+option:3</string>
+ <key>windowLayoutShortcutFullScreen</key>
+ <string>none</string>
+ <key>windowLayoutShortcutLeft</key>
+ <string>control+option:123</string>
+ <key>windowLayoutShortcutLeftThird</key>
+ <string>control+option:2</string>
+ <key>windowLayoutShortcutLeftTwoThirds</key>
+ <string>control+option:14</string>
+ <key>windowLayoutShortcutMaximize</key>
+ <string>control+option:36</string>
+ <key>windowLayoutShortcutNextDisplay</key>
+ <string>control+option+command:124</string>
+ <key>windowLayoutShortcutRestore</key>
+ <string>control+option:15</string>
+ <key>windowLayoutShortcutRight</key>
+ <string>control+option:124</string>
+ <key>windowLayoutShortcutRightThird</key>
+ <string>control+option:5</string>
+ <key>windowLayoutShortcutRightTwoThirds</key>
+ <string>control+option:17</string>
+ <key>windowLayoutShortcutTop</key>
+ <string>control+option:126</string>
+ <key>windowLayoutShortcutTopCenterSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutTopLeft</key>
+ <string>control+option:32</string>
+ <key>windowLayoutShortcutTopLeftSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutTopRight</key>
+ <string>control+option:34</string>
+ <key>windowLayoutShortcutTopRightSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutsEnabled</key>
+ <false/>
+ <key>windowMaximizeEnabled</key>
+ <true/>
+ </dict>
+ <key>vorssaintBackupAppVersion</key>
+ <string>3.2.0</string>
+ <key>vorssaintBackupVersion</key>
+ <integer>1</integer>
+</dict>
+</plist>
diff --git a/hosts/RyanMac/configuration.nix b/hosts/RyanMac/configuration.nix
index e8339e6..e98db39 100644
--- a/hosts/RyanMac/configuration.nix
+++ b/hosts/RyanMac/configuration.nix
@@ -24,20 +24,6 @@ let
];
});
- mkSpace = i: {
- name = toString (117+i);
- value = {
- enabled = true;
- value = {
- type = "standard";
- parameters = [ (48 + i) (builtins.elemAt [ 18 19 20 21 22 23 25 26 28 ] (i - 1)) 524288 ]; # opt + num
- #parameters = [ (48 + i) (builtins.elemAt [ 18 19 20 21 22 23 25 26 28 ] (i - 1)) 1048576 ]; # cmd + num
- };
- };
- };
-
- spaceHotkeys = builtins.listToAttrs (map mkSpace (lib.range 1 9));
-
manualHotkeys = {
"64" = {
enabled = true;
@@ -49,6 +35,7 @@ in {
imports = [
inputs.nix-homebrew.darwinModules.nix-homebrew
../../modules/darwin/random-wallpaper
+ ../../modules/darwin/yabai
];
# Define the system's user and home dir location
@@ -69,43 +56,14 @@ in {
};
# Configure yabai
- services.yabai = {
+ local.yabai = {
enable = true;
- enableScriptingAddition = false;
- config = {
- mouse_follows_focus = "on";
- layout = "bsp";
- window_placement = "second_child";
- top_padding = "10";
- bottom_padding = "10";
- left_padding = "10";
- right_padding = "10";
- window_gap = "10";
- mouse_modifier = "alt";
- mouse_drop_action = "swap";
- mouse_action1 = "move";
- mouse_action2 = "resize";
- focus_follows_mouse = "autofocus";
+ defaultPadding = 10;
+ defaultTopPadding = 40;
+ displayTopPadding = {
+ "37D8832A-2D66-02CA-B9F7-8F30A301B230" = 10; # macOS retina display
};
- extraConfig = ''
- yabai -m rule --add app='System Settings' manage=off
- yabai -m rule --add app="^Zen$" title="^Picture-in-Picture$" manage=off
- ''
- # Add simple-bar signals if enabled
- + lib.optionalString config.home-manager.users.${username}.ryan.simple-bar.enable ''
- yabai -m signal --add event=window_focused action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows when focused application changes"
- yabai -m signal --add event=window_resized action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows when a window is resized"
- yabai -m signal --add event=window_destroyed action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-ba spaces & windows when an application window is closed"
- yabai -m signal --add event=space_changed action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows on space change"
- yabai -m signal --add event=display_changed action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows on display focus change"
- yabai -m signal --add event=window_title_changed action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows when current window title changes"
- yabai -m signal --add event=space_destroyed action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows on space removal"
- yabai -m signal --add event=space_created action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows on space creation"
- yabai -m signal --add event=application_activated action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows when application is activated"
- yabai -m signal --add event=display_added action="curl http://localhost:7776/yabai/displays/refresh" label="Refresh simple-bar displays when a new dispay is added"
- yabai -m signal --add event=display_removed action="curl http://localhost:7776/yabai/displays/refresh" label="Refresh simple-bar displays when a dispay is removed"
- yabai -m signal --add event=display_moved action="curl http://localhost:7776/yabai/displays/refresh" label="Refresh simple-bar displays when a dispay is moved"
- '';
+ extraSymbolicHotkeys = manualHotkeys;
};
# Configure JankyBorders
@@ -200,6 +158,8 @@ in {
"gimp"
"kdenlive"
"rustdesk"
+ "vorssaint"
+ "obs"
];
};
@@ -209,35 +169,6 @@ in {
skhdConfig = ''
alt - d : osascript -e 'tell application "System Events" to key code 49 using {command down}'
alt - return : osascript -e 'tell application "Ghostty"' -e 'new window' -e 'activate' -e 'end tell'
-
- shift + alt - q : ${pkgs.yabai}/bin/yabai -m window --close
-
- alt - up : ${pkgs.yabai}/bin/yabai -m window --focus north || ${pkgs.yabai}/bin/yabai -m display --focus north
- shift + alt - up : ${pkgs.yabai}/bin/yabai -m window --swap north || ${pkgs.yabai}/bin/yabai -m window --display north
- alt - down : ${pkgs.yabai}/bin/yabai -m window --focus south || ${pkgs.yabai}/bin/yabai -m display --focus south
- shift + alt - down : ${pkgs.yabai}/bin/yabai -m window --swap south || ${pkgs.yabai}/bin/yabai -m window --display south
- alt - right : ${pkgs.yabai}/bin/yabai -m window --focus east || ${pkgs.yabai}/bin/yabai -m display --focus east
- shift + alt - right : ${pkgs.yabai}/bin/yabai -m window --swap east || ${pkgs.yabai}/bin/yabai -m window --display east
- alt - left : ${pkgs.yabai}/bin/yabai -m window --focus west || ${pkgs.yabai}/bin/yabai -m display --focus west
- shift + alt - left: ${pkgs.yabai}/bin/yabai -m window --swap west || ${pkgs.yabai}/bin/yabai -m window --display west
-
- shift + alt - space: ${pkgs.yabai}/bin/yabai -m window --toggle float
-
- shift + alt - return: ${pkgs.yabai}/bin/yabai -m window --toggle sticky
-
- alt - f : ${pkgs.yabai}/bin/yabai -m window --toggle zoom-fullscreen
-
- shift + alt - f: ${pkgs.yabai}/bin/yabai -m window --toggle native-fullscreen
-
- shift + alt - 1 : ${pkgs.yabai}/bin/yabai -m window --space 1
- shift + alt - 2 : ${pkgs.yabai}/bin/yabai -m window --space 2
- shift + alt - 3 : ${pkgs.yabai}/bin/yabai -m window --space 3
- shift + alt - 4 : ${pkgs.yabai}/bin/yabai -m window --space 4
- shift + alt - 5 : ${pkgs.yabai}/bin/yabai -m window --space 5
- shift + alt - 6 : ${pkgs.yabai}/bin/yabai -m window --space 6
- shift + alt - 7 : ${pkgs.yabai}/bin/yabai -m window --space 7
- shift + alt - 8 : ${pkgs.yabai}/bin/yabai -m window --space 8
- shift + alt - 9 : ${pkgs.yabai}/bin/yabai -m window --space 9
'';
};
@@ -250,9 +181,6 @@ in {
};
system.defaults = {
- WindowManager = {
- EnableTilingOptionAccelerator = false;
- };
NSGlobalDomain = {
# 24 hour time
AppleICUForce24HourTime = true;
@@ -391,10 +319,6 @@ in {
"com.apple.Accessibility" = {
ReduceMotionEnabled = 1;
};
-
- "com.apple.symbolichotkeys" = {
- AppleSymbolicHotKeys = manualHotkeys // spaceHotkeys;
- };
};
universalaccess.reduceMotion = true;
};
diff --git a/modules/darwin/yabai/default.nix b/modules/darwin/yabai/default.nix
new file mode 100644
index 0000000..8aa4fbf
--- /dev/null
+++ b/modules/darwin/yabai/default.nix
@@ -0,0 +1,296 @@
+# modules/darwin/yabai/default.nix
+{ config, lib, pkgs, ... }:
+
+let
+ cfg = config.local.yabai;
+
+ spaceFocusBinds = lib.concatStringsSep "\n"
+ (map (i: "shift + alt - ${toString i} : ${yabaiBin} -m window --space ${toString i}")
+ (lib.range 1 cfg.spaceHotkeys.count));
+
+ # symbolic hotkey IDs 118..126 == "Switch to Desktop 1".."Switch to Desktop 9"
+ spaceKeycodes = [ 18 19 20 21 22 23 25 26 28 ];
+
+ mkSpaceHotkey = i: {
+ name = toString (117 + i);
+ value = {
+ enabled = true;
+ value = {
+ type = "standard";
+ parameters = [
+ (48 + i) # ASCII '1'..'9'
+ (builtins.elemAt spaceKeycodes (i - 1)) # virtual keycode
+ cfg.spaceHotkeys.modifierMask
+ ];
+ };
+ };
+ };
+
+ spaceHotkeys = builtins.listToAttrs
+ (map mkSpaceHotkey (lib.range 1 cfg.spaceHotkeys.count));
+
+ padArms = lib.concatStringsSep "\n" (lib.mapAttrsToList
+ (uuid: pad: " ${uuid}) pad=${toString pad} ;;")
+ cfg.displayTopPadding);
+
+ paddingSignals = ''
+ yabai -m signal --add event=display_added action="${displayPaddingScript}" label="per-display padding (added)"
+ yabai -m signal --add event=display_removed action="${displayPaddingScript}" label="per-display padding (removed)"
+ yabai -m signal --add event=space_created action="${displayPaddingScript}" label="per-display padding (space)"
+ ${displayPaddingScript}
+ '';
+
+
+ displayPaddingScript = pkgs.writeShellScript "yabai-display-padding" ''
+ set -eu
+ yabai="${cfg.package}/bin/yabai"
+
+ "$yabai" -m query --displays \
+ | ${lib.getExe pkgs.jq} -r '.[] | "\(.uuid) \(.spaces | map(tostring) | join(" "))"' \
+ | while read -r uuid spaces; do
+ case "$uuid" in
+${padArms}
+ *) pad=${toString cfg.defaultTopPadding} ;;
+ esac
+ for s in $spaces; do
+ for k in top_padding; do
+ "$yabai" -m config --space "$s" "$k" "$pad" || true
+ done
+ done
+ done
+ '';
+
+ yabaiBin = "${cfg.package}/bin/yabai";
+
+ sudoersFile = pkgs.runCommand "sudoers-yabai" { } ''
+ YABAI_BIN="${yabaiBin}"
+ SHASUM=$(sha256sum "$YABAI_BIN" | cut -d' ' -f1)
+ cat > "$out" <<EOF
+ ${cfg.user} ALL=(root) NOPASSWD: sha256:$SHASUM $YABAI_BIN --load-sa
+ EOF
+ '';
+
+ dockRestartSignal = ''
+ yabai -m signal --add event=dock_did_restart action="/usr/bin/sudo ${yabaiBin} --load-sa" label="reload scripting addition"
+ '';
+
+ directionBinds = lib.concatStringsSep "\n" (map (d: ''
+ alt - ${d.key} : ${yabaiBin} -m window --focus ${d.dir} || ${yabaiBin} -m display --focus ${d.dir}
+ shift + alt - ${d.key} : ${yabaiBin} -m window --swap ${d.dir} || ${yabaiBin} -m window --display ${d.dir}
+ '') [
+ { key = "up"; dir = "north"; }
+ { key = "down"; dir = "south"; }
+ { key = "right"; dir = "east"; }
+ { key = "left"; dir = "west"; }
+ ]);
+
+ spaceMoveBinds = lib.concatStringsSep "\n"
+ (map (i: "shift + alt - ${toString i} : ${yabaiBin} -m window --space ${toString i}")
+ (lib.range 1 cfg.spaceHotkeys.count));
+
+in {
+ options.local.yabai = {
+ enable = lib.mkEnableOption "yabai tiling window manager setup";
+
+ package = lib.mkOption {
+ type = lib.types.package;
+ default = pkgs.yabai;
+ description = "yabai package. Referenced by keybindings and the padding script.";
+ };
+
+ user = lib.mkOption {
+ type = lib.types.str;
+ default = config.system.primaryUser;
+ defaultText = lib.literalExpression "config.system.primaryUser";
+ description = ''
+ User that is granted permission to run --load-sa
+ '';
+ };
+
+ enableScriptingAddition = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = ''
+ Only gates space creation/destruction and moving windows between
+ spaces. Window geometry and padding work without it.
+ '';
+ };
+
+ useYabaiSpaceFocus = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = ''
+ Bind alt+n to yabai native control rather than skhd. Requires scripting
+ additions.
+ '';
+ };
+
+ defaultPadding = lib.mkOption {
+ type = lib.types.int;
+ default = 10;
+ description = ''
+ Padding and window_gap for any display not listed in displayTopPadding.
+ Sized for Retina (2x) panels.
+ '';
+ };
+
+ defaultTopPadding = lib.mkOption {
+ type = lib.types.int;
+ default = 40;
+ description = ''
+ Padding for top edge of screen
+ '';
+ };
+
+ displayTopPadding = lib.mkOption {
+ type = lib.types.attrsOf lib.types.int;
+ default = { };
+ example = lib.literalExpression ''
+ { "37D8832A-2D66-02CA-B9F7-8F30A301B230" = 40; }
+ '';
+ description = ''
+ Display UUID -> padding value. The value is applied to all four
+ paddings and to window_gap for every space on that display.
+
+ Get UUIDs with:
+ yabai -m query --displays | jq -r '.[] | "\(.uuid) \(.frame)"'
+
+ Do not key off frame dimensions: yabai reports points, not pixels,
+ so a scaled 1080p panel will not report 1920x1080.
+ '';
+ };
+
+ manageKeybindings = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = "Emit yabai skhd bindings into services.skhd.skhdConfig.";
+ };
+
+ spaceHotkeys = {
+ enable = lib.mkEnableOption "opt+<n> macOS space-switching hotkeys" // {
+ default = true;
+ };
+
+ count = lib.mkOption {
+ type = lib.types.ints.between 1 9;
+ default = 9;
+ description = "Number of spaces to bind. macOS exposes 9 symbolic hotkeys.";
+ };
+
+ modifierMask = lib.mkOption {
+ type = lib.types.int;
+ default = 524288; # option
+ description = "Carbon modifier mask. 524288 = option, 1048576 = command.";
+ };
+ };
+
+ extraSymbolicHotkeys = lib.mkOption {
+ type = lib.types.attrs;
+ default = { };
+ description = ''
+ Merged into com.apple.symbolichotkeys alongside the space hotkeys.
+ Put non-yabai symbolic hotkeys here rather than defining
+ CustomUserPreferences."com.apple.symbolichotkeys" in the host config --
+ CustomUserPreferences is types.attrs and merges shallowly, so a second
+ definition of that domain silently clobbers this one.
+ '';
+ };
+
+ extraSettings = lib.mkOption {
+ type = lib.types.attrs;
+ default = { };
+ description = "Extra keys merged into services.yabai.config.";
+ };
+
+ extraConfig = lib.mkOption {
+ type = lib.types.lines;
+ default = "";
+ description = "Extra shell appended to yabairc (rules, signals).";
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = cfg.user != null && cfg.user != "";
+ message = ''
+ local.yabai.user is unset. Set it or system.primaryUser
+ '';
+ }
+ {
+ assertion = cfg.useYabaiSpaceFocus -> cfg.enableScriptingAssition;
+ message = ''
+ local.yabai.useYabaiSpaceFocus requires enableScriptingAddition.
+ '';
+ }
+ ];
+
+ services.yabai = {
+ enable = true;
+ inherit (cfg) package enableScriptingAddition;
+
+ config = {
+ mouse_follows_focus = "on";
+ layout = "bsp";
+ window_placement = "second_child";
+ top_padding = toString cfg.defaultTopPadding;
+ bottom_padding = toString cfg.defaultPadding;
+ left_padding = toString cfg.defaultPadding;
+ right_padding = toString cfg.defaultPadding;
+ window_gap = toString cfg.defaultPadding;
+ mouse_modifier = "alt";
+ mouse_drop_action = "swap";
+ mouse_action1 = "move";
+ mouse_action2 = "resize";
+ focus_follows_mouse = "autofocus";
+ } // cfg.extraSettings;
+
+ extraConfig = lib.concatStringsSep "\n" (
+ [
+ ''
+ yabai -m rule --add app='System Settings' manage=off
+ yabai -m rule --add app="^Zen$" title="^Picture-in-Picture$" manage=off
+ ''
+ ]
+ ++ lib.optional cfg.enableScriptingAddition dockRestartSignal
+ ++ lib.optional (cfg.displayTopPadding != { }) paddingSignals
+ ++ lib.optional (cfg.extraConfig != "") cfg.extraConfig
+ );
+ };
+
+ environment.etc."sudoers.d/yabai".source =
+ lib.mkIf cfg.enableScriptingAddition (lib.mkForce sudoersFile);
+
+ services.skhd = lib.mkIf cfg.manageKeybindings {
+ enable = true;
+ skhdConfig = lib.concatStringsSep "\n" (
+ [
+ ''
+ shift + alt - q : ${yabaiBin} -m window --close
+
+ ${directionBinds}
+
+ shift + alt - space : ${yabaiBin} -m window --toggle float
+ shift + alt - return : ${yabaiBin} -m window --toggle sticky
+ alt - f : ${yabaiBin} -m window --toggle zoom-fullscreen
+ shift + alt - f : ${yabaiBin} -m window --toggle native-fullscreen
+
+ ${spaceMoveBinds}
+ ''
+ ]
+ ++ lib.optional cfg.useYabaiSpaceFocus spaceFocusBinds
+ );
+ };
+
+ # Prerequisites, not preferences: yabai requires per-display spaces, and
+ # macOS's own tiling fights it for window placement.
+ system.defaults = {
+ spaces."spans-displays" = false;
+ WindowManager.EnableTilingOptionAccelerator = false;
+
+ CustomUserPreferences."com.apple.symbolichotkeys".AppleSymbolicHotKeys =
+ cfg.extraSymbolicHotkeys
+ // (lib.optionalAttrs cfg.spaceHotkeys.enable spaceHotkeys);
+ };
+ };
+}
diff --git a/users/ryan/common.nix b/users/ryan/common.nix
index b1599fd..e2e5890 100644
--- a/users/ryan/common.nix
+++ b/users/ryan/common.nix
@@ -1,7 +1,7 @@
{ config, pkgs, lib, ... }:
{
- imports = [ ./modules/nvim ];
+ imports = [ ./modules/nvim ./modules/aria2 ./modules/zen ];
news.display = "silent";
@@ -11,6 +11,10 @@
# My own modules
ryan.neovim.enable = true;
+ ryan.aria2.enable = true;
+
+ ryan.zen.enable = true;
+
programs.starship = {
enable = true;
settings = {
@@ -27,14 +31,6 @@
};
};
- # Package/platform overrides (darwinDefaultsId, package = null for homebrew,
- # etc.) live in darwin.nix/linux.nix. Policies/profile are shared.
- programs.zen-browser = {
- enable = true;
- policies = import ./zen/zenPolicies.nix;
- profiles.default = import ./zen/zenProfile.nix;
- };
-
programs.eza = {
enable = true;
enableZshIntegration = true;
@@ -120,6 +116,12 @@
services.gpg-agent = {
enable = pkgs.stdenv.isLinux;
enableSshSupport = true;
+ enableExtraSocket = true;
+ };
+
+ programs.fzf = {
+ enable = true;
+ enableZshIntegration = true;
};
programs.zsh = {
@@ -128,13 +130,16 @@
brewPath = if pkgs.stdenv.isDarwin && pkgs.stdenv.isAarch64 then ''
eval "$(/opt/homebrew/bin/brew shellenv)"
'' else "";
- in ''
- export GPG_TTY="$(tty)"
- export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)"
- gpgconf --launch gpg-agent
- gpg-connect-agent updatestartuptty /bye > /dev/null
- ${brewPath}
- '';
+ in lib.mkMerge [
+ (lib.mkOrder 550 "ZVM_INIT_MODE=sourcing")
+ ''
+ export GPG_TTY="$(tty)"
+ export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)"
+ gpgconf --launch gpg-agent
+ gpg-connect-agent updatestartuptty /bye > /dev/null
+ ${brewPath}
+ ''
+ ];
shellAliases = {
cat = "bat --paging=never";
diff = "delta";
@@ -197,8 +202,9 @@
recursive = true;
};
+ xdg.enable = true;
+
home.sessionVariables = {
- XDG_CONFIG_HOME = "${config.home.homeDirectory}/.config";
EDITOR = "${pkgs.neovim}/bin/nvim";
};
diff --git a/users/ryan/darwin.nix b/users/ryan/darwin.nix
index 568306a..0744b2f 100644
--- a/users/ryan/darwin.nix
+++ b/users/ryan/darwin.nix
@@ -10,14 +10,10 @@
ryan.sketchybar.enable = true;
- programs.zen-browser = {
- package = null; # managed via homebrew
- darwinDefaultsId = "app.zen-browser.zen";
- };
-
# Need special config for gpg-agent on macOS
home.file.".gnupg/gpg-agent.conf".text = ''
pinentry-program ${pkgs.pinentry_mac}/Applications/pinentry-mac.app/Contents/MacOS/pinentry-mac
+ extra-socket ${config.home.homeDirectory}/.gnupg/S.gpg-agent.extra
enable-ssh-support
'';
diff --git a/users/ryan/linux.nix b/users/ryan/linux.nix
index add81df..241ed3f 100644
--- a/users/ryan/linux.nix
+++ b/users/ryan/linux.nix
@@ -107,6 +107,7 @@
chromium
foot
kdePackages.kdenlive
+ obs-studio
waybar
fuzzel
@@ -119,8 +120,5 @@
inputs.clipboard-sync.packages.${pkgs.stdenv.hostPlatform.system}.default
- # hyprlock itself is provided by the host-specific module (e.g.
- # linux/guix.nix wraps it with an LD_PRELOAD shim; a plain NixOS host
- # would just add pkgs.hyprlock here instead).
];
}
diff --git a/users/ryan/linux/guix.nix b/users/ryan/linux/guix.nix
index 21fea52..db305af 100644
--- a/users/ryan/linux/guix.nix
+++ b/users/ryan/linux/guix.nix
@@ -1,72 +1,33 @@
{ config, pkgs, lib, inputs, ... }:
+# Host glue for the Guix System laptop. Everything generic about running
+# standalone home-manager on Guix lives in ../modules/targets/guix; this file
+# only holds what's specific to this machine.
{
- systemd.user.startServices = false;
+ imports = [ ../modules/targets/guix ];
- programs.zsh.profileExtra = ''
- if [ -f "$HOME/.guix-home/setup-environment" ]; then
- HOME_ENVIRONMENT="$HOME/.guix-home"
- . "$HOME_ENVIRONMENT/setup-environment"
- "$HOME_ENVIRONMENT/on-first-login"
- unset HOME_ENVIRONMENT
- fi
+ targets.guix.enable = true;
- # /etc/profile normally puts ~/.config/guix/current (the guix pull
- # profile with our actual channels: nonguix, rosenthal, ...) ahead of
- # /run/current-system/profile on PATH, but that only happens for bash
- # login shells -- zsh never sources /etc/profile. Without this, `guix`
- # resolves to the bare system profile's guix, which doesn't know about
- # our channels.
- if [ -d "$HOME/.config/guix/current" ]; then
- export PATH="$HOME/.config/guix/current/bin:$PATH"
- export INFOPATH="$HOME/.config/guix/current/share/info''${INFOPATH:+:}$INFOPATH"
- fi
- '';
+ # Guix's mesa doesn't match nixpkgs', so GL apps built by nix go through
+ # nixGL. Setting packages also makes config.lib.nixGL.wrap real (it's a
+ # no-op otherwise) and supersedes HM's systemd-tmpfiles-based GPU setup.
+ targets.genericLinux.nixGL = {
+ packages = inputs.nixgl.packages;
+ defaultWrapper = "mesa"; # nixGLIntel under the hood
+ };
+ home.packages = [
+ # Kept under its old name for anything that calls it directly.
+ inputs.nixgl.packages.${pkgs.stdenv.hostPlatform.system}.nixGLIntel
+ ];
- dconf.enable = false;
+ targets.guix.pamWrapped = [ pkgs.hyprlock ];
+ # gpg-agent is socket-activated through Shepherd now (translated from HM's
+ # gpg-agent.socket units), so no launch here; Hyprland's own environment
+ # still needs the socket path for apps it spawns.
wayland.windowManager.hyprland.extraConfig = ''
- exec-once = sh -c 'gpgconf --launch gpg-agent; hyprctl setenv SSH_AUTH_SOCK "$(gpgconf --list-dirs agent-ssh-socket)"'
+ exec-once = sh -c 'hyprctl setenv SSH_AUTH_SOCK "$(gpgconf --list-dirs agent-ssh-socket)"'
'';
programs.gpg.scdaemonSettings.disable-ccid = true;
-
- # Guix needs a special fonts.conf file in the user env since it doesn't
- # have one in /etc/fonts/fonts.conf from nixos
- xdg.configFile."fontconfig-nix/fonts.conf".text = ''
- <?xml version='1.0'?>
- <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
- <fontconfig>
- <include ignore_missing="yes">${pkgs.fontconfig.out}/etc/fonts/conf.d</include>
- <include ignore_missing="yes">${config.home.homeDirectory}/.config/fontconfig/conf.d</include>
- <dir>${config.home.homeDirectory}/.guix-home/profile/share/fonts</dir>
- <cachedir>${config.home.homeDirectory}/.cache/fontconfig</cachedir>
- </fontconfig>
- '';
-
- home.sessionVariables.FONTCONFIG_FILE = "${config.home.homeDirectory}/.config/fontconfig-nix/fonts.conf";
-
- home.packages = [
- (pkgs.writeScriptBin "hyprlock" ''
- #! ${pkgs.bash}/bin/bash
- export LD_PRELOAD="/run/current-system/profile/lib/libpam.so.0:$LD_PRELOAD"
- exec ${pkgs.hyprlock}/bin/hyprlock "$@"
- '')
-
- # Guix's mesa doesn't match nixpkgs', so GL apps built by nix need nixGL.
- inputs.nixgl.packages.${pkgs.stdenv.hostPlatform.system}.nixGLIntel
- ];
-
- # We need to correct the ssh config file's permissions on guix
- # Remove the symlink and just install the store file directly
- home.activation = {
- fixSshPermissions = lib.hm.dag.entryAfter [ "linkGeneration" ] ''
- run install -d -m 0700 "$HOME/.ssh"
- if [ -L "$HOME/.ssh/config" ]; then
- src="$(readlink -f "$HOME/.ssh/config")"
- run rm -f "$HOME/.ssh/config"
- run install -m 0600 "$src" "$HOME/.ssh/config"
- fi
- '';
- };
}
diff --git a/users/ryan/modules/aria2/default.nix b/users/ryan/modules/aria2/default.nix
new file mode 100644
index 0000000..8082fc1
--- /dev/null
+++ b/users/ryan/modules/aria2/default.nix
@@ -0,0 +1,185 @@
+# users/ryan/modules/aria2/default.nix
+#
+# aria2c RPC daemon + Zen hand-off via baptistecdr's "Aria2 Integration"
+# (AMO slug aria2-extension). The RPC secret is generated once at activation
+# into $XDG_STATE_HOME/aria2/rpc-secret (0600) and never touches the repo or
+# the store; the same activation splices it into both aria2's runtime conf and
+# the extension's storage.sync row.
+{ config, lib, pkgs, ... }:
+let
+ cfg = config.ryan.aria2;
+ zen = config.programs.zen-browser;
+ inherit (pkgs.stdenv.hostPlatform) isDarwin isLinux;
+
+ stateDir = "${config.xdg.stateHome}/aria2";
+ secretFile = "${stateDir}/rpc-secret";
+ runtimeConf = "${stateDir}/aria2.conf";
+ sessionFile = "${stateDir}/session";
+
+ extId = "baptistecdr@users.noreply.github.com";
+ # Fixed so captureServer can point at it and re-seeding is idempotent.
+ serverUuid = "6f1c2b7e-0a43-4d8e-9c55-3b2a1e0d9f10";
+
+ # Secret-free half of the config; rpc-secret is appended at activation.
+ baseConf = (pkgs.formats.keyValue { }).generate "aria2-base.conf" ({
+ enable-rpc = true;
+ rpc-listen-all = false;
+ rpc-listen-port = cfg.rpcPort;
+ dir = cfg.downloadDir;
+ input-file = sessionFile;
+ save-session = sessionFile;
+ save-session-interval = 60;
+ continue = true;
+ max-connection-per-server = 8;
+ split = 8;
+ min-split-size = "1M";
+ file-allocation = if isDarwin then "none" else "falloc";
+ log = "${stateDir}/aria2.log";
+ log-level = "warn";
+ } // lib.optionalAttrs isLinux {
+ #ca-certificate = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
+ });
+
+ # Extension state as stored by src/models/extension-options.ts: one
+ # storage.sync key, "options", holding a *stringified* JSON blob.
+ extOptions = pkgs.writeText "aria2-ext-options.json" (builtins.toJSON {
+ servers.${serverUuid} = {
+ uuid = serverUuid;
+ name = "Local aria2";
+ secure = false;
+ host = "127.0.0.1";
+ port = cfg.rpcPort;
+ path = "/jsonrpc";
+ secret = ""; # filled at activation
+ rpcParameters = { };
+ };
+ captureServer = serverUuid;
+ captureDownloads = true;
+ minFileSizeInBytes = 0;
+ excludedProtocols = [ "blob" "data" ];
+ excludedSites = [ ];
+ excludedFileTypes = [ ];
+ useCompleteFilePath = false;
+ notifyUrlIsAdded = true;
+ notifyFileIsAdded = false;
+ notifyErrorOccurs = true;
+ });
+
+ daemon = pkgs.writeShellApplication {
+ name = "aria2-daemon";
+ runtimeInputs = [ pkgs.aria2 ];
+ text = ''exec aria2c --conf-path="${runtimeConf}" "$@"'';
+ };
+in
+{
+ options.ryan.aria2 = {
+ enable = lib.mkEnableOption "aria2 RPC daemon wired into Zen";
+ rpcPort = lib.mkOption { type = lib.types.port; default = 6800; };
+ downloadDir = lib.mkOption {
+ type = lib.types.str;
+ default = "${config.home.homeDirectory}/Downloads";
+ };
+ daemon = lib.mkOption {
+ type = lib.types.package;
+ readOnly = true;
+ default = daemon;
+ description = "Wrapper that runs aria2c against the runtime conf.";
+ };
+ };
+
+ config = lib.mkIf cfg.enable (lib.mkMerge [
+ {
+ home.packages = [ pkgs.aria2 daemon ];
+
+ # Secret + runtime conf. Must precede the launchd/systemd/Shepherd reload and the
+ # Zen fragment, all of which consume what this writes.
+ home.activation.aria2Runtime =
+ lib.hm.dag.entryBetween
+ [ "setupLaunchAgents" "reloadSystemd" "shepherdServices" "zen-browser-default" ]
+ [ "writeBoundary" ] ''
+ if [[ ! -v DRY_RUN ]]; then
+ install -d -m 0700 "${stateDir}"
+ if [ ! -s "${secretFile}" ]; then
+ (umask 077; ${pkgs.openssl}/bin/openssl rand -hex 32 > "${secretFile}")
+ fi
+ [ -e "${sessionFile}" ] || touch "${sessionFile}"
+ tmp="$(mktemp "${stateDir}/.aria2.conf.XXXXXX")"
+ { cat ${baseConf}; printf 'rpc-secret=%s\n' "$(cat "${secretFile}")"; } > "$tmp"
+ chmod 0600 "$tmp"
+ mv -f "$tmp" "${runtimeConf}"
+ fi
+ '';
+ }
+
+ (lib.mkIf config.ryan.zen.enable {
+ ryan.zen.extensions.aria2 = {
+ id = extId;
+ version = "4.15.4";
+ };
+
+ # The extension only reads storage.sync (no storage.managed), so
+ # 3rdparty policy can't configure it. With Sync disabled, storage.sync
+ # is the local webext-storage DB; upsert our row there while Zen is
+ # closed. Declarative-owned: UI edits to this extension get reverted.
+ programs.zen-browser.activationFragments.default = [{
+ requiresLock = true;
+ skipSubject = "aria2 extension config";
+ text = ''
+ db="${zen.profilesPath}/${zen.profiles.default.path}/storage-sync-v2.sqlite"
+ if [ ! -f "$db" ]; then
+ echo "aria2: $db not created yet; start Zen once (extension initialises storage.sync), quit, rebuild."
+ elif [[ ! -v DRY_RUN ]]; then
+ payload="$(mktemp)"
+ ${lib.getExe pkgs.jq} -nc \
+ --arg secret "$(cat "${secretFile}")" \
+ --arg uuid "${serverUuid}" \
+ --slurpfile opts ${extOptions} \
+ '{options: ($opts[0] | .servers[$uuid].secret = $secret | tojson)}' > "$payload"
+ ${lib.getBin pkgs.sqlite}/bin/sqlite3 "$db" <<SQL
+ INSERT INTO storage_sync_data (ext_id, data, sync_change_counter)
+ VALUES ('${extId}', CAST(readfile('$payload') AS TEXT), 1)
+ ON CONFLICT(ext_id) DO UPDATE SET
+ data = excluded.data,
+ sync_change_counter = sync_change_counter + 1;
+ SQL
+ rm -f "$payload"
+ fi
+ '';
+ }];
+ })
+
+ (lib.mkIf isDarwin {
+ launchd.agents.aria2 = {
+ enable = true;
+ config = {
+ ProgramArguments = [ (lib.getExe daemon) ];
+ RunAtLoad = true;
+ KeepAlive = true;
+ ProcessType = "Background";
+ # Not read by aria2; changes the plist whenever the base conf
+ # changes, so setupLaunchAgents reloads the agent.
+ EnvironmentVariables.ARIA2_BASE_CONF = "${baseConf}";
+ };
+ };
+ })
+
+ # Linux: a plain systemd user unit. On Guix, targets.guix translates it
+ # to a Shepherd service; on NixOS / systemd distros it runs as-is.
+ (lib.mkIf isLinux {
+ systemd.user.services.aria2 = {
+ Unit = {
+ Description = "aria2 RPC daemon";
+ Documentation = "man:aria2c(1)";
+ # Unit text changes with the base conf -> sd-switch restarts it.
+ X-Restart-Triggers = [ "${baseConf}" ];
+ };
+ Service = {
+ ExecStart = lib.getExe daemon;
+ Restart = "on-failure";
+ };
+ Install.WantedBy = [ "default.target" ];
+ };
+ })
+
+ ]);
+}
diff --git a/users/ryan/modules/targets/guix/default.nix b/users/ryan/modules/targets/guix/default.nix
new file mode 100644
index 0000000..8433bb6
--- /dev/null
+++ b/users/ryan/modules/targets/guix/default.nix
@@ -0,0 +1,254 @@
+# targets.guix: make standalone home-manager behave on Guix System.
+#
+# The point is that other modules stay written against upstream
+# home-manager options (systemd.user.services, services.*, programs.*) and
+# this target adapts them, instead of each module growing a Guix branch.
+{ config, lib, pkgs, osConfig ? null, ... }:
+let
+ cfg = config.targets.guix;
+ sh = import ./shepherd.nix { inherit lib; setpriv = pkgs.util-linux; };
+
+ result = sh.translate {
+ systemdUser = config.systemd.user;
+ inherit (cfg.shepherd) unenforced ignoreUnits;
+ };
+
+ servicesDir = pkgs.runCommandLocal "hm-shepherd-services" { } (''
+ mkdir -p $out
+ '' + lib.concatStrings (lib.mapAttrsToList (file: text: ''
+ cp ${pkgs.writeText file text} $out/${file}
+ '') result.files));
+
+ stateDir = "${config.xdg.stateHome}/home-manager/shepherd";
+
+ findHerd = ''
+ herd="$HOME/.guix-home/profile/bin/herd"
+ [ -x "$herd" ] || herd="$(command -v herd || true)"
+ '';
+
+ # Compositor -> Shepherd environment bridge (replaces
+ # `dbus-update-activation-environment --systemd` + hyprland-session.target).
+ sessionBridge = pkgs.writeShellScript "hm-shepherd-session" ''
+ ${findHerd}
+ [ -n "$herd" ] || { echo "hm-shepherd-session: herd not found" >&2; exit 1; }
+ # Stop first: dependents go down with it and come back with the new env.
+ "$herd" stop ${sh.graphicalSym} >/dev/null 2>&1 || true
+ expr="(begin"
+ for v in ${lib.escapeShellArgs cfg.shepherd.sessionVariables}; do
+ if [ -n "''${!v+x}" ]; then
+ val="''${!v}"; val="''${val//\\/\\\\}"; val="''${val//\"/\\\"}"
+ expr+=" (setenv \"$v\" \"$val\")"
+ else
+ expr+=" (unsetenv \"$v\")"
+ fi
+ done
+ expr+=" #t)"
+ "$herd" eval root "$expr"
+ "$herd" start ${sh.graphicalSym}
+ ${lib.concatMapStrings (s: ''
+ "$herd" start ${lib.escapeShellArg s}
+ '') result.graphical}
+ '';
+in
+{
+ options.targets.guix = {
+ enable = lib.mkEnableOption "Guix System integration for standalone home-manager";
+
+ pamWrapped = lib.mkOption {
+ type = lib.types.listOf lib.types.package;
+ default = [ ];
+ example = lib.literalExpression "[ pkgs.hyprlock ]";
+ description = ''
+ Packages that authenticate through PAM. Nix's libpam can't drive Guix's
+ PAM stack, so their binaries are re-exported with Guix's libpam
+ preloaded. Each must set meta.mainProgram.
+ '';
+ };
+
+ shepherd = {
+ unenforced = lib.mkOption {
+ type = with lib.types; attrsOf (listOf str);
+ default = { };
+ example = { "foo.service" = [ "Service.ProtectSystem" "Service.PrivateTmp" ]; };
+ description = ''
+ Per-unit acknowledgement that the listed `Section.Key`s (or
+ `Section.*`) are dropped when translating to Shepherd. Without an
+ entry, any untranslatable key is an evaluation error. Each dropped
+ key is reported as a build warning on every switch.
+ '';
+ };
+ ignoreUnits = lib.mkOption {
+ type = with lib.types; listOf str;
+ default = [ ];
+ example = [ "tray.target" ];
+ description = "Full unit names to skip entirely (not translated, no error).";
+ };
+ sessionBridge = lib.mkOption {
+ type = lib.types.package;
+ readOnly = true;
+ default = sessionBridge;
+ description = "Script the compositor runs (exec-once) to start graphical services; also usable by hand.";
+ };
+ sessionVariables = lib.mkOption {
+ type = with lib.types; listOf str;
+ default = config.wayland.windowManager.hyprland.systemd.variables
+ ++ [ "DBUS_SESSION_BUS_ADDRESS" "SSH_AUTH_SOCK" ];
+ defaultText = lib.literalExpression ''hyprland.systemd.variables ++ [ "DBUS_SESSION_BUS_ADDRESS" "SSH_AUTH_SOCK" ]'';
+ description = "Variables the compositor pushes into Shepherd before starting graphical services.";
+ };
+ };
+ };
+
+ config = lib.mkIf cfg.enable (lib.mkMerge [
+ {
+ assertions = [
+ {
+ # osConfig is only passed when home-manager runs as a NixOS or
+ # nix-darwin module, i.e. never on a Guix host.
+ assertion = osConfig == null;
+ message = "targets.guix is enabled inside a NixOS/nix-darwin home-manager module; it only applies to standalone home-manager on Guix System.";
+ }
+ {
+ assertion = config.targets.genericLinux.enable;
+ message = "targets.guix builds on targets.genericLinux; enable it too.";
+ }
+ ] ++ map (e: { assertion = false; message = "targets.guix: ${e}"; }) result.errors;
+
+ warnings = result.warnings;
+
+ # HM's ssh-auth-sock module (pulled in by gpg-agent's SSH support) adds
+ # a oneshot that runs `dbus-update-activation-environment --systemd
+ # SSH_AUTH_SOCK`; its whole job is systemd/D-Bus environment plumbing.
+ # The shell integration from the same module still sets the variable,
+ # and the session bridge pushes it into Shepherd (sessionVariables).
+ targets.guix.shepherd.ignoreUnits = [ "set-SSH_AUTH_SOCK.service" ];
+
+ # No systemd user manager: units are translated to Shepherd instead.
+ systemd.user.startServices = false;
+
+ # HM's GPU setup installs /etc/tmpfiles.d + runs systemd-tmpfiles as
+ # root; neither exists on Guix, so it only produces a warning per switch.
+ targets.genericLinux.gpu.enable = lib.mkDefault false;
+
+ # Guix's SSH patches openssh to not allow files outside of the GNU store
+ # so we need to copy the SSH config from the store into the userr's
+ # home directory to not get a permission error
+ home.activation.removeSshConfigSymlink = lib.mkIf config.programs.ssh.enable (
+ lib.hm.dag.entryBefore [ "checkLinkTargets" ] ''
+ if [ -n "''${HOME:-}" ] && [ -e "$HOME/.ssh/config" ]; then
+ rm -f "$HOME/.ssh/config"
+ fi
+ ''
+ );
+
+ home.activation.fixSshPermissions = lib.mkIf config.programs.ssh.enable (
+ lib.hm.dag.entryAfter [ "linkGeneration" ] ''
+ run install -d -m 0700 "$HOME/.ssh"
+ if [ -L "$HOME/.ssh/config" ]; then
+ src="$(readlink -f "$HOME/.ssh/config")"
+ run rm -f "$HOME/.ssh/config"
+ run install -m 0600 "$src" "$HOME/.ssh/config"
+ fi
+ ''
+ );
+
+ # Its exec-once runs `dbus-update-activation-environment --systemd &&
+ # systemctl --user ...`, which fails at the first step on Guix. The
+ # bridge below does the Shepherd equivalent.
+ wayland.windowManager.hyprland.systemd.enable = lib.mkDefault false;
+ wayland.windowManager.hyprland.extraConfig = lib.mkIf config.wayland.windowManager.hyprland.enable ''
+ exec-once = ${sessionBridge}
+ '';
+
+ dconf.enable = lib.mkDefault false;
+
+ # Guix Home owns ~/.zprofile's job of loading its environment (and
+ # starting the user Shepherd via on-first-login); keep that working
+ # under an HM-managed zsh.
+ programs.zsh.profileExtra = lib.mkBefore ''
+ if [ -f "$HOME/.guix-home/setup-environment" ]; then
+ HOME_ENVIRONMENT="$HOME/.guix-home"
+ . "$HOME_ENVIRONMENT/setup-environment"
+ "$HOME_ENVIRONMENT/on-first-login"
+ unset HOME_ENVIRONMENT
+ fi
+
+ # /etc/profile puts ~/.config/guix/current (guix pull profile with
+ # our channels) ahead of the system profile, but only bash login
+ # shells source it.
+ if [ -d "$HOME/.config/guix/current" ]; then
+ export PATH="$HOME/.config/guix/current/bin:$PATH"
+ export INFOPATH="$HOME/.config/guix/current/share/info''${INFOPATH:+:}$INFOPATH"
+ fi
+ '';
+
+ # No /etc/fonts/fonts.conf from Nix's point of view on Guix.
+ xdg.configFile."fontconfig-nix/fonts.conf".text = ''
+ <?xml version='1.0'?>
+ <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
+ <fontconfig>
+ <include ignore_missing="yes">${pkgs.fontconfig.out}/etc/fonts/conf.d</include>
+ <include ignore_missing="yes">${config.xdg.configHome}/fontconfig/conf.d</include>
+ <dir>${config.home.homeDirectory}/.guix-home/profile/share/fonts</dir>
+ <cachedir>${config.xdg.cacheHome}/fontconfig</cachedir>
+ </fontconfig>
+ '';
+ home.sessionVariables.FONTCONFIG_FILE = "${config.xdg.configHome}/fontconfig-nix/fonts.conf";
+
+ home.packages = map
+ (p: lib.hiPrio (pkgs.writeShellScriptBin p.meta.mainProgram ''
+ export LD_PRELOAD="/run/current-system/profile/lib/libpam.so.0''${LD_PRELOAD:+:$LD_PRELOAD}"
+ exec ${lib.getExe p} "$@"
+ ''))
+ cfg.pamWrapped;
+
+ # Sync generated Shepherd services; reload only what changed.
+ home.activation.shepherdServices = lib.hm.dag.entryAfter [ "linkGeneration" "reloadSystemd" ] ''
+ if [ ! -x /run/current-system/profile/bin/guix ]; then
+ errorEcho "targets.guix: this host isn't Guix System; refusing to manage Shepherd services."
+ exit 1
+ fi
+
+ ${findHerd}
+ live=
+ if [ -n "$herd" ] && "$herd" status root >/dev/null 2>&1; then live=1; fi
+
+ dst=${lib.escapeShellArg stateDir}
+ run mkdir -p "$dst"
+
+ for f in "$dst"/*.scm; do
+ [ -e "$f" ] || continue
+ n="$(basename "$f" .scm)"
+ if [ ! -e ${servicesDir}/"$n".scm ]; then
+ [ -n "$live" ] && run "$herd" unload root "$n" || true
+ run rm -f "$f"
+ fi
+ done
+
+ for f in ${servicesDir}/*.scm; do
+ n="$(basename "$f" .scm)"
+ if ! cmp -s "$f" "$dst/$n.scm"; then
+ run install -m 0644 "$f" "$dst/$n.scm"
+ if [ -n "$live" ]; then
+ run "$herd" unload root "$n" >/dev/null 2>&1 || true
+ run "$herd" load root "$dst/$n.scm"
+ # Autostart services restart themselves from the loaded file;
+ # graphical ones only if a session is up (else they'd start
+ # with no compositor environment).
+ case " ${lib.concatStringsSep " " result.graphical} " in
+ *" $n "*)
+ if "$herd" status ${sh.graphicalSym} 2>/dev/null | grep -q 'It is running'; then
+ run "$herd" start "$n"
+ fi ;;
+ esac
+ fi
+ fi
+ done
+
+ if [ -z "$live" ]; then
+ warnEcho "targets.guix: user Shepherd not reachable; services register at next login via the Guix Home loader."
+ fi
+ '';
+ }
+ ]);
+}
diff --git a/users/ryan/modules/targets/guix/shepherd.nix b/users/ryan/modules/targets/guix/shepherd.nix
new file mode 100644
index 0000000..55750ba
--- /dev/null
+++ b/users/ryan/modules/targets/guix/shepherd.nix
@@ -0,0 +1,408 @@
+# systemd user units (as home-manager models them) -> GNU Shepherd services.
+#
+# Fail-closed by design: every [Section] Key must be either translated with
+# the same semantics, or on the explicit `ignoredKeys` list below (keys that
+# don't change what runs or with what privileges). Anything else is an
+# eval-time error, unless the user acknowledges it per unit via
+# `targets.guix.shepherd.unenforced`, in which case it's dropped *with a
+# build warning*. Sandboxing (Protect*, Private*, SystemCallFilter, ...) is
+# deliberately never translated: a service that declares it is relying on it.
+{ lib, setpriv }:
+let
+ inherit (lib)
+ concatStringsSep concatMapStringsSep concatMapStrings concatMap mapAttrs mapAttrsToList
+ filterAttrs attrNames hasSuffix removeSuffix optional optionals
+ optionalString elem toList last unique;
+
+ # ---------------------------------------------------------------- helpers
+
+ # JSON string escapes are a subset of Guile's (\" \\ \n \uXXXX).
+ q = builtins.toJSON;
+ sym = s: "(string->symbol ${q s})";
+
+ # HM's unit types leave null / [] placeholders for typed-but-unset keys
+ # (Description, Documentation, X-*-Triggers, Environment, ExecStart); HM's
+ # own INI renderer drops them, so do the same.
+ clean = unit:
+ filterAttrs (_: s: s != { })
+ (mapAttrs (_: filterAttrs (_: v: v != null && v != [ ])) unit);
+
+ str = v: if builtins.isBool v then (if v then "true" else "false") else toString v;
+ # Scalar keys: systemd's last assignment wins.
+ scalar = v: str (last (toList v));
+ isTrue = v: elem (scalar v) [ "true" "yes" "on" "1" ];
+
+ # ------------------------------------------------------- key allowlists
+
+ # Keys with no effect on what runs, how, or with what privileges.
+ ignoredKeys = {
+ Unit = [
+ "Description" # consumed as #:documentation
+ "Documentation"
+ "After" # pure ordering; dependencies below imply ordering in Shepherd
+ "Before"
+ "RefuseManualStart"
+ "RefuseManualStop"
+ "X-SwitchMethod"
+ ];
+ Service = [ "ExecReload" ]; # only reachable via `systemctl reload`
+ Socket = [ ];
+ Install = [ ];
+ };
+
+ limitMap = {
+ LimitCPU = "cpu"; LimitFSIZE = "fsize"; LimitDATA = "data";
+ LimitSTACK = "stack"; LimitCORE = "core"; LimitRSS = "rss";
+ LimitNOFILE = "nofile"; LimitAS = "as"; LimitNPROC = "nproc";
+ LimitMEMLOCK = "memlock";
+ };
+
+ translatedKeys = {
+ # Triggers are embedded in the generated file, so a trigger change is a
+ # content change and activation reloads the service (reload -> restart,
+ # the conservative direction).
+ Unit = [ "Wants" "Requires" "BindsTo" "PartOf" "X-Restart-Triggers" "X-Reload-Triggers" ];
+ Service = [
+ "Type" "ExecStart" "Environment" "WorkingDirectory" "Restart"
+ "RestartSec" "UMask" "NoNewPrivileges"
+ ] ++ attrNames limitMap;
+ Socket = [
+ "ListenStream" "FileDescriptorName" "Service" "Accept"
+ "SocketMode" "DirectoryMode"
+ ];
+ Install = [ "WantedBy" ];
+ };
+
+ sectionsFor = kind: [ "Unit" "Install" (if kind == "service" then "Service" else "Socket") ];
+
+ # ----------------------------------------------------- systemd specifiers
+
+ specExprs = {
+ t = ''(getenv "XDG_RUNTIME_DIR")'';
+ h = ''(getenv "HOME")'';
+ U = "(number->string (getuid))";
+ u = "(passwd:name (getpwuid (getuid)))";
+ "%" = q "%";
+ };
+ specParts = s: builtins.split "%(.)" s;
+ badSpecs = s:
+ concatMap
+ (p: optional (builtins.isList p && !(specExprs ? ${builtins.head p})) "%${builtins.head p}")
+ (specParts s);
+ # -> Scheme expression, evaluated at service start (not at load).
+ expand = s:
+ let
+ exprs = concatMap
+ (p: if builtins.isList p then [ specExprs.${builtins.head p} ] else optional (p != "") (q p))
+ (specParts s);
+ in
+ if exprs == [ ] then q ""
+ else if builtins.length exprs == 1 then builtins.head exprs
+ else "(string-append ${concatStringsSep " " exprs})";
+
+ # ------------------------------------------------- command-line splitting
+
+ # systemd quoting, minus the parts we refuse: backslash escapes (C-style in
+ # systemd, so not a literal-char escape) and $VAR expansion are rejected by
+ # cmdErrors rather than approximated.
+ tokenize = s:
+ let
+ ws = c: c == " " || c == "\t" || c == "\n";
+ step = st: c:
+ if st.q != null then
+ (if c == st.q then st // { q = null; } else st // { cur = st.cur + c; })
+ else if c == "\"" || c == "'" then
+ st // { q = c; cur = if st.cur == null then "" else st.cur; }
+ else if ws c then
+ (if st.cur == null then st else st // { out = st.out ++ [ st.cur ]; cur = null; })
+ else
+ st // { cur = (if st.cur == null then "" else st.cur) + c; };
+ end = builtins.foldl' step { out = [ ]; cur = null; q = null; }
+ (lib.stringToCharacters s);
+ in
+ {
+ tokens = end.out ++ optional (end.cur != null) end.cur;
+ unterminated = end.q != null;
+ };
+
+ cmdErrors = what: s:
+ let t = tokenize s; first = if t.tokens == [ ] then "" else builtins.head t.tokens;
+ in
+ optional (t.tokens == [ ]) "${what} is empty"
+ ++ optional t.unterminated "${what} has an unterminated quote"
+ ++ optional (lib.hasInfix "\\" s) "${what} uses backslash escapes (not translated)"
+ ++ optional (lib.hasInfix "$" s) "${what} uses $VAR expansion (not translated)"
+ ++ optional (builtins.match "[-@:+!|].*" first != null)
+ "${what} uses an executable prefix (${builtins.substring 0 1 first}) with no Shepherd equivalent"
+ ++ map (sp: "${what} uses unsupported specifier ${sp}") (badSpecs s);
+
+ # ----------------------------------------------------------- validation
+
+ octal = s: builtins.match "0?[0-7]{3,4}" s != null;
+ limitVal = s: builtins.match "(infinity|[0-9]+)(:(infinity|[0-9]+))?" s != null;
+ seconds = s: builtins.match "([0-9]+)s?" s;
+
+ keyErrors = { kind, name, unit, unenforced }:
+ let
+ full = "${name}.${kind}";
+ allowed = sectionsFor kind;
+ in
+ concatMap
+ (sec:
+ if !(elem sec allowed) then
+ optional (!(elem "${sec}.*" unenforced)) "${full}: section [${sec}] has no Shepherd equivalent"
+ else
+ concatMap
+ (key: optional
+ (!(elem key (translatedKeys.${sec} ++ ignoredKeys.${sec}))
+ && !(elem "${sec}.${key}" unenforced))
+ "${full}: ${sec}.${key} has no Shepherd equivalent")
+ (attrNames unit.${sec}))
+ (attrNames unit);
+
+ # Names Shepherd knows about, used to resolve dependency edges.
+ graphicalSym = "hm-graphical-session";
+
+ # Which service a socket unit activates (systemd default: same name).
+ socketTarget = sname: sock:
+ removeSuffix ".service" (scalar (sock.Socket.Service or "${sname}.service"));
+
+ resolveDep = { self, services, sockets }: dep:
+ if dep == "graphical-session.target" then { ok = graphicalSym; }
+ else if hasSuffix ".service" dep && services ? ${removeSuffix ".service" dep} then
+ { ok = removeSuffix ".service" dep; }
+ else if hasSuffix ".socket" dep && sockets ? ${removeSuffix ".socket" dep} then
+ let target = socketTarget (removeSuffix ".socket" dep) sockets.${removeSuffix ".socket" dep};
+ in if target == self then { skip = true; } else { ok = target; }
+ else { err = "dependency ${dep} is not a translated unit"; };
+
+ serviceErrors = { name, unit, services, sockets, unenforced }:
+ let
+ full = "${name}.service";
+ svc = unit.Service or { };
+ exec = toList (svc.ExecStart or [ ]);
+ deps = concatMap (k: toList (unit.Unit.${k} or [ ])) [ "Wants" "Requires" "BindsTo" "PartOf" ];
+ envTokens = concatMap (e: (tokenize (str e)).tokens) (toList (svc.Environment or [ ]));
+ limits = filterAttrs (k: _: limitMap ? ${k}) svc;
+ in
+ keyErrors { kind = "service"; inherit name unit unenforced; }
+ ++ optional (!(elem (scalar (svc.Type or "simple")) [ "simple" "exec" ]))
+ "${full}: Type=${scalar svc.Type} is not translated (only simple/exec)"
+ ++ optional (builtins.length exec != 1)
+ "${full}: needs exactly one ExecStart (has ${toString (builtins.length exec)})"
+ ++ concatMap (cmdErrors "${full}: ExecStart") (map str exec)
+ ++ concatMap (e: optional (!(lib.hasInfix "=" e)) "${full}: Environment entry '${e}' is not K=V") envTokens
+ ++ concatMap (e: map (sp: "${full}: Environment uses unsupported specifier ${sp}") (badSpecs e)) envTokens
+ ++ optional (lib.any (e: lib.hasInfix "$" e || lib.hasInfix "\\" e) (map str (toList (svc.Environment or [ ]))))
+ "${full}: Environment uses escapes or $VAR (not translated)"
+ ++ optionals (svc ? WorkingDirectory) (
+ let d = scalar svc.WorkingDirectory; in
+ optional (lib.hasPrefix "-" d) "${full}: WorkingDirectory=-... (ignore-missing) is not translated"
+ ++ map (sp: "${full}: WorkingDirectory uses unsupported specifier ${sp}") (badSpecs d))
+ ++ optional (svc ? Restart && !(elem (scalar svc.Restart) [ "no" "always" "on-failure" "on-abnormal" ]))
+ "${full}: Restart=${scalar svc.Restart} is not translated"
+ ++ optional (svc ? RestartSec && seconds (scalar svc.RestartSec) == null)
+ "${full}: RestartSec must be whole seconds"
+ ++ optional (svc ? UMask && !(octal (scalar svc.UMask))) "${full}: UMask must be octal"
+ ++ mapAttrsToList (k: v: "${full}: ${k}=${scalar v} is not a plain integer/infinity limit")
+ (filterAttrs (_: v: !(limitVal (scalar v))) limits)
+ ++ concatMap (d: let r = resolveDep { self = name; inherit services sockets; } d;
+ in optional (r ? err) "${full}: ${r.err}") deps
+ ++ concatMap (t: optional (!(elem t [ "default.target" "graphical-session.target" ]))
+ "${full}: WantedBy=${t} is not translated")
+ (toList (unit.Install.WantedBy or [ ]));
+
+ socketErrors = { name, unit, services, unenforced }:
+ let
+ full = "${name}.socket";
+ sock = unit.Socket or { };
+ target = socketTarget name unit;
+ paths = map str (toList (sock.ListenStream or [ ]));
+ dirMode = if sock ? DirectoryMode then scalar sock.DirectoryMode else null;
+ in
+ keyErrors { kind = "socket"; inherit name unit unenforced; }
+ ++ optional (!(services ? ${target})) "${full}: activates ${target}.service, which is not translated"
+ ++ optional (paths == [ ]) "${full}: no ListenStream"
+ ++ concatMap (p: optional (builtins.match "(/|%t|%h).*" p == null)
+ "${full}: ListenStream=${p} is not a unix socket path (TCP/UDP not translated)")
+ paths
+ ++ concatMap (p: map (sp: "${full}: ListenStream uses unsupported specifier ${sp}") (badSpecs p)) paths
+ ++ optional (sock ? Accept && isTrue sock.Accept) "${full}: Accept=yes (inetd-style) is not translated"
+ ++ optional (dirMode != null && !(octal dirMode)) "${full}: DirectoryMode must be octal"
+ # Shepherd can set the parent directory's mode but not the socket's. A
+ # 0700 parent makes the socket's own mode moot; anything looser doesn't.
+ ++ optional (sock ? SocketMode && !(elem dirMode [ "0700" "700" ]))
+ "${full}: SocketMode is only honoured with DirectoryMode=0700 (Shepherd can't chmod the socket)"
+ ++ concatMap (t: optional (t != "sockets.target") "${full}: WantedBy=${t} is not translated")
+ (toList (unit.Install.WantedBy or [ ]));
+
+ # ------------------------------------------------------------- emission
+
+ prelude = ''
+ (use-modules (shepherd service))
+
+ ;; Unit Environment= first, then Shepherd's *current* environment (which
+ ;; the compositor bridge updates via `herd eval root (setenv ...)`).
+ (define (hm-env overrides)
+ (let ((keys (map (lambda (kv) (substring kv 0 (string-index kv #\=)))
+ overrides)))
+ (append overrides
+ (filter (lambda (kv)
+ (let ((i (string-index kv #\=)))
+ (not (and i (member (substring kv 0 i) keys)))))
+ (environ)))))
+ '';
+
+ limitExpr = v:
+ let
+ parts = lib.splitString ":" (scalar v);
+ one = x: if x == "infinity" then "#f" else x;
+ in
+ if builtins.length parts == 1 then "${one (builtins.head parts)} ${one (builtins.head parts)}"
+ else "${one (builtins.elemAt parts 0)} ${one (builtins.elemAt parts 1)}";
+
+ octalExpr = s: "#o${lib.removePrefix "0" s}";
+
+ emitService = { name, unit, services, sockets }:
+ let
+ svc = unit.Service or { };
+ mySockets = filterAttrs (sn: s: socketTarget sn s == name) sockets;
+ socketActivated = mySockets != { };
+
+ argv0 = (tokenize (str (builtins.head (toList svc.ExecStart)))).tokens;
+ argv = optionals (svc ? NoNewPrivileges && isTrue svc.NoNewPrivileges)
+ [ "${setpriv}/bin/setpriv" "--no-new-privs" ]
+ ++ argv0;
+ cmd = "(list ${concatMapStringsSep " " expand argv})";
+
+ env = concatMap (e: (tokenize (str e)).tokens) (toList (svc.Environment or [ ]));
+ envExpr = "(hm-env (list ${concatMapStringsSep " " (kv:
+ let i = lib.stringLength (builtins.head (lib.splitString "=" kv)); in
+ "(string-append ${q (builtins.substring 0 (i + 1) kv)} ${expand (builtins.substring (i + 1) (-1) kv)})")
+ env}))";
+
+ deps = unique (concatMap
+ (d: let r = resolveDep { self = name; inherit services sockets; } d;
+ in optional (r ? ok) r.ok)
+ (concatMap (k: toList (unit.Unit.${k} or [ ])) [ "Wants" "Requires" "BindsTo" "PartOf" ]));
+ wantedBy = toList (unit.Install.WantedBy or [ ]);
+ graphical = elem "graphical-session.target" wantedBy || elem graphicalSym deps;
+ requirement = unique (deps ++ optional graphical graphicalSym);
+ autostart = elem "default.target" wantedBy
+ || lib.any (s: elem "sockets.target" (toList (s.Install.WantedBy or [ ]))) (lib.attrValues mySockets);
+
+ limits = filterAttrs (k: _: limitMap ? ${k}) svc;
+ opts = concatStringsSep "\n "
+ ([ "#:environment-variables ${envExpr}" ]
+ ++ optional (svc ? WorkingDirectory)
+ "#:directory ${let d = scalar svc.WorkingDirectory; in if d == "~" then specExprs.h else expand d}"
+ ++ optional (svc ? UMask) "#:file-creation-mask ${octalExpr (scalar svc.UMask)}"
+ ++ optional (limits != { }) "#:resource-limits (list ${concatStringsSep " "
+ (mapAttrsToList (k: v: "(list '${limitMap.${k}} ${limitExpr v})") limits)})");
+
+ endpoints = concatStringsSep "\n " (concatMap
+ (sn:
+ let
+ s = mySockets.${sn}.Socket;
+ fdName = scalar (s.FileDescriptorName or sn);
+ dirMode = if s ? DirectoryMode then octalExpr (scalar s.DirectoryMode) else "#o755";
+ in
+ map (p: "(endpoint (make-socket-address AF_UNIX ${expand (str p)}) #:name ${q fdName} #:socket-directory-permissions ${dirMode})")
+ (toList s.ListenStream))
+ (attrNames mySockets));
+
+ # Socket units keep listening after the daemon exits regardless of
+ # Restart= (that's socket-activation semantics), hence respawn? here.
+ respawn = socketActivated || elem (scalar (svc.Restart or "no")) [ "always" "on-failure" "on-abnormal" ];
+ restartSec = if svc ? RestartSec then builtins.head (seconds (scalar svc.RestartSec)) else null;
+
+ constructor =
+ if socketActivated then ''
+ (make-systemd-constructor ${cmd}
+ (list ${endpoints})
+ #:lazy-start? #t
+ ${opts})''
+ else ''
+ (make-forkexec-constructor ${cmd}
+ ${opts})'';
+ in
+ {
+ inherit graphical autostart;
+ text = ''
+ ;; Generated by home-manager (targets.guix) from ${name}.service${
+ optionalString socketActivated " + ${concatMapStringsSep ", " (s: "${s}.socket") (attrNames mySockets)}"
+ }.
+ ;; Do not edit; regenerated on every activation.${
+ concatMapStrings (t: "\n;; trigger: ${str t}")
+ (concatMap (k: toList (unit.Unit.${k} or [ ])) [ "X-Restart-Triggers" "X-Reload-Triggers" ])}
+ ${prelude}
+ (register-services
+ (list
+ (service (list ${sym name})
+ #:documentation ${q (scalar (unit.Unit.Description or "${name} (from home-manager)"))}
+ #:requirement (list ${concatMapStringsSep " " sym requirement})
+ #:respawn? ${if respawn then "#t" else "#f"}${
+ optionalString (restartSec != null) "\n #:respawn-delay ${restartSec}"}
+ ;; Constructed at start time so specifiers and the inherited
+ ;; environment reflect the session at that moment.
+ #:start (lambda args
+ (apply ${constructor}
+ args))
+ #:stop ${if socketActivated then "(make-systemd-destructor)" else "(make-kill-destructor)"})))
+ ${optionalString (autostart && !graphical) "\n(start-in-the-background (list ${sym name}))"}
+ '';
+ };
+
+ graphicalSessionFile = ''
+ ;; Generated by home-manager (targets.guix). Marker for graphical-session.target:
+ ;; started by the compositor bridge after it pushes its environment into
+ ;; Shepherd; stopping it stops every graphical service.
+ (use-modules (shepherd service))
+ (register-services
+ (list (service (list ${sym graphicalSym})
+ #:documentation "Graphical session (home-manager graphical-session.target)"
+ #:start (const #t)
+ #:stop (const #f))))
+ '';
+in
+{
+ inherit graphicalSym;
+
+ # -> { errors, warnings, files = { "<name>.scm" = text; }, graphical = [ names ] }
+ translate = { systemdUser, unenforced, ignoreUnits }:
+ let
+ keep = kind: units: filterAttrs (n: _: !(elem "${n}.${kind}" ignoreUnits)) (mapAttrs (_: clean) units);
+ services = keep "service" (systemdUser.services or { });
+ sockets = keep "socket" (systemdUser.sockets or { });
+ unen = full: unenforced.${full} or [ ];
+
+ # Units that make things happen on their own. Targets and slices are
+ # inert unless something references them, and any such reference
+ # (Wants=/WantedBy=/PartOf=/Slice=) is already an error above.
+ otherKinds = [ "timers" "paths" "mounts" "automounts" ];
+ otherErrors = concatMap
+ (k: map (n: "${n}.${removeSuffix "s" k}: ${removeSuffix "s" k} units are not translated (add to targets.guix.shepherd.ignoreUnits to skip)")
+ (attrNames (keep (removeSuffix "s" k) (systemdUser.${k} or { }))))
+ otherKinds;
+
+ errors =
+ concatMap (n: serviceErrors { name = n; unit = services.${n}; inherit services sockets; unenforced = unen "${n}.service"; }) (attrNames services)
+ ++ concatMap (n: socketErrors { name = n; unit = sockets.${n}; inherit services; unenforced = unen "${n}.socket"; }) (attrNames sockets)
+ ++ otherErrors
+ ++ concatMap (full: optional (!(services ? ${removeSuffix ".service" full}) && !(sockets ? ${removeSuffix ".socket" full}))
+ "targets.guix.shepherd.unenforced: ${full} is not a defined service/socket")
+ (attrNames unenforced);
+
+ warnings = concatMap
+ (full: map (k: "targets.guix: ${full}: ${k} dropped (not enforced under Shepherd)") unenforced.${full})
+ (attrNames unenforced);
+
+ emitted = mapAttrs (n: u: emitService { name = n; unit = u; inherit services sockets; }) services;
+ in
+ {
+ inherit errors warnings;
+ graphical = attrNames (filterAttrs (_: e: e.graphical) emitted);
+ files = { "${graphicalSym}.scm" = graphicalSessionFile; }
+ // lib.mapAttrs' (n: e: lib.nameValuePair "${n}.scm" e.text) emitted;
+ };
+}
diff --git a/users/ryan/modules/zen/default.nix b/users/ryan/modules/zen/default.nix
new file mode 100644
index 0000000..8e7a73b
--- /dev/null
+++ b/users/ryan/modules/zen/default.nix
@@ -0,0 +1,94 @@
+{ config, lib, pkgs, ... }:
+let
+ inherit (lib) mkOption mkEnableOption types mkIf mkMerge;
+ inherit (pkgs.stdenv.hostPlatform) isDarwin;
+ cfg = config.ryan.zen;
+
+ extensionType = types.submodule {
+ options = {
+ enable = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Set false to drop an entry defined elsewhere (attrsOf can't delete keys).";
+ };
+ id = mkOption {
+ type = types.str;
+ description = "Add-on GUID; also the policy key and the AMO API lookup key.";
+ };
+ version = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "Hard pin. null = whatever extensions.lock.json holds (bump with lock.sh).";
+ };
+ mode = mkOption {
+ type = types.enum [ "force_installed" "normal_installed" ];
+ default = "force_installed";
+ };
+ privateBrowsing = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = "Emit private_browsing only when non-null.";
+ };
+ extraSettings = mkOption {
+ type = types.attrsOf types.anything;
+ default = { };
+ description = "Merged last into the ExtensionSettings entry (e.g. default_area).";
+ };
+ };
+ };
+in
+{
+ imports = [ ./extensions.nix ];
+
+ options.ryan.zen = {
+ enable = mkEnableOption "Zen browser with locked policies, profile and extensions";
+
+ extensions = mkOption {
+ type = types.attrsOf extensionType;
+ default = { };
+ description = "Policy-managed extensions, resolved through extensions.lock.json.";
+ };
+
+ exclusive = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Disallow all undeclared extensions and disable all temporary loading";
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ programs.zen-browser = {
+ enable = true;
+ policies = import ./policies.nix;
+ profiles.default = import ./profile.nix;
+ };
+
+ ryan.zen.extensions = {
+ ublock-origin = {
+ id = "uBlock0@raymondhill.net";
+ privateBrowsing = true;
+ };
+ bitwarden = {
+ id = "{446900e4-71c2-419f-a6a7-df9c091e268b}";
+ mode = "normal_installed";
+ privateBrowsing = true;
+ };
+ sponsorblock.id = "sponsorBlocker@ajay.app";
+ dearrow.id = "deArrow@ajay.app";
+ return-youtube-dislikes.id = "{762f9885-5a13-4abd-9c77-433dcd38b8fd}";
+ youtube-nonstop.id = "{0d7cafdd-501c-49ca-8ebb-e3341caaa55e}";
+ tampermonkey.id = "firefox@tampermonkey.net";
+ floccus.id = "floccus@handmadeideas.org";
+ mailvelope.id = "jid1-AQqSMBYb0a8ADg@jetpack";
+ };
+ }
+
+ (mkIf isDarwin {
+ programs.zen-browser = {
+ package = null; # managed via homebrew
+ darwinDefaultsId = "app.zen-browser.zen";
+ };
+ })
+ ]);
+}
diff --git a/users/ryan/modules/zen/extensions.lock.json b/users/ryan/modules/zen/extensions.lock.json
new file mode 100644
index 0000000..cd0df78
--- /dev/null
+++ b/users/ryan/modules/zen/extensions.lock.json
@@ -0,0 +1,62 @@
+{
+ "aria2": {
+ "hash": "sha256-0V6zKAqz8uKlDE4q/szSW732B9X0TUKdo8qUChv2IW0=",
+ "id": "baptistecdr@users.noreply.github.com",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/5055892/aria2_extension-4.15.4.xpi",
+ "version": "4.15.4"
+ },
+ "bitwarden": {
+ "hash": "sha256-XdbvpdIo2+rHoMaog/lReH3GbleYUyLOVJIQsN+NBUw=",
+ "id": "{446900e4-71c2-419f-a6a7-df9c091e268b}",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/5076543/bitwarden_password_manager-2026.9.3.xpi",
+ "version": "2026.9.3"
+ },
+ "dearrow": {
+ "hash": "sha256-MVGlHbgJN0a+ZGwEGvPq1VPl3pX6Tr1frgM+A54QVtE=",
+ "id": "deArrow@ajay.app",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4897568/dearrow-2.3.10.xpi",
+ "version": "2.3.10"
+ },
+ "floccus": {
+ "hash": "sha256-FOk0NhveQToZjq88hRU3wjJVwR82ZQ/DJAz6hqlkJYg=",
+ "id": "floccus@handmadeideas.org",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/5063393/floccus-5.11.0.xpi",
+ "version": "5.11.0"
+ },
+ "mailvelope": {
+ "hash": "sha256-MUAU3OzESJWHE9zkuylyMh8/4nxj725urSDouFA5Juw=",
+ "id": "jid1-AQqSMBYb0a8ADg@jetpack",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4846833/mailvelope-6.3.0.xpi",
+ "version": "6.3.0"
+ },
+ "return-youtube-dislikes": {
+ "hash": "sha256-WXGXSfbfOMFgHKXznAIVjV8AXEPp7uofGVJ/RoyUEhc=",
+ "id": "{762f9885-5a13-4abd-9c77-433dcd38b8fd}",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/5012638/return_youtube_dislikes-4.0.6.xpi",
+ "version": "4.0.6"
+ },
+ "sponsorblock": {
+ "hash": "sha256-DVDhYyxvFe4VpUPmcOHFcpdGBaXAJiKRbgjgJoA9+D8=",
+ "id": "sponsorBlocker@ajay.app",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4897574/sponsorblock-6.1.7.xpi",
+ "version": "6.1.7"
+ },
+ "tampermonkey": {
+ "hash": "sha256-GQAxx428VpYRSDVgHyyOa4Va0eE0313yePj8FYwGWQg=",
+ "id": "firefox@tampermonkey.net",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4797143/tampermonkey-5.5.0.xpi",
+ "version": "5.5.0"
+ },
+ "ublock-origin": {
+ "hash": "sha256-W3RBWGBFY3BkS9gPFhJehlsObDVrtd/PuEBpln6qUoc=",
+ "id": "uBlock0@raymondhill.net",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/5034826/ublock_origin-1.75.0.xpi",
+ "version": "1.75.0"
+ },
+ "youtube-nonstop": {
+ "hash": "sha256-dlnRgPduqQjqgbhO2b3RiGJOqqYriKzL5titToyu/zg=",
+ "id": "{0d7cafdd-501c-49ca-8ebb-e3341caaa55e}",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4187690/youtube_nonstop-0.9.2.xpi",
+ "version": "0.9.2"
+ }
+}
diff --git a/users/ryan/modules/zen/extensions.nix b/users/ryan/modules/zen/extensions.nix
new file mode 100644
index 0000000..f91984b
--- /dev/null
+++ b/users/ryan/modules/zen/extensions.nix
@@ -0,0 +1,89 @@
+# version = null -> whatever the lock holds; `lock.sh <attr>` bumps it
+# version = "x.y.z" -> hard pin; lock.sh resolves exactly that version and
+# eval fails if the lock disagrees
+{ config, lib, pkgs, ... }:
+let
+ inherit (lib) mapAttrs' mapAttrsToList nameValuePair optionalAttrs;
+ cfg = config.ryan.zen;
+ exts = lib.filterAttrs (_: e: e.enable) cfg.extensions;
+ zen = config.programs.zen-browser;
+
+ lockFile = ./extensions.lock.json;
+ lock = lib.importJSON lockFile;
+
+ # Validated lock entry. Throws (not assertions) so the message surfaces no
+ # matter which consumer forces the policy first (HM wrapper, darwin defaults).
+ locked = name: e:
+ let
+ l = lock.${name} or (throw
+ "zen extension '${name}' has no entry in extensions.lock.json; run modules/zen/lock.sh");
+ in
+ if l.id != e.id then throw
+ "zen extension '${name}': lock has id ${l.id}, config has ${e.id}; re-run lock.sh"
+ else if e.version != null && l.version != e.version then throw
+ "zen extension '${name}': pinned ${e.version}, lock has ${l.version}; re-run lock.sh"
+ else l;
+
+ xpi = name: e:
+ let l = locked name e; in
+ pkgs.fetchurl {
+ name = "zen-ext-${name}-${l.version}.xpi";
+ inherit (l) url hash;
+ };
+
+ settingsFor = name: e:
+ {
+ install_url = "file://${xpi name e}";
+ installation_mode = e.mode;
+ updates_disabled = true;
+ }
+ // optionalAttrs (e.privateBrowsing != null) { private_browsing = e.privateBrowsing; }
+ // e.extraSettings;
+
+ pins = pkgs.writeText "zen-extension-pins.json" (builtins.toJSON
+ (mapAttrsToList (name: e: { inherit (e) id; version = (locked name e).version; }) exts));
+
+ jq = lib.getExe pkgs.jq;
+ sort = "${pkgs.coreutils}/bin/sort";
+in
+{
+ config = lib.mkIf (cfg.enable && exts != { }) {
+ programs.zen-browser.policies = {
+ # Locks extensions.update.enabled=false; belt to updates_disabled's braces.
+ ExtensionUpdate = false;
+ ExtensionSettings =
+ mapAttrs' (name: e: nameValuePair e.id (settingsFor name e)) exts
+ // optionalAttrs cfg.exclusive { "*".installation_mode = "blocked"; };
+ };
+
+ # Explicit GC root for every pinned XPI, independent of how the policy
+ # gets serialized on each platform. Also a handy place to inspect them.
+ home.file.".local/share/zen-extensions".source = pkgs.linkFarm "zen-extensions"
+ (mapAttrsToList (name: e: { name = "${name}.xpi"; path = xpi name e; }) exts);
+
+ # The policy engine refuses downgrades (installed > pinned is cancelled in
+ # PoliciesHelpers installAddonFromURL). For exactly that case, delete the
+ # installed XPI while Zen is closed; the next start drops it from the DB
+ # and the policy installs the pin. Upgrades are left to the policy engine.
+ programs.zen-browser.activationFragments.default = [{
+ requiresLock = true;
+ skipSubject = "zen extension downgrade check";
+ text = ''
+ prof="${zen.profilesPath}/${zen.profiles.default.path}"
+ if [ -f "$prof/extensions.json" ]; then
+ while IFS=$'\t' read -r id want have; do
+ [ -n "$have" ] && [ "$have" != "$want" ] || continue
+ newest="$(printf '%s\n%s\n' "$want" "$have" | ${sort} -V | tail -n1)"
+ [ "$newest" = "$have" ] || continue
+ echo "zen-extensions: $id installed $have > pinned $want; removing installed XPI"
+ [[ -v DRY_RUN ]] || rm -f "$prof/extensions/$id.xpi"
+ done < <(${jq} -r --slurpfile ej "$prof/extensions.json" '
+ .[] | . as $p
+ | [ $p.id, $p.version,
+ ([ $ej[0].addons[] | select(.id == $p.id and .location == "app-profile") | .version ][0] // "") ]
+ | @tsv' ${pins})
+ fi
+ '';
+ }];
+ };
+}
diff --git a/users/ryan/modules/zen/lock.sh b/users/ryan/modules/zen/lock.sh
new file mode 100755
index 0000000..a375fd8
--- /dev/null
+++ b/users/ryan/modules/zen/lock.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+# Resolve ryan.zen.extensions into extensions.lock.json via AMO's v5 API.
+#
+# lock.sh <attr> [--missing]
+#
+# <attr> is the evaluated home-manager config, e.g.
+# ~/.config/home-manager#homeConfigurations.ryan.config
+# ~/.config/nix#darwinConfigurations.<host>.config.home-manager.users.ryan
+#
+# Default: re-resolve everything (unpinned entries move to AMO's current version).
+# --missing: keep existing entries that still satisfy the spec; resolve the rest.
+#
+# Only ryan.zen.extensions.*.{id,version} is evaluated, which never touches the
+# lock, so this works when the lock is empty or stale.
+# Needs: nix (>= 2.19 for `nix hash convert`), curl, jq.
+set -euo pipefail
+
+attr="${1:?usage: lock.sh <hm-config-attr> [--missing]}"
+mode="${2:-all}"
+here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+lockfile="$here/extensions.lock.json"
+api="https://addons.mozilla.org/api/v5/addons/addon"
+
+spec="$(nix eval --json "$attr.ryan.zen.extensions" \
+ --apply 'es: builtins.mapAttrs (_: e: { inherit (e) id version; }) (builtins.removeAttrs es (builtins.filter (n: !es.${n}.enable) (builtins.attrNames es)))')"
+old="$(cat "$lockfile" 2>/dev/null || echo '{}')"
+new='{}'
+
+uri() { jq -rn --arg s "$1" '$s|@uri'; }
+
+for name in $(jq -r 'keys[]' <<<"$spec"); do
+ id="$(jq -r --arg n "$name" '.[$n].id' <<<"$spec")"
+ want="$(jq -r --arg n "$name" '.[$n].version // empty' <<<"$spec")"
+
+ if [ "$mode" = "--missing" ]; then
+ keep="$(jq -c --arg n "$name" --arg id "$id" --arg v "$want" \
+ '.[$n] | select(. != null and .id == $id and ($v == "" or .version == $v))' <<<"$old")"
+ if [ -n "$keep" ]; then
+ new="$(jq --arg n "$name" --argjson e "$keep" '.[$n] = $e' <<<"$new")"
+ continue
+ fi
+ fi
+
+ if [ -n "$want" ]; then
+ v="$(curl -fsSL "$api/$(uri "$id")/versions/$(uri "$want")/")"
+ else
+ v="$(curl -fsSL "$api/$(uri "$id")/" | jq '.current_version')"
+ fi
+
+ version="$(jq -r '.version' <<<"$v")"
+ url="$(jq -r '.file.url' <<<"$v")"
+ algo_hex="$(jq -r '.file.hash' <<<"$v")" # "sha256:<hex>"
+ [ "${algo_hex%%:*}" = sha256 ] || { echo "$name: unexpected hash $algo_hex" >&2; exit 1; }
+ hash="$(nix hash convert --hash-algo sha256 --to sri "${algo_hex#sha256:}")"
+
+ echo "$name ($id) -> $version" >&2
+ new="$(jq --arg n "$name" --arg id "$id" --arg ver "$version" --arg url "$url" --arg h "$hash" \
+ '.[$n] = {id: $id, version: $ver, url: $url, hash: $h}' <<<"$new")"
+done
+
+jq -S . <<<"$new" > "$lockfile.tmp" && mv "$lockfile.tmp" "$lockfile"
diff --git a/users/ryan/modules/zen/policies.nix b/users/ryan/modules/zen/policies.nix
new file mode 100644
index 0000000..93a5324
--- /dev/null
+++ b/users/ryan/modules/zen/policies.nix
@@ -0,0 +1,78 @@
+let
+ lock-false = {
+ Value = false;
+ Status = "locked";
+ };
+ lock-true = {
+ Value = true;
+ Status = "locked";
+ };
+in
+{
+ EnableTrackingProtection = {
+ Value = true;
+ Locked = true;
+ Cryptomining = true;
+ Fingerprinting = true;
+ EmailTracking = true;
+ };
+ UserMessaging = {
+ WhatsNew = false;
+ ExtensionRecommendations = false;
+ FeatureRecommendations = false;
+ UrlbarInterventions = false;
+ SkipOnboarding = true;
+ MoreFromMozilla = false;
+ Labs = false;
+ Locked = true;
+ };
+ DisableAppUpdate = true;
+ DisableAccounts = true;
+ DisableFirefoxAccounts = true;
+ DisableFirefoxStudies = true;
+ DisablePocket = true;
+ DisableTelemetry = true;
+ AutofillAddressEnabled = false;
+ AutofillCreditCardEnabled = false;
+ DisableMasterPasswordCreation = true;
+ PasswordManagerEnabled = false;
+ PrimaryPassword = false;
+ OfferToSaveLogins = false;
+ NoDefaultBookmarks = true;
+ OverrideFirstRunPage = "";
+ OverridePostUpdatePage = "";
+ FirefoxHome = {
+ Search = true;
+ TopSites = true;
+ SponsoredTopSites = false;
+ Highlights = false;
+ Pocket = false;
+ SponsoredPocket = false;
+ Snippets = false;
+ Locked = true;
+ };
+ SearchSuggestEnabled = true;
+ FirefoxSuggest = {
+ WebSuggestions = true;
+ SponsoredSuggestions = false;
+ ImproveSuggest = false;
+ Locked = true;
+ };
+ PictureInPicture = lock-true;
+ HardwareAcceleration = true;
+ Certificates = {
+ ImportEnterpriseRoots = true;
+ };
+ Preferences = {
+ "xpinstall.whitelist.required" = lock-true;
+ "dom.webgpu.enabled" = lock-true;
+ "media.eme.enabled" = lock-true;
+ "ui.preferReducedMotion" = lock-false;
+ "general.autoScroll" = lock-true;
+ "general.smoothScroll" = lock-true;
+ "browser.crashReports.unsubmittedCheck.autoSubmit2" = lock-false;
+ "browser.aboutConfig.showWarning" = lock-false;
+ "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled" = lock-true;
+ };
+}
+
diff --git a/users/ryan/zen/zenProfile.nix b/users/ryan/modules/zen/profile.nix
similarity index 100%
rename from users/ryan/zen/zenProfile.nix
rename to users/ryan/modules/zen/profile.nix
diff --git a/users/ryan/zen/zenPolicies.nix b/users/ryan/zen/zenPolicies.nix
deleted file mode 100644
index 8d39689..0000000
--- a/users/ryan/zen/zenPolicies.nix
+++ /dev/null
@@ -1,128 +0,0 @@
-let
- lock-false = {
- Value = false;
- Status = "locked";
- };
- lock-true = {
- Value = true;
- Status = "locked";
- };
-in
-{
- EnableTrackingProtection = {
- Value = true;
- Locked = true;
- Cryptomining = true;
- Fingerprinting = true;
- EmailTracking = true;
- };
- UserMessaging = {
- WhatsNew = false;
- ExtensionRecommendations = false;
- FeatureRecommendations = false;
- UrlbarInterventions = false;
- SkipOnboarding = true;
- MoreFromMozilla = false;
- Labs = false;
- Locked = true;
- };
- DisableAppUpdate = true;
- DisableAccounts = true;
- DisableFirefoxAccounts = true;
- DisableFirefoxStudies = true;
- DisablePocket = true;
- DisableTelemetry = true;
- AutofillAddressEnabled = false;
- AutofillCreditCardEnabled = false;
- DisableMasterPasswordCreation = true;
- PasswordManagerEnabled = false;
- PrimaryPassword = false;
- OfferToSaveLogins = false;
- NoDefaultBookmarks = true;
- OverrideFirstRunPage = "";
- OverridePostUpdatePage = "";
- FirefoxHome = {
- Search = true;
- TopSites = true;
- SponsoredTopSites = false;
- Highlights = false;
- Pocket = false;
- SponsoredPocket = false;
- Snippets = false;
- Locked = true;
- };
- SearchSuggestEnabled = true;
- FirefoxSuggest = {
- WebSuggestions = true;
- SponsoredSuggestions = false;
- ImproveSuggest = false;
- Locked = true;
- };
- PictureInPicture = lock-true;
- HardwareAcceleration = true;
- Certificates = {
- ImportEnterpriseRoots = true;
- };
- ExtensionSettings = {
- #"*".installation_mode = "blocked";
- # uBlock Origin
- "uBlock0@raymondhill.net" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi";
- installation_mode = "force_installed";
- private_browsing = true;
- };
- # Bitwarden
- "{446900e4-71c2-419f-a6a7-df9c091e268b}" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/bitwarden-password-manager/latest.xpi";
- installation_mode = "normal_installed";
- private_browsing = true;
- };
- # SponsorBlock
- "sponsorBlocker@ajay.app" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi";
- installation_mode = "force_installed";
- };
- # DeArrow
- "deArrow@ajay.app" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/dearrow/latest.xpi";
- installation_mode = "force_installed";
- };
- # Return Youtube Dislike
- "{762f9885-5a13-4abd-9c77-433dcd38b8fd}" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/return-youtube-dislikes/latest.xpi";
- installation_mode = "force_installed";
- };
- # Youtube Nonstop
- "{0d7cafdd-501c-49ca-8ebb-e3341caaa55e}" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/youtube-nonstop/latest.xpi";
- installation_mode = "force_installed";
- };
- # TamperMonkey
- "firefox@tampermonkey.net" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/tampermonkey/latest.xpi";
- installation_mode = "force_installed";
- };
- # Floccus
- "floccus@handmadeideas.org" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/floccus/latest.xpi";
- installation_mode = "force_installed";
- };
- # Mailvelope
- "jid1-AQqSMBYb0a8ADg@jetpack" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/mailvelope/latest.xpi";
- installation_mode = "force_installed";
- };
- };
- Preferences = {
- "xpinstall.whitelist.required" = lock-true;
- "dom.webgpu.enabled" = lock-true;
- "media.eme.enabled" = lock-true;
- "ui.preferReducedMotion" = lock-false;
- "general.autoScroll" = lock-true;
- "general.smoothScroll" = lock-true;
- "browser.crashReports.unsubmittedCheck.autoSubmit2" = lock-false;
- "browser.aboutConfig.showWarning" = lock-false;
- "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled" = lock-true;
- };
-}
-