├── .envrc ├── .github ├── FUNDING.yml └── workflows │ ├── backport.yml │ ├── ci.yml │ └── github_pages.yml ├── .gitignore ├── LICENSE ├── README.md ├── default.nix ├── docs ├── default.nix ├── manual │ ├── introduction.md │ ├── manpage-urls.json │ ├── manual.md │ ├── options.md │ └── preface.md ├── plasma-manager-options.nix └── static │ └── style.css ├── examples ├── .gitignore ├── home.nix ├── homeManager │ ├── README.md │ └── home.nix ├── homeManagerFlake │ └── flake.nix └── systemFlake │ └── flake.nix ├── flake.lock ├── flake.nix ├── lib ├── colorscheme.nix ├── panel.nix ├── qfont.nix ├── types.nix ├── wallpapers.nix └── writeconfig.nix ├── modules ├── apps │ ├── default.nix │ ├── elisa.nix │ ├── ghostwriter.nix │ ├── kate │ │ ├── check-theme-name-free.sh │ │ └── default.nix │ ├── konsole.nix │ └── okular.nix ├── default.nix ├── desktop.nix ├── files.nix ├── fonts.nix ├── hotkeys.nix ├── input.nix ├── krunner.nix ├── kscreenlocker.nix ├── kwin.nix ├── panels.nix ├── powerdevil.nix ├── session.nix ├── shortcuts.nix ├── spectacle.nix ├── startup.nix ├── widgets │ ├── app-menu.nix │ ├── application-title-bar.nix │ ├── battery.nix │ ├── default.nix │ ├── digital-clock.nix │ ├── icon-tasks.nix │ ├── keyboard-layout.nix │ ├── kicker.nix │ ├── kickerdash.nix │ ├── kickoff.nix │ ├── lib.nix │ ├── pager.nix │ ├── panel-spacer.nix │ ├── plasma-panel-colorizer.nix │ ├── plasmusic-toolbar.nix │ ├── system-monitor.nix │ └── system-tray.nix ├── window-rules.nix ├── windows.nix └── workspace.nix ├── script ├── rc2nix.py ├── rc2nix.rb └── write_config.py ├── test ├── basic.nix ├── demo.nix └── rc2nix │ ├── test_data │ ├── kcminputrc │ ├── kglobalshortcutsrc │ ├── kglobalshortcutsrc.bak │ ├── krunnerrc │ ├── kscreenlockerrc │ └── kwinrc │ └── test_rc2nix.py └── treefmt.toml /.envrc: -------------------------------------------------------------------------------- 1 | # -*- sh -*- 2 | 3 | use flake 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: HeitorAugustoLN 2 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | on: 3 | pull_request_target: 4 | types: [closed, labeled] 5 | 6 | # WARNING: 7 | # When extending this action, be aware that $GITHUB_TOKEN allows write access to 8 | # the GitHub repository. This means that it should not evaluate user input in a 9 | # way that allows code injection. 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | backport: 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | name: Backport pull request 20 | if: github.repository_owner == 'nix-community' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4.2.1 25 | with: 26 | ref: ${{ github.event.pull_request.head.sha }} 27 | - name: Create backport pull request 28 | uses: korthout/backport-action@v3.1.0 29 | with: 30 | pull_description: |- 31 | This is an automated backport of #${pull_number} to `${target_branch}`. 32 | 33 | Before merging, make sure this change is backwards compatible with the stable release branch. 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Nix Checks" 2 | on: 3 | pull_request: 4 | paths: 5 | - '**/*.nix' 6 | - 'flake.lock' 7 | - 'script/**' 8 | 9 | # cancel previous runs when pushing new changes 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | checks: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: cachix/install-nix-action@v22 20 | with: 21 | extra_nix_config: "system-features = kvm nixos-test" 22 | - run: nix flake check -L --keep-going 23 | - run: nix flake check -L --keep-going --override-input plasma-manager . ./examples/homeManagerFlake 24 | - run: nix flake check -L --keep-going --override-input plasma-manager . ./examples/systemFlake 25 | -------------------------------------------------------------------------------- /.github/workflows/github_pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages Docs Generation 2 | on: 3 | push: 4 | branches: 5 | - trunk 6 | paths: 7 | - 'flake.nix' 8 | - 'flake.lock' 9 | - 'modules/**' 10 | - 'docs/**' 11 | 12 | jobs: 13 | publish: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: cachix/install-nix-action@v27 21 | with: 22 | nix_path: nixpkgs=channel:nixos-unstable 23 | - uses: cachix/cachix-action@v15 24 | with: 25 | name: nix-community 26 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 27 | - run: | 28 | nix-build -A docs.html 29 | cp -r result/share/doc/plasma-manager public 30 | - name: Deploy 31 | uses: peaceiris/actions-gh-pages@v4 32 | with: 33 | github_token: ${{ secrets.GITHUB_TOKEN }} 34 | publish_dir: ./public 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | result-* 2 | result 3 | .direnv 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Plasma Manager contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manage KDE Plasma with Home Manager 2 | 3 | This project aims to provide [Home Manager][home-manager] modules which allow you 4 | to configure KDE Plasma using Nix. 5 | 6 | ## Table of contents 7 | - [Manage KDE Plasma with Home Manager](#manage-kde-plasma-with-home-manager) 8 | - [Table of contents](#table-of-contents) 9 | - [Supported versions](#supported-versions) 10 | - [What's supported](#whats-supported) 11 | - [What's not well supported (at the moment)](#whats-not-well-supported-at-the-moment) 12 | - [What will not be supported](#what-will-not-be-supported) 13 | - [Getting started](#getting-started) 14 | - [Make your configuration more declarative with overrideConfig](#make-your-configuration-more-declarative-with-overrideconfig) 15 | - [Capturing Your Current Configuration with rc2nix](#capturing-your-current-configuration-with-rc2nix) 16 | - [Contributions and Maintenance](#contributions-and-maintenance) 17 | - [Special Thanks](#special-thanks) 18 | 19 | ## Supported versions 20 | `plasma-manager` supports both plasma 5 and plasma 6. The `trunk` branch is the 21 | most up-to-date branch and is mainly focused on plasma 6, but may still work on 22 | plasma 5. If you are running plasma 5, it's recommended to use the `plasma-5` 23 | branch, which is designed to have better compatibility with plasma 5. To do this 24 | with flakes you can use "github:nix-community/plasma-manager/plasma-5" as your 25 | flake url, or if you are using nix-channels you can set the channel url to 26 | "https://github.com/nix-community/plasma-manager/archive/plasma-5.tar.gz". It's 27 | worth noting that the plasma 5 branch, due to the extra work required for 28 | maintaining, may lag behind a bit in features, but in general it should be less 29 | broken when used with plasma 5. If you want the best experience with 30 | `plasma-manager` it's recommended running plasma 6. 31 | 32 | ## What's supported 33 | At the moment `plasma-manager` supports configuring the following: 34 | - KDE configuration files (via the `files` module) 35 | - Global themes, colorschemes, icons, cursortheme, wallpaper (via the `workspace` module) 36 | - Desktop icons, widgets, and mouse actions (via the `desktop` module) 37 | - Configuration of spectacle shortcuts (via the `spectacle` module) 38 | - Shortcuts (via the `shortcuts` module) 39 | - Hotkeys (via the `hotkeys` module) 40 | - Panels and Extra Widgets (via the `panels` module) 41 | - Keyboards, Touchpads and Mice (via the `input` module) 42 | - KRunner (via the `krunner` module) 43 | - Screen locker (via the `kscreenlocker` module) 44 | - Fonts (via the `fonts` module) 45 | - Window Rules (via the `window-rules` module) 46 | - Session (via the `session` module) 47 | - KDE apps (via the `apps` module). In particular the following kde apps have 48 | modules in `plasma-manager`: 49 | - ghostwriter 50 | - kate 51 | - konsole 52 | - okular 53 | 54 | Additionally there are more functionality than just listed above, and more 55 | functionality to come in the future! 56 | 57 | ## What's not well supported (at the moment) 58 | There also are some things which at the moment isn't very well supported, in 59 | particular: 60 | - Real-time updates of configuration without having to log out and back in 61 | - Usage of high-level modules in the configuration generated by `rc2nix` 62 | - Keybindings to some key combinations (`Ctrl+Alt+T` and `Print` for example, see https://github.com/nix-community/plasma-manager/issues/109 and https://github.com/nix-community/plasma-manager/issues/136) 63 | 64 | There may also be more things we aren't aware of. If you find some other 65 | limitations don't hesitate to open an issue or submit a pr. 66 | 67 | ## What will not be supported 68 | There are some things which are out of bounds for this project due to technical 69 | reasons. For example 70 | - SDDM configuration (requires root-privileges and thus not suited for a `home-manager` module) 71 | 72 | ## Getting started 73 | We provide some examples to help you get started. These are located in the 74 | [examples](./examples/) directory. Here you in particular can find: 75 | - [An example home-manager configuration](./examples/homeManager/home.nix) [with instructions](./examples/homeManager/README.md) 76 | - [An example flake.nix for usage with home-manager only](./examples/homeManagerFlake//flake.nix) 77 | - [An example flake.nix for usage with the system configuration](./examples/systemFlake/flake.nix) 78 | - [An example home.nix showing some of the capabilities of plasma-manager](./examples/home.nix) 79 | 80 | With more to come! These should give you some idea how to get started with 81 | `plasma-manager`. 82 | 83 | Additionally, 84 | [the manual section containing all the supported plasma-manager options](https://nix-community.github.io/plasma-manager/options.xhtml) 85 | may come in handy. 86 | 87 | ## Make your configuration more declarative with overrideConfig 88 | By default `plasma-manager` will simply write the specified configurations to 89 | various config-files and leave all other options alone. This way settings not 90 | specified in `plasma-manager` will be left alone, meaning that configurations 91 | made outside `plasma-manager` will still be set. This can lead to different 92 | settings on different machines even with the same `plasma-manager` 93 | configuration. If you like a declarative approach better consider enabling 94 | `overrideConfig`. This makes it so all options not set by `plasma-manager` will 95 | be set to the default on login. In practice this then becomes a declarative 96 | setup, much like what you would expect from most `home-manager` options/modules. 97 | 98 | One thing to keep in mind is that enabling this option will delete all the KDE 99 | config-files on `home-manager` activation, and replace them with config-files 100 | generated by `plasma-manager`. Therefore make sure you backup your KDE 101 | config-files before enabling this option if you don't want to lose them. 102 | 103 | ## Capturing Your Current Configuration with rc2nix 104 | 105 | To make it easier to migrate to `plasma-manager`, and to help maintain your Nix 106 | configuration when not using `overrideConfig`, this project includes a tool 107 | called `rc2nix`. 108 | 109 | This tool will read KDE configuration files and translate them to Nix. The 110 | translated configuration is written to standard output. This makes it easy to: 111 | 112 | - Generate an initial Plasma Manager configuration file. 113 | - See what settings are changed by a GUI tool by capturing a file 114 | before and after using the tool and then using `diff`. 115 | 116 | Keep in mind that the `rc2nix` module isn't perfect and often will give somewhat 117 | suboptimal configurations (it will in some cases prefer using the `files` module 118 | when better configurations can be achieved using higher-level modules). However, 119 | it is still a useful tool to quickly get your configuration up and running or 120 | converting config-files generated by the gui settings app to nix expressions. 121 | 122 | To run the `rc2nix` tool without having to clone this repository run 123 | the following shell command: 124 | 125 | ```sh 126 | nix run github:nix-community/plasma-manager 127 | ``` 128 | 129 | ## Contributions and Maintenance 130 | 131 | This is a community project and we welcome all contributions. KDE plasma and its 132 | apps consists of a lot of configuration options, and if you find that some 133 | options are missing and have the skills to implement this, PRs are very welcome. 134 | Issues are also welcome for everything from feature-requests to bug-reports or 135 | just general suggestions for improving the project. 136 | 137 | ## Special Thanks 138 | 139 | `plasma-manager` started off it's development under 140 | [pjones](https://github.com/pjones), whose contributions have laid the 141 | foundation of the project to this day. The project was otherwise inspired by the 142 | suggestions on [Home Manager Issue 143 | #607][hm607] by people such as [bew](https://github.com/bew) and 144 | [kurnevsky](https://github.com/kurnevsky). Thank you. 145 | 146 | [home-manager]: https://github.com/nix-community/home-manager 147 | [hm607]: https://github.com/nix-community/home-manager/issues/607 148 | [nix-community]: https://github.com/nix-community 149 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import { }, 3 | }: 4 | { 5 | docs = import ./docs { 6 | inherit pkgs; 7 | lib = pkgs.lib; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /docs/default.nix: -------------------------------------------------------------------------------- 1 | { pkgs, lib, ... }: 2 | let 3 | dontCheckModules = { 4 | _module.check = false; 5 | }; 6 | modules = [ 7 | ../modules 8 | dontCheckModules 9 | ]; 10 | 11 | githubDeclaration = user: repo: branch: subpath: { 12 | url = "https://github.com/${user}/${repo}/blob/${branch}/${subpath}"; 13 | name = "<${repo}/${subpath}>"; 14 | }; 15 | 16 | pmPath = toString ./..; 17 | transformOptions = 18 | opt: 19 | opt 20 | // { 21 | declarations = ( 22 | map ( 23 | decl: 24 | if (lib.hasPrefix pmPath (toString decl)) then 25 | (githubDeclaration "nix-community" "plasma-manager" "trunk" ( 26 | lib.removePrefix "/" (lib.removePrefix pmPath (toString decl)) 27 | )) 28 | else 29 | decl 30 | ) opt.declarations 31 | ); 32 | }; 33 | 34 | buildOptionsDocs = ( 35 | args@{ modules, ... }: 36 | let 37 | opts = 38 | (lib.evalModules { 39 | inherit modules; 40 | class = "homeManager"; 41 | }).options; 42 | options = builtins.removeAttrs opts [ "_module" ]; 43 | in 44 | pkgs.buildPackages.nixosOptionsDoc { 45 | inherit options; 46 | inherit transformOptions; 47 | warningsAreErrors = false; 48 | } 49 | ); 50 | 51 | pmOptionsDoc = buildOptionsDocs { inherit modules; }; 52 | plasma-manager-options = pkgs.callPackage ./plasma-manager-options.nix { 53 | nixos-render-docs = pkgs.nixos-render-docs; 54 | plasma-manager-options = pmOptionsDoc.optionsJSON; 55 | revision = "latest"; 56 | }; 57 | in 58 | { 59 | html = plasma-manager-options; 60 | json = pmOptionsDoc.optionsJSON; 61 | } 62 | -------------------------------------------------------------------------------- /docs/manual/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction {#intro} 2 | To get started, check out the [**getting started** section in the README](https://github.com/nix-community/plasma-manager#getting-started). 3 | There are a couple of examples on how you can get started using `plasma-manager`, 4 | using either a flake or traditional Nix channels. 5 | 6 | More details may be available here in the future! 7 | -------------------------------------------------------------------------------- /docs/manual/manpage-urls.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /docs/manual/manual.md: -------------------------------------------------------------------------------- 1 | # Plasma-Manager manual {#plasma-manager-manual} 2 | 3 | ## Version: @VERSION@ 4 | 5 | ```{=include=} preface html:into-file=//preface.xhtml 6 | preface.md 7 | ``` 8 | 9 | ```{=include=} parts html:into-file=//introduction.xhtml 10 | introduction.md 11 | ``` 12 | 13 | ```{=include=} appendix html:into-file=//options.xhtml 14 | options.md 15 | ``` -------------------------------------------------------------------------------- /docs/manual/options.md: -------------------------------------------------------------------------------- 1 | # Plasma-Manager Options {#ch-options} 2 | 3 | ```{=include=} options 4 | id-prefix: opt- 5 | list-id: plasma-manager-options 6 | source: @OPTIONS_JSON@ 7 | ``` -------------------------------------------------------------------------------- /docs/manual/preface.md: -------------------------------------------------------------------------------- 1 | # Preface {#preface} 2 | Plasma Manager is a [Home Manager](https://github.com/nix-community/home-manager) 3 | module capable of configuring as much of KDE Plasma as 4 | possible, using [Nix](https://nixos.org). 5 | 6 | The project has progressed a lot lately, to the extent where 7 | [most of the configuration options present in KDE Plasma 6 are configurable through `plasma-manager`](https://github.com/nix-community/plasma-manager#whats-supported). 8 | 9 | The main focus of the project has been on KDE Plasma 6 for a little while now, 10 | but it's also possible to use it to some extent on Plasma 5 as well. 11 | -------------------------------------------------------------------------------- /docs/plasma-manager-options.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenv, 3 | nixos-render-docs, 4 | plasma-manager-options, 5 | revision, 6 | lib, 7 | documentation-highlighter, 8 | }: 9 | let 10 | outputPath = "share/doc/plasma-manager"; 11 | in 12 | stdenv.mkDerivation { 13 | name = "plasma-manager-options"; 14 | 15 | nativeBuildInputs = [ nixos-render-docs ]; 16 | 17 | src = ./manual; 18 | 19 | buildPhase = '' 20 | mkdir -p out/highlightjs 21 | 22 | cp -t out/highlightjs \ 23 | ${documentation-highlighter}/highlight.pack.js \ 24 | ${documentation-highlighter}/LICENSE \ 25 | ${documentation-highlighter}/mono-blue.css \ 26 | ${documentation-highlighter}/loader.js 27 | 28 | cp ${./static/style.css} out/style.css 29 | 30 | substituteInPlace options.md \ 31 | --replace-fail \ 32 | '@OPTIONS_JSON@' \ 33 | ${plasma-manager-options}/share/doc/nixos/options.json 34 | 35 | substituteInPlace manual.md \ 36 | --replace-fail \ 37 | '@VERSION@' \ 38 | ${revision} 39 | 40 | nixos-render-docs manual html \ 41 | --manpage-urls ./manpage-urls.json \ 42 | --revision ${lib.trivial.revisionWithDefault revision} \ 43 | --style style.css \ 44 | --script highlightjs/highlight.pack.js \ 45 | --script highlightjs/loader.js \ 46 | --toc-depth 1 \ 47 | --section-toc-depth 1 \ 48 | manual.md \ 49 | out/index.xhtml 50 | ''; 51 | 52 | installPhase = '' 53 | dest="$out/${outputPath}" 54 | mkdir -p "$(dirname "$dest")" 55 | mv out "$dest" 56 | ''; 57 | } 58 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | */flake.lock 2 | -------------------------------------------------------------------------------- /examples/home.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | { 3 | home.stateVersion = "23.11"; 4 | 5 | programs.plasma = { 6 | enable = true; 7 | 8 | # 9 | # Some high-level settings: 10 | # 11 | workspace = { 12 | clickItemTo = "open"; # If you liked the click-to-open default from plasma 5 13 | lookAndFeel = "org.kde.breezedark.desktop"; 14 | cursor = { 15 | theme = "Bibata-Modern-Ice"; 16 | size = 32; 17 | }; 18 | iconTheme = "Papirus-Dark"; 19 | wallpaper = "${pkgs.kdePackages.plasma-workspace-wallpapers}/share/wallpapers/Patak/contents/images/1080x1920.png"; 20 | }; 21 | 22 | hotkeys.commands."launch-konsole" = { 23 | name = "Launch Konsole"; 24 | key = "Meta+Alt+K"; 25 | command = "konsole"; 26 | }; 27 | 28 | fonts = { 29 | general = { 30 | family = "JetBrains Mono"; 31 | pointSize = 12; 32 | }; 33 | }; 34 | 35 | desktop.widgets = [ 36 | { 37 | plasmusicToolbar = { 38 | position = { 39 | horizontal = 51; 40 | vertical = 100; 41 | }; 42 | size = { 43 | width = 250; 44 | height = 250; 45 | }; 46 | }; 47 | } 48 | ]; 49 | 50 | panels = [ 51 | # Windows-like panel at the bottom 52 | { 53 | location = "bottom"; 54 | widgets = [ 55 | # We can configure the widgets by adding the name and config 56 | # attributes. For example to add the the kickoff widget and set the 57 | # icon to "nix-snowflake-white" use the below configuration. This will 58 | # add the "icon" key to the "General" group for the widget in 59 | # ~/.config/plasma-org.kde.plasma.desktop-appletsrc. 60 | { 61 | name = "org.kde.plasma.kickoff"; 62 | config = { 63 | General = { 64 | icon = "nix-snowflake-white"; 65 | alphaSort = true; 66 | }; 67 | }; 68 | } 69 | # Or you can configure the widgets by adding the widget-specific options for it. 70 | # See modules/widgets for supported widgets and options for these widgets. 71 | # For example: 72 | { 73 | kickoff = { 74 | sortAlphabetically = true; 75 | icon = "nix-snowflake-white"; 76 | }; 77 | } 78 | # Adding configuration to the widgets can also for example be used to 79 | # pin apps to the task-manager, which this example illustrates by 80 | # pinning dolphin and konsole to the task-manager by default with widget-specific options. 81 | { 82 | iconTasks = { 83 | launchers = [ 84 | "applications:org.kde.dolphin.desktop" 85 | "applications:org.kde.konsole.desktop" 86 | ]; 87 | }; 88 | } 89 | # Or you can do it manually, for example: 90 | { 91 | name = "org.kde.plasma.icontasks"; 92 | config = { 93 | General = { 94 | launchers = [ 95 | "applications:org.kde.dolphin.desktop" 96 | "applications:org.kde.konsole.desktop" 97 | ]; 98 | }; 99 | }; 100 | } 101 | # If no configuration is needed, specifying only the name of the 102 | # widget will add them with the default configuration. 103 | "org.kde.plasma.marginsseparator" 104 | # If you need configuration for your widget, instead of specifying the 105 | # the keys and values directly using the config attribute as shown 106 | # above, plasma-manager also provides some higher-level interfaces for 107 | # configuring the widgets. See modules/widgets for supported widgets 108 | # and options for these widgets. The widgets below shows two examples 109 | # of usage, one where we add a digital clock, setting 12h time and 110 | # first day of the week to Sunday and another adding a systray with 111 | # some modifications in which entries to show. 112 | { 113 | digitalClock = { 114 | calendar.firstDayOfWeek = "sunday"; 115 | time.format = "12h"; 116 | }; 117 | } 118 | { 119 | systemTray.items = { 120 | # We explicitly show bluetooth and battery 121 | shown = [ 122 | "org.kde.plasma.battery" 123 | "org.kde.plasma.bluetooth" 124 | ]; 125 | # And explicitly hide networkmanagement and volume 126 | hidden = [ 127 | "org.kde.plasma.networkmanagement" 128 | "org.kde.plasma.volume" 129 | ]; 130 | }; 131 | } 132 | ]; 133 | hiding = "autohide"; 134 | } 135 | # Application name, Global menu and Song information and playback controls at the top 136 | { 137 | location = "top"; 138 | height = 26; 139 | widgets = [ 140 | { 141 | applicationTitleBar = { 142 | behavior = { 143 | activeTaskSource = "activeTask"; 144 | }; 145 | layout = { 146 | elements = [ "windowTitle" ]; 147 | horizontalAlignment = "left"; 148 | showDisabledElements = "deactivated"; 149 | verticalAlignment = "center"; 150 | }; 151 | overrideForMaximized.enable = false; 152 | titleReplacements = [ 153 | { 154 | type = "regexp"; 155 | originalTitle = "^Brave Web Browser$"; 156 | newTitle = "Brave"; 157 | } 158 | { 159 | type = "regexp"; 160 | originalTitle = ''\\bDolphin\\b''; 161 | newTitle = "File manager"; 162 | } 163 | ]; 164 | windowTitle = { 165 | font = { 166 | bold = false; 167 | fit = "fixedSize"; 168 | size = 12; 169 | }; 170 | hideEmptyTitle = true; 171 | margins = { 172 | bottom = 0; 173 | left = 10; 174 | right = 5; 175 | top = 0; 176 | }; 177 | source = "appName"; 178 | }; 179 | }; 180 | } 181 | "org.kde.plasma.appmenu" 182 | "org.kde.plasma.panelspacer" 183 | { 184 | plasmusicToolbar = { 185 | panelIcon = { 186 | albumCover = { 187 | useAsIcon = false; 188 | radius = 8; 189 | }; 190 | icon = "view-media-track"; 191 | }; 192 | playbackSource = "auto"; 193 | musicControls.showPlaybackControls = true; 194 | songText = { 195 | displayInSeparateLines = true; 196 | maximumWidth = 640; 197 | scrolling = { 198 | behavior = "alwaysScroll"; 199 | speed = 3; 200 | }; 201 | }; 202 | }; 203 | } 204 | ]; 205 | } 206 | ]; 207 | 208 | window-rules = [ 209 | { 210 | description = "Dolphin"; 211 | match = { 212 | window-class = { 213 | value = "dolphin"; 214 | type = "substring"; 215 | }; 216 | window-types = [ "normal" ]; 217 | }; 218 | apply = { 219 | noborder = { 220 | value = true; 221 | apply = "force"; 222 | }; 223 | # `apply` defaults to "apply-initially" 224 | maximizehoriz = true; 225 | maximizevert = true; 226 | }; 227 | } 228 | ]; 229 | 230 | powerdevil = { 231 | AC = { 232 | powerButtonAction = "lockScreen"; 233 | autoSuspend = { 234 | action = "shutDown"; 235 | idleTimeout = 1000; 236 | }; 237 | turnOffDisplay = { 238 | idleTimeout = 1000; 239 | idleTimeoutWhenLocked = "immediately"; 240 | }; 241 | }; 242 | battery = { 243 | powerButtonAction = "sleep"; 244 | whenSleepingEnter = "standbyThenHibernate"; 245 | }; 246 | lowBattery = { 247 | whenLaptopLidClosed = "hibernate"; 248 | }; 249 | }; 250 | 251 | kwin = { 252 | edgeBarrier = 0; # Disables the edge-barriers introduced in plasma 6.1 253 | cornerBarrier = false; 254 | 255 | scripts.polonium.enable = true; 256 | }; 257 | 258 | kscreenlocker = { 259 | lockOnResume = true; 260 | timeout = 10; 261 | }; 262 | 263 | # 264 | # Some mid-level settings: 265 | # 266 | shortcuts = { 267 | ksmserver = { 268 | "Lock Session" = [ 269 | "Screensaver" 270 | "Meta+Ctrl+Alt+L" 271 | ]; 272 | }; 273 | 274 | kwin = { 275 | "Expose" = "Meta+,"; 276 | "Switch Window Down" = "Meta+J"; 277 | "Switch Window Left" = "Meta+H"; 278 | "Switch Window Right" = "Meta+L"; 279 | "Switch Window Up" = "Meta+K"; 280 | }; 281 | }; 282 | 283 | # 284 | # Some low-level settings: 285 | # 286 | configFile = { 287 | baloofilerc."Basic Settings"."Indexing-Enabled" = false; 288 | kwinrc."org.kde.kdecoration2".ButtonsOnLeft = "SF"; 289 | kwinrc.Desktops.Number = { 290 | value = 8; 291 | # Forces kde to not change this value (even through the settings app). 292 | immutable = true; 293 | }; 294 | kscreenlockerrc = { 295 | Greeter.WallpaperPlugin = "org.kde.potd"; 296 | # To use nested groups use / as a separator. In the below example, 297 | # Provider will be added to [Greeter][Wallpaper][org.kde.potd][General]. 298 | "Greeter/Wallpaper/org.kde.potd/General".Provider = "bing"; 299 | }; 300 | }; 301 | }; 302 | } 303 | -------------------------------------------------------------------------------- /examples/homeManager/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | Add this repository as a channel: 4 | 5 | ```nix 6 | $ nix-channel --add https://github.com/nix-community/plasma-manager/archive/trunk.tar.gz plasma-manager 7 | ``` 8 | 9 | Update / unpack the channel: 10 | 11 | ```nix 12 | $ nix-channel --update plasma-manager 13 | ``` 14 | 15 | Add to your configuration file, for example `~/.config/home-manager/plasma.nix`: 16 | 17 | ```nix 18 | { pkgs, ...}: 19 | { 20 | imports = [ 21 | 22 | ]; 23 | 24 | programs = { 25 | plasma = { 26 | enable = true; 27 | # etc. 28 | }; 29 | }; 30 | } 31 | ``` 32 | 33 | -------------------------------------------------------------------------------- /examples/homeManager/home.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | 3 | { 4 | imports = [ ]; 5 | 6 | programs.plasma = { 7 | enable = true; 8 | 9 | # 10 | # Some high-level settings: 11 | # 12 | workspace = { 13 | clickItemTo = "select"; 14 | lookAndFeel = "org.kde.breezedark.desktop"; 15 | cursor.theme = "Bibata-Modern-Ice"; 16 | iconTheme = "Papirus-Dark"; 17 | wallpaper = "${pkgs.kdePackages.plasma-workspace-wallpapers}/share/wallpapers/Patak/contents/images/1080x1920.png"; 18 | }; 19 | 20 | hotkeys.commands."launch-konsole" = { 21 | name = "Launch Konsole"; 22 | key = "Meta+Alt+K"; 23 | command = "konsole"; 24 | }; 25 | 26 | panels = [ 27 | # Windows-like panel at the bottom 28 | { 29 | location = "bottom"; 30 | widgets = [ 31 | "org.kde.plasma.kickoff" 32 | "org.kde.plasma.icontasks" 33 | "org.kde.plasma.marginsseparator" 34 | "org.kde.plasma.systemtray" 35 | "org.kde.plasma.digitalclock" 36 | ]; 37 | } 38 | # Global menu at the top 39 | { 40 | location = "top"; 41 | height = 26; 42 | widgets = [ "org.kde.plasma.appmenu" ]; 43 | } 44 | ]; 45 | 46 | # 47 | # Some mid-level settings: 48 | # 49 | shortcuts = { 50 | ksmserver = { 51 | "Lock Session" = [ 52 | "Screensaver" 53 | "Meta+Ctrl+Alt+L" 54 | ]; 55 | }; 56 | 57 | kwin = { 58 | "Expose" = "Meta+,"; 59 | "Switch Window Down" = "Meta+J"; 60 | "Switch Window Left" = "Meta+H"; 61 | "Switch Window Right" = "Meta+L"; 62 | "Switch Window Up" = "Meta+K"; 63 | }; 64 | }; 65 | 66 | # 67 | # Some low-level settings: 68 | # 69 | configFile = { 70 | "baloofilerc"."Basic Settings"."Indexing-Enabled" = false; 71 | "kwinrc"."org.kde.kdecoration2"."ButtonsOnLeft" = "SF"; 72 | "kwinrc"."Desktops"."Number" = { 73 | value = 8; 74 | # Forces kde to not change this value (even through the settings app). 75 | immutable = true; 76 | }; 77 | }; 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /examples/homeManagerFlake/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Plasma Manager Example with standalone home-manager flake"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | home-manager = { 7 | url = "github:nix-community/home-manager"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | plasma-manager = { 11 | url = "github:nix-community/plasma-manager"; 12 | inputs.nixpkgs.follows = "nixpkgs"; 13 | inputs.home-manager.follows = "home-manager"; 14 | }; 15 | }; 16 | 17 | outputs = 18 | inputs@{ 19 | nixpkgs, 20 | home-manager, 21 | plasma-manager, 22 | ... 23 | }: 24 | let 25 | # Replace with your username 26 | username = "jdoe"; 27 | 28 | # Replace with the fitting architecture 29 | system = "x86_64-linux"; 30 | in 31 | { 32 | # Replace `standAloneConfig` with the name of your configuration (your `username` or `"username@hostname"`) 33 | homeConfigurations.standAloneConfig = home-manager.lib.homeManagerConfiguration { 34 | pkgs = import nixpkgs { inherit system; }; 35 | 36 | modules = [ 37 | inputs.plasma-manager.homeManagerModules.plasma-manager 38 | 39 | # Specify the path to your home configuration here: 40 | ../home.nix 41 | 42 | { 43 | home = { 44 | inherit username; 45 | homeDirectory = "/home/${username}"; 46 | }; 47 | } 48 | ]; 49 | }; 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /examples/systemFlake/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Plasma Manager Example with system configuration flake"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | home-manager = { 7 | url = "github:nix-community/home-manager"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | plasma-manager = { 11 | url = "github:nix-community/plasma-manager"; 12 | inputs.nixpkgs.follows = "nixpkgs"; 13 | inputs.home-manager.follows = "home-manager"; 14 | }; 15 | }; 16 | 17 | outputs = 18 | inputs@{ 19 | nixpkgs, 20 | home-manager, 21 | plasma-manager, 22 | ... 23 | }: 24 | let 25 | # Replace with your username 26 | username = "jdoe"; 27 | 28 | # Replace with the fitting architecture 29 | system = "x86_64-linux"; 30 | in 31 | { 32 | # Replace `moduleConfig` with the name of you configuration 33 | nixosConfigurations.moduleConfig = nixpkgs.lib.nixosSystem { 34 | inherit system; 35 | 36 | modules = [ 37 | # We include the system-configuration here as well. Replace this with 38 | # your own configuration or import your configuration.nix. The demo 39 | # here is just the bare minimum to get the flake to not fail. 40 | { 41 | system.stateVersion = "23.11"; 42 | users.users."${username}".isNormalUser = true; 43 | fileSystems."/".device = "/dev/sda"; 44 | boot.loader.grub.devices = [ "/dev/sda" ]; 45 | } 46 | 47 | home-manager.nixosModules.home-manager 48 | { 49 | home-manager.useGlobalPkgs = true; 50 | home-manager.useUserPackages = true; 51 | home-manager.sharedModules = [ plasma-manager.homeManagerModules.plasma-manager ]; 52 | 53 | # This should point to your home.nix path of course. For an example 54 | # of this see ./home.nix in this directory. 55 | home-manager.users."${username}" = import ../home.nix; 56 | } 57 | ]; 58 | }; 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "home-manager": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1740494361, 11 | "narHash": "sha256-Dd/GhJ9qKmUwuhgt/PAROG8J6YdU2ZjtJI9SQX5sVQI=", 12 | "owner": "nix-community", 13 | "repo": "home-manager", 14 | "rev": "74f0a8546e3f2458c870cf90fc4b38ac1f498b17", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "nix-community", 19 | "repo": "home-manager", 20 | "type": "github" 21 | } 22 | }, 23 | "nixpkgs": { 24 | "locked": { 25 | "lastModified": 1740367490, 26 | "narHash": "sha256-WGaHVAjcrv+Cun7zPlI41SerRtfknGQap281+AakSAw=", 27 | "owner": "NixOS", 28 | "repo": "nixpkgs", 29 | "rev": "0196c0175e9191c474c26ab5548db27ef5d34b05", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "NixOS", 34 | "ref": "nixos-unstable", 35 | "repo": "nixpkgs", 36 | "type": "github" 37 | } 38 | }, 39 | "root": { 40 | "inputs": { 41 | "home-manager": "home-manager", 42 | "nixpkgs": "nixpkgs" 43 | } 44 | } 45 | }, 46 | "root": "root", 47 | "version": 7 48 | } 49 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Manage KDE Plasma with Home Manager"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | 7 | home-manager.url = "github:nix-community/home-manager"; 8 | home-manager.inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | 11 | outputs = 12 | inputs@{ self, ... }: 13 | let 14 | # Systems that can run tests: 15 | supportedSystems = [ 16 | "aarch64-linux" 17 | "i686-linux" 18 | "x86_64-linux" 19 | ]; 20 | 21 | # Function to generate a set based on supported systems: 22 | forAllSystems = inputs.nixpkgs.lib.genAttrs supportedSystems; 23 | 24 | # Attribute set of nixpkgs for each system: 25 | nixpkgsFor = forAllSystems (system: import inputs.nixpkgs { inherit system; }); 26 | in 27 | { 28 | homeManagerModules.plasma-manager = 29 | { ... }: 30 | { 31 | imports = [ ./modules ]; 32 | }; 33 | 34 | packages = forAllSystems ( 35 | system: 36 | let 37 | pkgs = nixpkgsFor.${system}; 38 | docs = import ./docs { 39 | inherit pkgs; 40 | lib = pkgs.lib; 41 | }; 42 | in 43 | { 44 | default = self.packages.${system}.rc2nix; 45 | 46 | demo = 47 | (inputs.nixpkgs.lib.nixosSystem { 48 | inherit system; 49 | modules = [ 50 | (import test/demo.nix { 51 | home-manager-module = inputs.home-manager.nixosModules.home-manager; 52 | plasma-module = self.homeManagerModules.plasma-manager; 53 | }) 54 | (_: { environment.systemPackages = [ self.packages.${system}.rc2nix ]; }) 55 | ]; 56 | }).config.system.build.vm; 57 | 58 | docs-html = docs.html; 59 | docs-json = docs.json; 60 | 61 | rc2nix = pkgs.writeShellApplication { 62 | name = "rc2nix"; 63 | runtimeInputs = with pkgs; [ python3 ]; 64 | text = ''python3 ${script/rc2nix.py} "$@"''; 65 | }; 66 | } 67 | ); 68 | 69 | apps = forAllSystems (system: { 70 | default = self.apps.${system}.rc2nix; 71 | 72 | demo = { 73 | type = "app"; 74 | program = "${self.packages.${system}.demo}/bin/run-plasma-demo-vm"; 75 | }; 76 | 77 | rc2nix = { 78 | type = "app"; 79 | program = "${self.packages.${system}.rc2nix}/bin/rc2nix"; 80 | }; 81 | }); 82 | 83 | checks = forAllSystems (system: { 84 | default = nixpkgsFor.${system}.callPackage ./test/basic.nix { 85 | home-manager-module = inputs.home-manager.nixosModules.home-manager; 86 | plasma-module = self.homeManagerModules.plasma-manager; 87 | }; 88 | }); 89 | 90 | formatter = forAllSystems (system: nixpkgsFor.${system}.treefmt); 91 | 92 | devShells = forAllSystems (system: { 93 | default = nixpkgsFor.${system}.mkShell { 94 | buildInputs = with nixpkgsFor.${system}; [ 95 | nixfmt-rfc-style 96 | ruby 97 | ruby.devdoc 98 | (python3.withPackages (pyPkgs: [ 99 | pyPkgs.python-lsp-server 100 | pyPkgs.black 101 | pyPkgs.isort 102 | ])) 103 | ]; 104 | }; 105 | }); 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /lib/colorscheme.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | colorEffectsKeys = [ 4 | "ChangeSelectionColor" 5 | "Color" 6 | "ColorAmount" 7 | "ColorEffect" 8 | "ContrastAmount" 9 | "ContrastEffect" 10 | "Enable" 11 | "IntensityAmount" 12 | "IntensityEffect" 13 | ]; 14 | colorUIKeys = [ 15 | "BackgroundAlternate" 16 | "BackgroundNormal" 17 | "DecorationFocus" 18 | "DecorationHover" 19 | "ForegroundActive" 20 | "ForegroundInactive" 21 | "ForegroundLink" 22 | "ForegroundNegative" 23 | "ForegroundNeutral" 24 | "ForegroundNormal" 25 | "ForegroundVisited" 26 | "regroundPositive" 27 | ]; 28 | ignoreKeys = { 29 | "ColorEffects:Disabled" = colorEffectsKeys; 30 | "ColorEffects:Inactive" = colorEffectsKeys; 31 | "Colors:Button" = colorUIKeys; 32 | "Colors:Selection" = colorUIKeys; 33 | "Colors:Tooltip" = colorUIKeys; 34 | "Colors:View" = colorUIKeys; 35 | "Colors:Window" = colorUIKeys; 36 | }; 37 | in 38 | (lib.mkMerge ( 39 | lib.mapAttrsToList (group: keys: { 40 | "kdeglobals"."${group}" = ( 41 | lib.mkMerge (map (key: { "${key}"."persistent" = (lib.mkDefault true); }) keys) 42 | ); 43 | }) ignoreKeys 44 | )) 45 | -------------------------------------------------------------------------------- /lib/panel.nix: -------------------------------------------------------------------------------- 1 | { lib, config, ... }: 2 | let 3 | widgets = (import ../modules/widgets { inherit lib; }); 4 | panelToLayout = 5 | panel: 6 | let 7 | inherit (widgets.lib) addWidgetStmts stringIfNotNull; 8 | inherit (lib) boolToString; 9 | inherit (builtins) toString; 10 | 11 | plasma6OnlyCmd = cmd: '' 12 | if (isPlasma6) { 13 | ${cmd} 14 | } 15 | ''; 16 | in 17 | '' 18 | ${ 19 | if (panel.screen == "all") then 20 | "for (screenID = 0; screenID < screenCount; screenID++)" 21 | else if (builtins.isList panel.screen) then 22 | "for (var screenID in [${builtins.concatStringsSep "," (map builtins.toString panel.screen)}])" 23 | else 24 | "" 25 | } 26 | { 27 | const panel = new Panel(); 28 | panel.height = ${toString panel.height}; 29 | panel.floating = ${boolToString panel.floating}; 30 | ${stringIfNotNull panel.alignment ''panel.alignment = "${panel.alignment}";''} 31 | ${stringIfNotNull panel.hiding ''panel.hiding = "${panel.hiding}";''} 32 | ${stringIfNotNull panel.location ''panel.location = "${panel.location}";''} 33 | ${stringIfNotNull panel.lengthMode (plasma6OnlyCmd ''panel.lengthMode = "${panel.lengthMode}";'')} 34 | ${stringIfNotNull panel.maxLength "panel.maximumLength = ${toString panel.maxLength};"} 35 | ${stringIfNotNull panel.minLength "panel.minimumLength = ${toString panel.minLength};"} 36 | ${stringIfNotNull panel.offset "panel.offset = ${toString panel.offset};"} 37 | ${stringIfNotNull panel.opacity ''panel.opacity = "${panel.opacity}";''} 38 | ${stringIfNotNull panel.screen ''panel.writeConfig("lastScreen[$i]", ${if ((panel.screen == "all") || (builtins.isList panel.screen)) then "screenID" else toString panel.screen});''} 39 | 40 | ${addWidgetStmts "panel" "panelWidgets" panel.widgets} 41 | ${stringIfNotNull panel.extraSettings panel.extraSettings} 42 | } 43 | ''; 44 | in 45 | '' 46 | // Removes all existing panels 47 | panels().forEach((panel) => panel.remove()); 48 | 49 | const isPlasma6 = applicationVersion.split(".")[0] == 6; 50 | 51 | // Adds the panels 52 | ${lib.concatMapStringsSep "\n" panelToLayout config.programs.plasma.panels} 53 | '' 54 | -------------------------------------------------------------------------------- /lib/qfont.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | #=== ENUMS === 4 | enums = { 5 | # QFont::StyleHint 6 | styleHint = rec { 7 | anyStyle = 5; 8 | sansSerif = helvetica; 9 | helvetica = 0; 10 | serif = times; 11 | times = 1; 12 | typewriter = courier; 13 | courier = 2; 14 | oldEnglish = 3; 15 | decorative = oldEnglish; 16 | monospace = 7; 17 | fantasy = 8; 18 | cursive = 6; 19 | system = 4; 20 | }; 21 | 22 | # QFont::Weight 23 | weight = { 24 | thin = 100; 25 | extraLight = 200; 26 | light = 300; 27 | normal = 400; 28 | medium = 500; 29 | demiBold = 600; 30 | bold = 700; 31 | extraBold = 800; 32 | black = 900; 33 | }; 34 | 35 | # QFont::Style 36 | style = { 37 | normal = 0; 38 | italic = 1; 39 | oblique = 2; 40 | }; 41 | 42 | # QFont::Capitalization 43 | capitalization = { 44 | mixedCase = 0; 45 | allUppercase = 1; 46 | allLowercase = 2; 47 | smallCaps = 3; 48 | capitalize = 4; 49 | }; 50 | 51 | # QFont::SpacingType 52 | spacingType = { 53 | percentage = 0; 54 | absolute = 1; 55 | }; 56 | 57 | # QFont::Stretch 58 | stretch = { 59 | anyStretch = 0; 60 | ultraCondensed = 50; 61 | extraCondensed = 62; 62 | condensed = 75; 63 | semiCondensed = 87; 64 | unstretched = 100; 65 | semiExpanded = 112; 66 | expanded = 125; 67 | extraExpanded = 150; 68 | ultraExpanded = 200; 69 | }; 70 | 71 | # QFont::StyleStrategy 72 | # This one's... special. 73 | styleStrategy = { 74 | prefer = { 75 | default = 1; 76 | bitmap = 2; 77 | device = 4; 78 | outline = 8; 79 | forceOutline = 16; 80 | }; 81 | matchingPrefer = { 82 | default = 0; 83 | exact = 32; 84 | quality = 64; 85 | }; 86 | antialiasing = { 87 | default = 0; 88 | prefer = 128; 89 | disable = 256; 90 | }; 91 | noSubpixelAntialias = 2048; 92 | preferNoShaping = 4096; 93 | noFontMerging = 32768; 94 | }; 95 | }; 96 | 97 | inherit (builtins) 98 | attrNames 99 | mapAttrs 100 | removeAttrs 101 | isAttrs 102 | ; 103 | inherit (lib) filterAttrs; 104 | 105 | toEnums = v: lib.types.enum (attrNames v); 106 | in 107 | mapAttrs (_: toEnums) (removeAttrs enums [ "styleStrategy" ]) 108 | // { 109 | styleStrategy = mapAttrs (_: toEnums) (filterAttrs (_: isAttrs) enums.styleStrategy); 110 | 111 | # Converts a font specified by the given attrset to a string representation compatible with 112 | # QFont::fromString and QFont::toString. 113 | fontToString = 114 | { 115 | family, 116 | pointSize ? null, 117 | pixelSize ? null, 118 | styleHint ? "anyStyle", 119 | weight ? "normal", 120 | style ? "normal", 121 | underline ? false, 122 | strikeOut ? false, 123 | fixedPitch ? false, 124 | capitalization ? "mixedCase", 125 | letterSpacingType ? "percentage", 126 | letterSpacing ? 0, 127 | wordSpacing ? 0, 128 | stretch ? "anyStretch", 129 | styleStrategy ? { }, 130 | styleName ? null, 131 | }: 132 | let 133 | inherit (builtins) 134 | isString 135 | toString 136 | foldl' 137 | bitOr 138 | ; 139 | 140 | styleStrategy' = 141 | let 142 | match = s: enums.styleStrategy.${s}.${styleStrategy.${s} or "default"}; 143 | ifSet = k: if styleStrategy.${k} or false then enums.styleStrategy.${k} else 0; 144 | in 145 | foldl' bitOr 0 [ 146 | (match "prefer") 147 | (match "matchingPrefer") 148 | (match "antialiasing") 149 | (ifSet "noSubpixelAntialias") 150 | (ifSet "preferNoShaping") 151 | (ifSet "noFontMerging") 152 | ]; 153 | 154 | sizeToString = s: if s == null then "-1" else toString s; 155 | 156 | numOrEnum = attrs: s: if isString s then toString attrs.${s} else toString s; 157 | 158 | zeroOrOne = b: if b then "1" else "0"; 159 | in 160 | assert lib.assertMsg (lib.xor (pointSize != null) ( 161 | pixelSize != null 162 | )) "Exactly one of `pointSize` and `pixelSize` has to be set."; 163 | builtins.concatStringsSep "," ( 164 | [ 165 | family 166 | (sizeToString pointSize) 167 | (sizeToString pixelSize) 168 | (toString enums.styleHint.${styleHint}) 169 | (numOrEnum enums.weight weight) 170 | (numOrEnum enums.style style) 171 | (zeroOrOne underline) 172 | (zeroOrOne strikeOut) 173 | (zeroOrOne fixedPitch) 174 | "0" 175 | (toString enums.capitalization.${capitalization}) 176 | (toString enums.spacingType.${letterSpacingType}) 177 | (toString letterSpacing) 178 | (toString wordSpacing) 179 | (numOrEnum enums.stretch stretch) 180 | (toString styleStrategy') 181 | ] 182 | ++ lib.optional (styleName != null) styleName 183 | ); 184 | } 185 | -------------------------------------------------------------------------------- /lib/types.nix: -------------------------------------------------------------------------------- 1 | { lib, config, ... }: 2 | let 3 | ############################################################################## 4 | # Types for storing settings. 5 | basicSettingsType = ( 6 | with lib.types; 7 | nullOr (oneOf [ 8 | bool 9 | float 10 | int 11 | str 12 | ]) 13 | ); 14 | advancedSettingsType = ( 15 | with lib.types; 16 | submodule { 17 | options = { 18 | value = lib.mkOption { 19 | type = basicSettingsType; 20 | default = null; 21 | description = "The value for some key."; 22 | }; 23 | immutable = lib.mkOption { 24 | type = bool; 25 | default = config.programs.plasma.immutableByDefault; 26 | description = '' 27 | Whether to make the key immutable. This corresponds to adding [$i] to 28 | the end of the key. 29 | ''; 30 | }; 31 | shellExpand = lib.mkOption { 32 | type = bool; 33 | default = false; 34 | description = '' 35 | Whether to mark the key for shell expansion. This corresponds to 36 | adding [$e] to the end of the key. 37 | ''; 38 | }; 39 | persistent = lib.mkOption { 40 | type = bool; 41 | default = false; 42 | description = '' 43 | When overrideConfig is enabled and the key is persistent, 44 | plasma-manager will leave it unchanged after activation. 45 | ''; 46 | }; 47 | escapeValue = lib.mkOption { 48 | type = bool; 49 | default = true; 50 | description = '' 51 | Whether to escape the value according to kde's escape-format. See: 52 | https://invent.kde.org/frameworks/kconfig/-/blob/v6.7.0/src/core/kconfigini.cpp?ref_type=tags#L880-945 53 | for info about this format. 54 | ''; 55 | }; 56 | }; 57 | } 58 | ); 59 | coercedSettingsType = 60 | with lib.types; 61 | coercedTo basicSettingsType (value: { inherit value; }) advancedSettingsType; 62 | in 63 | { 64 | inherit basicSettingsType; 65 | inherit advancedSettingsType; 66 | inherit coercedSettingsType; 67 | } 68 | -------------------------------------------------------------------------------- /lib/wallpapers.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | { 3 | wallpaperPictureOfTheDayType = 4 | with lib.types; 5 | submodule { 6 | options = { 7 | provider = lib.mkOption { 8 | type = nullOr (enum [ 9 | "apod" 10 | "bing" 11 | "flickr" 12 | "natgeo" 13 | "noaa" 14 | "wcpotd" 15 | "epod" 16 | "simonstalenhag" 17 | ]); 18 | description = "The provider for the Picture of the Day plugin."; 19 | }; 20 | updateOverMeteredConnection = lib.mkOption { 21 | type = bool; 22 | default = false; 23 | description = "Whether to update the wallpaper on a metered connection."; 24 | }; 25 | }; 26 | }; 27 | 28 | wallpaperSlideShowType = 29 | with lib.types; 30 | submodule { 31 | options = { 32 | path = lib.mkOption { 33 | type = either path (listOf path); 34 | description = "The path(s) where the wallpapers are located."; 35 | }; 36 | interval = lib.mkOption { 37 | type = int; 38 | default = 300; 39 | description = "The length between wallpaper switches."; 40 | }; 41 | }; 42 | }; 43 | 44 | # Values are taken from 45 | # https://invent.kde.org/plasma/kdeplasma-addons/-/blob/bc53d651cf60709396c9229f8c582ec8a9d2ee53/applets/mediaframe/package/contents/ui/ConfigGeneral.qml#L148-170 46 | wallpaperFillModeTypes = { 47 | "stretch" = 0; # a.k.a. Scaled 48 | "preserveAspectFit" = 1; # a.k.a. Scaled Keep Proportions 49 | "preserveAspectCrop" = 2; # a.k.a. Scaled And Cropped 50 | "tile" = 3; 51 | "tileVertically" = 4; 52 | "tileHorizontally" = 5; 53 | "pad" = 6; # a.k.a. Centered 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /lib/writeconfig.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, ... }: 2 | 3 | let 4 | writeConfigScript = pkgs.writeShellApplication { 5 | name = "write_config"; 6 | runtimeInputs = with pkgs; [ python3 ]; 7 | text = ''python ${../script/write_config.py} "$@"''; 8 | }; 9 | 10 | ############################################################################## 11 | # Generate a command to run the config-writer script by first sending in the 12 | # attribute-set as json. Here a is the attribute-set. 13 | # 14 | # Type: AttrSet -> string 15 | writeConfig = 16 | json: overrideConfig: resetFilesList: 17 | let 18 | jsonStr = builtins.toJSON json; 19 | # Writing to file handles special characters better than passing it in as 20 | # an argument to the script. 21 | jsonFile = pkgs.writeText "data.json" jsonStr; 22 | resetFilesStr = builtins.toString ( 23 | if overrideConfig then 24 | resetFilesList ++ [ "${config.xdg.dataHome}/plasma-manager/last_run_*" ] 25 | else 26 | resetFilesList 27 | ); 28 | immutableByDefault = (builtins.toString config.programs.plasma.immutableByDefault); 29 | in 30 | '' 31 | ${writeConfigScript}/bin/write_config ${jsonFile} "${resetFilesStr}" "${immutableByDefault}" 32 | ''; 33 | in 34 | { 35 | inherit writeConfig; 36 | } 37 | -------------------------------------------------------------------------------- /modules/apps/default.nix: -------------------------------------------------------------------------------- 1 | { ... }: 2 | 3 | { 4 | imports = [ 5 | ./elisa.nix 6 | ./ghostwriter.nix 7 | ./konsole.nix 8 | ./kate 9 | ./okular.nix 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /modules/apps/elisa.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | let 8 | cfg = config.programs.elisa; 9 | 10 | capitalizeWord = 11 | word: 12 | if word == null then 13 | null 14 | else 15 | lib.concatImapStrings (pos: char: if pos == 1 then lib.toUpper char else char) ( 16 | lib.stringToCharacters word 17 | ); 18 | in 19 | { 20 | options.programs.elisa = { 21 | enable = lib.mkEnableOption "the configuration module for Elisa, KDE's music player"; 22 | package = 23 | lib.mkPackageOption pkgs 24 | [ 25 | "kdePackages" 26 | "elisa" 27 | ] 28 | { 29 | nullable = true; 30 | example = "pkgs.libsForQt5.elisa"; 31 | extraDescription = '' 32 | Use `pkgs.libsForQt5.elisa` for Plasma 5 or `pkgs.kdePackages.elisa` for Plasma 6. 33 | You can also set this to `null` if you're using a system-wide installation of Elisa on NixOS. 34 | ''; 35 | }; 36 | 37 | appearance = { 38 | colorScheme = lib.mkOption { 39 | type = lib.types.nullOr lib.types.str; 40 | default = null; 41 | example = "Krita dark orange"; 42 | description = '' 43 | The colour scheme of the UI. Leave this setting at `null` in order to 44 | not override the systems default scheme for for this application. 45 | ''; 46 | }; 47 | showNowPlayingBackground = lib.mkOption { 48 | description = '' 49 | Set to `true` in order to use a blurred version of the album artwork as the background for the 'Now Playing' section in Elisa. 50 | Set to `false` in order to use a solid colour inherited from the Plasma theme. 51 | ''; 52 | default = null; 53 | type = lib.types.nullOr lib.types.bool; 54 | }; 55 | showProgressOnTaskBar = lib.mkOption { 56 | description = '' 57 | Whether to present the current track progress in the task manager widgets in panels. 58 | ''; 59 | default = null; 60 | type = lib.types.nullOr lib.types.bool; 61 | }; 62 | embeddedView = lib.mkOption { 63 | description = '' 64 | Select the sidebar-embedded view for Elisa. The selected view will 65 | be omitted from the sidebar, and its contents will instead be individually 66 | displayed after the main view buttons. 67 | ''; 68 | default = null; 69 | type = lib.types.nullOr ( 70 | lib.types.enum [ 71 | "albums" 72 | "artists" 73 | "genres" 74 | ] 75 | ); 76 | apply = capitalizeWord; 77 | }; 78 | defaultView = lib.mkOption { 79 | description = '' 80 | The default view which will be opened when Elisa is started. 81 | ''; 82 | default = null; 83 | type = lib.types.nullOr ( 84 | lib.types.enum [ 85 | "nowPlaying" 86 | "recentlyPlayed" 87 | "frequentlyPlayed" 88 | "allAlbums" 89 | "allArtists" 90 | "allTracks" 91 | "allGenres" 92 | "files" 93 | "radios" 94 | ] 95 | ); 96 | apply = capitalizeWord; 97 | }; 98 | defaultFilesViewPath = lib.mkOption { 99 | description = '' 100 | The default path which will be opened in the Files view. 101 | Unlike the index paths, shell variables cannot be used here. 102 | ''; 103 | default = null; 104 | example = "/home/username/Music"; 105 | type = lib.types.nullOr lib.types.str; 106 | }; 107 | }; 108 | 109 | indexer = { 110 | paths = lib.mkOption { 111 | description = '' 112 | Stateful, persistent paths to be indexed by the Elisa Indexer. 113 | The Indexer will recursively search for valid music files along the given paths. 114 | Shell variables, such as `$HOME`, may be used freely. 115 | ''; 116 | default = null; 117 | example = '' 118 | [ 119 | "$HOME/Music" 120 | "/ExternalDisk/more-music" 121 | ] 122 | ''; 123 | type = lib.types.nullOr (lib.types.listOf lib.types.str); 124 | }; 125 | scanAtStartup = lib.mkOption { 126 | description = "Whether to automatically scan the configured index paths for new tracks when Elisa is started."; 127 | default = null; 128 | example = true; 129 | type = lib.types.nullOr lib.types.bool; 130 | }; 131 | ratingsStyle = lib.mkOption { 132 | description = '' 133 | The Elisa music database can attach user-defined ratings to each track. 134 | This option defines if the rating is a `0-5 stars` rating, or a binary `Favourite/Not Favourite` rating. 135 | ''; 136 | default = null; 137 | type = lib.types.nullOr ( 138 | lib.types.enum [ 139 | "stars" 140 | "favourites" 141 | ] 142 | ); 143 | }; 144 | }; 145 | 146 | player = { 147 | playAtStartup = lib.mkOption { 148 | description = "Whether to automatically play the previous track when Elisa is started."; 149 | default = null; 150 | type = lib.types.nullOr lib.types.bool; 151 | }; 152 | minimiseToSystemTray = lib.mkOption { 153 | description = '' 154 | Set to `true` in order to make Elisa continue playing in the System Tray after being closed. 155 | Set to `false` in order to make Elisa quit after being closed. 156 | 157 | By default, the system tray icon is the symbolic variant of the Elisa icon. 158 | ''; 159 | default = null; 160 | type = lib.types.nullOr lib.types.bool; 161 | }; 162 | useAbsolutePlaylistPaths = lib.mkOption { 163 | description = '' 164 | Set to `true` in order to make Elisa write `.m3u8` playlist files using the absolute paths to each track. 165 | Setting to `false` will make Elisa intelligently pick between relative or absolute paths. 166 | ''; 167 | default = null; 168 | type = lib.types.nullOr lib.types.bool; 169 | }; 170 | }; 171 | }; 172 | 173 | config = lib.mkIf cfg.enable { 174 | home.packages = lib.mkIf (cfg.package != null) [ cfg.package ]; 175 | programs.plasma.configFile."elisarc" = 176 | let 177 | concatenatedPaths = builtins.concatStringsSep "," cfg.indexer.paths; 178 | in 179 | lib.mkMerge [ 180 | (lib.mkIf (cfg.indexer.paths != null) { 181 | ElisaFileIndexer.RootPath = { 182 | shellExpand = true; 183 | value = concatenatedPaths; 184 | }; 185 | }) 186 | (lib.mkMerge [ 187 | (lib.mkIf (cfg.player.playAtStartup != null) { 188 | PlayerSettings.PlayAtStartup.value = cfg.player.playAtStartup; 189 | }) 190 | (lib.mkIf (cfg.indexer.scanAtStartup != null) { 191 | PlayerSettings.ScanAtStartup.value = cfg.indexer.scanAtStartup; 192 | }) 193 | (lib.mkIf (cfg.appearance.showNowPlayingBackground != null) { 194 | PlayerSettings.ShowNowPlayingBackground.value = cfg.appearance.showNowPlayingBackground; 195 | }) 196 | (lib.mkIf (cfg.appearance.showProgressOnTaskBar != null) { 197 | PlayerSettings.ShowProgressOnTaskBar.value = cfg.appearance.showProgressOnTaskBar; 198 | }) 199 | (lib.mkIf (cfg.player.minimiseToSystemTray != null) { 200 | PlayerSettings.ShowSystemTrayIcon.value = cfg.player.minimiseToSystemTray; 201 | }) 202 | (lib.mkIf (cfg.indexer.ratingsStyle != null) { 203 | PlayerSettings.UseFavoriteStyleRatings.value = 204 | if (cfg.indexer.ratingsStyle == "Stars") then false else true; 205 | }) 206 | ]) 207 | (lib.mkIf (cfg.player.useAbsolutePlaylistPaths != null) { 208 | Playlist.AlwaysUseAbsolutePlaylistPaths.value = cfg.player.useAbsolutePlaylistPaths; 209 | }) 210 | (lib.mkIf (cfg.appearance.colorScheme != null) { 211 | UiSettings.ColorScheme.value = cfg.appearance.colorScheme; 212 | }) 213 | (lib.mkMerge [ 214 | (lib.mkIf (cfg.appearance.embeddedView != null) { 215 | Views.EmbeddedView.value = "All" + cfg.appearance.embeddedView; 216 | }) 217 | (lib.mkIf (cfg.appearance.defaultFilesViewPath != null) { 218 | Views.InitialFilesViewPath.value = cfg.appearance.defaultFilesViewPath; 219 | }) 220 | (lib.mkIf (cfg.appearance.defaultView != null) { 221 | Views.InitialView.value = cfg.appearance.defaultView; 222 | }) 223 | ]) 224 | ]; 225 | }; 226 | } 227 | -------------------------------------------------------------------------------- /modules/apps/kate/check-theme-name-free.sh: -------------------------------------------------------------------------------- 1 | # there could be a bash shebang to ${pkgs.bash}/bin/bash here 2 | 3 | # https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux/5947802#5947802 4 | RED='\033[0;31m' 5 | 6 | # https://stackoverflow.com/questions/2924697/how-does-one-output-bold-text-in-bash/2924755#2924755 7 | BOLD=$(tput bold) 8 | NORMAL=$(tput sgr0) 9 | 10 | 11 | # # ===================================== 12 | # # CHECK THE NUMBER OF ARGS 13 | # # 14 | # # https://www.baeldung.com/linux/bash-check-script-arguments 15 | 16 | if [[ "$#" -ne 1 ]]; then 17 | # https://stackoverflow.com/questions/3005963/how-can-i-have-a-newline-in-a-string-in-sh/3182519#3182519 18 | >&2 printf "%sIncorrect number of arguments.%s Expected one: The name of the theme that should not already be in use" "${RED}${BOLD}" "${NORMAL}${RED}" 19 | exit 1 20 | fi 21 | 22 | THEMENAME=$1 23 | 24 | 25 | # ===================================== 26 | # GO THROUGH THE THEMES 27 | # 28 | # reference the XDG dir as proposed in https://github.com/nix-community/home-manager/pull/4594#issuecomment-1774024207 29 | 30 | THEME_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/org.kde.syntax-highlighting/themes/" 31 | 32 | # TODO and symlinks! (or: skip over dirs) 33 | find "${THEME_DIR}" \( -type f -o -type l \) | while read -r themefile; do 34 | THIS_THEMENAME=$(jq -r .metadata.name "${themefile}") 35 | 36 | if [[ "${THIS_THEMENAME}" == "${themefile}" ]]; then 37 | # make sure to not look at symbolic links to the nix store 38 | # https://stackoverflow.com/questions/17918367/linux-shell-verify-whether-a-file-exists-and-is-a-soft-link/17918442#17918442 39 | # https://stackoverflow.com/questions/2172352/in-bash-how-can-i-check-if-a-string-begins-with-some-value/2172367#2172367 40 | if [[ ! ( -L "${themefile}" && $(readlink -f "${themefile}") == /nix/store/* ) ]]; then 41 | >&2 printf "%s In %s there is already a theme with the name %s (%s).%s You could rename the theme given in config.programs.kate.editor.theme.src by changing the value for metadata.name inside the theme." "${RED}${BOLD}" "${THEME_DIR}" "${THEMENAME}" "${themefile}" "${NORMAL}${RED}" 42 | exit 1 # even on dryrun 43 | fi 44 | fi 45 | done 46 | -------------------------------------------------------------------------------- /modules/apps/konsole.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | let 3 | inherit 4 | (import ../../lib/types.nix { 5 | inherit lib; 6 | inherit config; 7 | }) 8 | basicSettingsType 9 | ; 10 | 11 | # used as shown in the example in the library docs: 12 | # https://noogle.dev/f/lib/attrsets/mapAttrs' 13 | createColorSchemes = lib.attrsets.mapAttrs' ( 14 | name: value: 15 | lib.attrsets.nameValuePair "konsole/${name}.colorscheme" { 16 | enable = true; 17 | source = value; 18 | } 19 | ); 20 | 21 | cfg = config.programs.konsole; 22 | profilesSubmodule = { 23 | options = { 24 | name = lib.mkOption { 25 | type = with lib.types; nullOr str; 26 | default = null; 27 | description = '' 28 | Name of the profile. Defaults to the attribute name. 29 | ''; 30 | }; 31 | colorScheme = lib.mkOption { 32 | type = with lib.types; nullOr str; 33 | default = null; 34 | example = "Catppuccin-Mocha"; 35 | description = '' 36 | Color scheme the profile will use. You can check the files you can 37 | use in `$HOME/.local/share/konsole` or `/run/current-system/sw/share/konsole`. 38 | You might also add a custom color scheme using 39 | `programs.konsole.customColorSchemes`. 40 | ''; 41 | }; 42 | command = lib.mkOption { 43 | type = with lib.types; nullOr str; 44 | default = null; 45 | example = lib.literalExpression ''"''${pkgs.zsh}/bin/zsh"''; 46 | description = '' 47 | The command to run on new sessions. 48 | ''; 49 | }; 50 | font = { 51 | name = lib.mkOption { 52 | type = lib.types.str; 53 | /* 54 | TODO: Set default to null after adding an assertion 55 | Konsole needs to have a font set to be able to change font size 56 | Since I couldn't get that to work I'll just set a default font 57 | Not ideal since IMO we should only write things that are set explicitly 58 | by the user but ehh it is what it is 59 | */ 60 | default = "Hack"; 61 | example = "Hack"; 62 | description = '' 63 | Name of the font the profile should use. 64 | ''; 65 | }; 66 | size = lib.mkOption { 67 | # The konsole ui gives you a limited range 68 | type = (lib.types.ints.between 4 128); 69 | default = 10; 70 | example = 12; 71 | description = '' 72 | Size of the font. 73 | Due to Konsole limitations, only a limited range of sizes is possible. 74 | ''; 75 | }; 76 | }; 77 | extraConfig = lib.mkOption { 78 | type = with lib.types; attrsOf (attrsOf basicSettingsType); 79 | default = { }; 80 | example = { }; 81 | description = '' 82 | Extra keys to manually add to the profile. 83 | ''; 84 | }; 85 | }; 86 | }; 87 | in 88 | 89 | { 90 | options.programs.konsole = { 91 | enable = lib.mkEnableOption '' 92 | configuration management for Konsole, the KDE Terminal. 93 | ''; 94 | 95 | defaultProfile = lib.mkOption { 96 | type = with lib.types; nullOr str; 97 | default = null; 98 | example = "Catppuccin"; 99 | description = '' 100 | The name of the Konsole profile file to use by default. 101 | To see what options you have, take a look at `$HOME/.local/share/konsole` 102 | ''; 103 | }; 104 | 105 | profiles = lib.mkOption { 106 | type = with lib.types; nullOr (attrsOf (submodule profilesSubmodule)); 107 | default = { }; 108 | description = '' 109 | Plasma profiles to generate. 110 | ''; 111 | }; 112 | 113 | customColorSchemes = lib.mkOption { 114 | type = with lib.types; attrsOf path; 115 | default = { }; 116 | description = '' 117 | Custom color schemes to be added to the installation. The attribute key maps to their name. 118 | Choose them in any profile with `profiles..colorScheme = `; 119 | ''; 120 | }; 121 | 122 | ui.colorScheme = lib.mkOption { 123 | type = with lib.types; nullOr str; 124 | default = null; 125 | example = "Krita dark orange"; 126 | description = '' 127 | The color scheme of the UI. Leave this setting at `null` in order to 128 | not override the system's default scheme for for this application. 129 | ''; 130 | }; 131 | 132 | extraConfig = lib.mkOption { 133 | type = with lib.types; attrsOf (attrsOf basicSettingsType); 134 | default = { }; 135 | description = '' 136 | Extra config to add to the `konsolerc`. 137 | ''; 138 | }; 139 | }; 140 | 141 | config = lib.mkIf (cfg.enable) { 142 | programs.plasma.configFile."konsolerc" = lib.mkMerge [ 143 | (lib.mkIf (cfg.defaultProfile != null) { 144 | "Desktop Entry"."DefaultProfile" = "${cfg.defaultProfile}.profile"; 145 | }) 146 | (lib.mapAttrs ( 147 | groupName: (lib.mapAttrs (keyName: keyAttrs: { value = keyAttrs; })) 148 | ) cfg.extraConfig) 149 | { 150 | "UiSettings"."ColorScheme" = lib.mkIf (cfg.ui.colorScheme != null) { 151 | value = cfg.ui.colorScheme; 152 | # The key needs to be immutable to work properly when using overrideConfig. 153 | # See discussion at: https://github.com/nix-community/plasma-manager/pull/186 154 | immutable = lib.mkIf config.programs.plasma.overrideConfig (lib.mkDefault true); 155 | }; 156 | } 157 | ]; 158 | 159 | xdg.dataFile = lib.mkMerge [ 160 | (lib.mkIf (cfg.profiles != { }) ( 161 | lib.mkMerge ( 162 | lib.mapAttrsToList ( 163 | attrName: profile: 164 | let 165 | # Use the name from the name option if it's set 166 | profileName = if builtins.isString profile.name then profile.name else attrName; 167 | fontString = lib.mkIf ( 168 | profile.font.name != null 169 | ) "${profile.font.name},${builtins.toString profile.font.size}"; 170 | in 171 | { 172 | "konsole/${profileName}.profile".text = lib.generators.toINI { } ( 173 | lib.recursiveUpdate { 174 | "General" = ( 175 | { 176 | "Name" = profileName; 177 | # Konsole generated profiles seem to always have this 178 | "Parent" = "FALLBACK/"; 179 | } 180 | // (lib.optionalAttrs (profile.command != null) { "Command" = profile.command; }) 181 | ); 182 | "Appearance" = ( 183 | { 184 | # If the font size is not set we leave a comma at the end after the name 185 | # We should fix this probs but konsole doesn't seem to care ¯\_(ツ)_/¯ 186 | "Font" = fontString.content; 187 | } 188 | // (lib.optionalAttrs (profile.colorScheme != null) { "ColorScheme" = profile.colorScheme; }) 189 | ); 190 | } profile.extraConfig 191 | ); 192 | } 193 | ) cfg.profiles 194 | ) 195 | )) 196 | (createColorSchemes cfg.customColorSchemes) 197 | ]; 198 | }; 199 | } 200 | -------------------------------------------------------------------------------- /modules/apps/okular.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | lib, 4 | pkgs, 5 | ... 6 | }: 7 | 8 | let 9 | cfg = config.programs.okular; 10 | getIndexFromEnum = 11 | enum: value: 12 | if value == null then 13 | null 14 | else 15 | lib.lists.findFirstIndex (x: x == value) 16 | (throw "getIndexFromEnum (okular): Value ${value} isn't present in the enum. This is a bug.") 17 | enum; 18 | in 19 | with lib.types; 20 | { 21 | options.programs.okular = { 22 | enable = lib.mkEnableOption '' 23 | configuration management for okular. 24 | ''; 25 | 26 | package = 27 | lib.mkPackageOption pkgs 28 | [ 29 | "kdePackages" 30 | "okular" 31 | ] 32 | { 33 | nullable = true; 34 | example = "pkgs.libsForQt5.okular"; 35 | extraDescription = '' 36 | Which okular package to install. Use `pkgs.libsForQt5.okular` in Plasma5 and 37 | `pkgs.kdePackages.okular` in Plasma6. Use `null` if home-manager should not install Okular. 38 | ''; 39 | }; 40 | 41 | # ================================== 42 | # GENERAL 43 | general = { 44 | smoothScrolling = lib.mkOption { 45 | description = "Whether to use smooth scrolling."; 46 | default = null; 47 | type = nullOr bool; 48 | }; 49 | 50 | showScrollbars = lib.mkOption { 51 | description = "Whether to show scrollbars in the document viewer."; 52 | default = null; 53 | type = nullOr bool; 54 | }; 55 | 56 | openFileInTabs = lib.mkOption { 57 | description = "Whether to open files in tabs."; 58 | default = null; 59 | type = nullOr bool; 60 | }; 61 | 62 | viewContinuous = lib.mkOption { 63 | description = "Whether to open in continous mode by default."; 64 | default = null; 65 | type = nullOr bool; 66 | }; 67 | 68 | viewMode = lib.mkOption { 69 | description = "The view mode for the pages."; 70 | default = null; 71 | type = nullOr (enum [ 72 | "Single" 73 | "Facing" 74 | "FacingFirstCentered" 75 | "Summary" 76 | ]); 77 | }; 78 | 79 | zoomMode = 80 | let 81 | enumVals = [ 82 | "100%" 83 | "fitWidth" 84 | "fitPage" 85 | "autoFit" 86 | ]; 87 | in 88 | lib.mkOption { 89 | description = '' 90 | Specifies the default zoom mode for file which were never opened before. 91 | For those files which were opened before the previous zoom mode is applied. 92 | ''; 93 | default = null; 94 | type = nullOr (enum enumVals); 95 | apply = getIndexFromEnum enumVals; 96 | }; 97 | 98 | obeyDrm = lib.mkOption { 99 | description = '' 100 | Whether Okular should obey DRM (Digital Rights Management) restrictions. 101 | DRM limitations are used to make it impossible to perform certain actions with PDF documents, such as copying content to the clipboard. 102 | Note that in some configurations of Okular, this option is not available. 103 | ''; 104 | default = null; 105 | type = nullOr bool; 106 | }; 107 | 108 | mouseMode = lib.mkOption { 109 | description = '' 110 | Changes what the mouse does. 111 | See the [Okular Documentation](https://docs.kde.org/stable5/en/okular/okular/menutools.html) for the full description. 112 | 113 | - `Browse`: Click-and-drag with left mouse button. 114 | - `Zoom`: Zoom in with left mouse button. Reset zoom with right mouse button. 115 | - `RectSelect`: Draw area selection with left mouse button. Display options with right mouse button. 116 | - `TextSelect`: Select text with left mouse button. Display options with right mouse button. 117 | - `TableSelect`: Similar to text selection but allows for transforming the document into a table. 118 | - `Magnifier`: Activates the magnifier with left mouse button. 119 | ''; 120 | default = null; 121 | type = nullOr (enum [ 122 | "Browse" 123 | "Zoom" 124 | "RectSelect" 125 | "TextSelect" 126 | "TableSelect" 127 | "Magnifier" 128 | "TrimSelect" 129 | ]); 130 | }; 131 | }; 132 | 133 | # ================================== 134 | # ACCESSIBILITY 135 | accessibility = { 136 | highlightLinks = lib.mkOption { 137 | description = "Whether to draw borders around links."; 138 | default = null; 139 | type = nullOr bool; 140 | }; 141 | 142 | changeColors = { 143 | enable = lib.mkEnableOption "" // { 144 | description = "Whether to change the colors of the documents."; 145 | }; 146 | mode = lib.mkOption { 147 | description = "Mode used to change the colors."; 148 | default = null; 149 | type = nullOr (enum [ 150 | # Inverts colors, including hue 151 | "Inverted" 152 | # Change background color (see option below) 153 | "Paper" 154 | # Change light and dark colors (see options below) 155 | "Recolor" 156 | # Change to black & white colors (see options below) 157 | "BlackWhite" 158 | # Invert lightness but leave hue and saturation 159 | "InvertLightness" 160 | # Like InvertLightness, but slightly more contrast 161 | "InvertLumaSymmetric" 162 | # Like InvertLightness, but much more contrast 163 | "InvertLuma" 164 | # Shift hue of all colors by 120 degrees 165 | "HueShiftPositive" 166 | # Shift hue of all colors by 240 degrees 167 | "HueShiftNegative" 168 | ]); 169 | }; 170 | paperColor = lib.mkOption { 171 | description = "Paper color in RGB. Used for the `Paper` mode."; 172 | default = null; 173 | example = "255,255,255"; 174 | type = nullOr str; 175 | }; 176 | recolorBackground = lib.mkOption { 177 | description = "New background color in RGB. Used for the `Recolor` mode."; 178 | default = null; 179 | example = "0,0,0"; 180 | type = nullOr str; 181 | }; 182 | recolorForeground = lib.mkOption { 183 | description = "New foreground color in RGB. Used for the `Recolor` mode."; 184 | default = null; 185 | example = "255,255,255"; 186 | type = nullOr str; 187 | }; 188 | blackWhiteContrast = lib.mkOption { 189 | description = "New contrast strength. Used for the `BlackWhite` mode."; 190 | default = null; 191 | example = 4; 192 | type = nullOr (ints.between 2 6); 193 | }; 194 | blackWhiteThreshold = lib.mkOption { 195 | description = '' 196 | A threshold for deciding between black and white. 197 | Higher values lead to brighter grays. 198 | Used for the `BlackWhite` mode. 199 | ''; 200 | default = null; 201 | example = 127; 202 | type = nullOr (numbers.between 2 253); 203 | }; 204 | }; 205 | }; 206 | 207 | # ================================== 208 | # PERFORMANCE 209 | performance = { 210 | enableTransparencyEffects = lib.mkOption { 211 | description = "Whether to enable transparancy effects. This may increase CPU usage."; 212 | default = null; 213 | type = nullOr bool; 214 | }; 215 | 216 | memoryUsage = lib.mkOption { 217 | description = "Memory usage profile for Okular. This may impact the speed performance of Okular, as it determines how many computation results are kept in memory."; 218 | default = null; 219 | type = nullOr (enum [ 220 | "Low" 221 | "Normal" 222 | "Aggressive" 223 | "Greedy" 224 | ]); 225 | }; 226 | }; 227 | }; 228 | 229 | config = { 230 | home.packages = lib.mkIf (cfg.enable && cfg.package != null) [ cfg.package ]; 231 | }; 232 | 233 | # ================================== 234 | # WRITING THE OKULARPARTRC 235 | config.programs.plasma.configFile."okularpartrc" = lib.mkIf cfg.enable ( 236 | let 237 | gen = cfg.general; 238 | acc = cfg.accessibility; 239 | perf = cfg.performance; 240 | applyIfSet = opt: lib.mkIf (opt != null) opt; 241 | in 242 | { 243 | "PageView" = { 244 | "SmoothScrolling" = applyIfSet gen.smoothScrolling; 245 | "ShowScrollBars" = applyIfSet gen.showScrollbars; 246 | "ViewContinuous" = applyIfSet gen.viewContinuous; 247 | "ViewMode" = applyIfSet gen.viewMode; 248 | "MouseMode" = applyIfSet gen.mouseMode; 249 | }; 250 | 251 | "Zoom" = { 252 | "ZoomMode" = applyIfSet gen.zoomMode; 253 | }; 254 | "Core General" = { 255 | "ObeyDRM" = applyIfSet gen.obeyDrm; 256 | }; 257 | 258 | "General" = { 259 | "ShellOpenFileInTabs" = applyIfSet gen.openFileInTabs; 260 | }; 261 | 262 | "Document" = { 263 | "ChangeColors" = applyIfSet acc.changeColors.enable; 264 | "RenderMode" = applyIfSet acc.changeColors.mode; 265 | "PaperColor" = applyIfSet acc.changeColors.paperColor; 266 | }; 267 | 268 | "Dlg Accessibility" = { 269 | "HighlightLinks" = applyIfSet acc.highlightLinks; 270 | "RecolorBackground" = applyIfSet acc.changeColors.recolorBackground; 271 | "RecolorForeground" = applyIfSet acc.changeColors.recolorForeground; 272 | "BWContrast" = applyIfSet acc.changeColors.blackWhiteContrast; 273 | "BWThreshold" = applyIfSet acc.changeColors.blackWhiteThreshold; 274 | }; 275 | 276 | "Core Performance" = { 277 | "MemoryLevel" = applyIfSet perf.memoryUsage; 278 | }; 279 | 280 | "Dlg Performance" = { 281 | "EnableCompositing" = applyIfSet perf.enableTransparencyEffects; 282 | }; 283 | } 284 | ); 285 | } 286 | -------------------------------------------------------------------------------- /modules/default.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | 3 | { 4 | imports = [ 5 | ./apps 6 | ./desktop.nix 7 | ./files.nix 8 | ./fonts.nix 9 | ./hotkeys.nix 10 | ./input.nix 11 | ./krunner.nix 12 | ./kscreenlocker.nix 13 | ./kwin.nix 14 | ./panels.nix 15 | ./powerdevil.nix 16 | ./session.nix 17 | ./shortcuts.nix 18 | ./spectacle.nix 19 | ./startup.nix 20 | ./window-rules.nix 21 | ./windows.nix 22 | ./workspace.nix 23 | ]; 24 | 25 | options.programs.plasma.enable = lib.mkEnableOption '' 26 | declarative configuration options for the KDE Plasma Desktop. 27 | ''; 28 | } 29 | -------------------------------------------------------------------------------- /modules/files.nix: -------------------------------------------------------------------------------- 1 | # Low-level access to changing Plasma settings. 2 | { 3 | config, 4 | lib, 5 | pkgs, 6 | ... 7 | }: 8 | 9 | let 10 | inherit (import ../lib/writeconfig.nix { inherit lib pkgs config; }) writeConfig; 11 | inherit 12 | (import ../lib/types.nix { 13 | inherit lib; 14 | inherit config; 15 | }) 16 | coercedSettingsType 17 | ; 18 | 19 | # Helper function to prepend the appropriate path prefix (e.g. XDG_CONFIG_HOME) to file 20 | prependPath = 21 | prefix: attrset: 22 | lib.mapAttrs' (path: config: { 23 | name = "${prefix}/${path}"; 24 | value = config; 25 | }) attrset; 26 | plasmaCfg = config.programs.plasma; 27 | cfg = 28 | (prependPath config.home.homeDirectory plasmaCfg.file) 29 | // (prependPath config.xdg.configHome plasmaCfg.configFile) 30 | // (prependPath config.xdg.dataHome plasmaCfg.dataFile); 31 | 32 | fileSettingsType = with lib.types; attrsOf (attrsOf (attrsOf coercedSettingsType)); 33 | 34 | ############################################################################## 35 | # Generate a script that will use write_config.py to update all 36 | # settings. 37 | resetFilesList = ( 38 | map (f: "${config.xdg.configHome}/${f}") ( 39 | lib.lists.subtractLists plasmaCfg.resetFilesExclude plasmaCfg.resetFiles 40 | ) 41 | ); 42 | script = pkgs.writeScript "plasma-config" (writeConfig cfg plasmaCfg.overrideConfig resetFilesList); 43 | 44 | ############################################################################## 45 | # Generate a script that will remove all the current config files. 46 | defaultResetFiles = ( 47 | if plasmaCfg.overrideConfig then 48 | [ 49 | "auroraerc" 50 | "baloofilerc" 51 | "dolphinrc" 52 | "ffmpegthumbsrc" 53 | "kactivitymanagerdrc" 54 | "katerc" 55 | "kcminputrc" 56 | "KDE/Sonnet.conf" 57 | "kde.org/ghostwriter.conf" 58 | "kded5rc" 59 | "kded6rc" 60 | "kdeglobals" 61 | "kgammarc" 62 | "kglobalshortcutsrc" 63 | "khotkeysrc" 64 | "kiorc" 65 | "klaunchrc" 66 | "klipperrc" 67 | "kmixrc" 68 | "krunnerrc" 69 | "kscreenlockerrc" 70 | "kservicemenurc" 71 | "ksmserverrc" 72 | "ksplashrc" 73 | "kwalletrc" 74 | "kwin_rules_dialogrc" 75 | "kwinrc" 76 | "kwinrulesrc" 77 | "kxkbrc" 78 | "plasma_calendar_alternatecalendar" 79 | "plasma_calendar_astronomicalevents" 80 | "plasma_calendar_holiday_regions" 81 | "plasma-localerc" 82 | "plasmanotifyrc" 83 | "plasmarc" 84 | "plasmashellrc" 85 | "powerdevilrc" 86 | "systemsettingsrc" 87 | ] 88 | else 89 | lib.optional (builtins.length plasmaCfg.window-rules > 0) "kwinrulesrc" 90 | ); 91 | in 92 | { 93 | options.programs.plasma = { 94 | file = lib.mkOption { 95 | type = fileSettingsType; 96 | default = { }; 97 | description = '' 98 | An attribute set where the keys are file names (relative to 99 | `$HOME`) and the values are attribute sets that represent 100 | configuration groups and settings inside those groups. 101 | ''; 102 | }; 103 | configFile = lib.mkOption { 104 | type = fileSettingsType; 105 | default = { }; 106 | description = '' 107 | An attribute set where the keys are file names (relative to 108 | `$XDG_CONFIG_HOME`) and the values are attribute sets that 109 | represent configuration groups and settings inside those groups. 110 | ''; 111 | }; 112 | dataFile = lib.mkOption { 113 | type = fileSettingsType; 114 | default = { }; 115 | description = '' 116 | An attribute set where the keys are file names (relative to 117 | `$XDG_DATA_HOME`) and the values are attribute sets that 118 | represent configuration groups and settings inside those groups. 119 | ''; 120 | }; 121 | overrideConfig = lib.mkOption { 122 | type = lib.types.bool; 123 | default = false; 124 | description = '' 125 | Wether to discard changes made outside `plasma-manager`. If enabled, all 126 | settings not specified explicitly in `plasma-manager` will be set to the 127 | default on next login. This will automatically delete a lot of 128 | KDE Plasma configuration files on each generation, so do be careful with this 129 | option. 130 | ''; 131 | }; 132 | resetFiles = lib.mkOption { 133 | type = lib.types.listOf lib.types.str; 134 | default = defaultResetFiles; 135 | description = '' 136 | Configuration files which should be explicitly deleted on each generation. 137 | ''; 138 | }; 139 | resetFilesExclude = lib.mkOption { 140 | type = lib.types.listOf lib.types.str; 141 | default = [ ]; 142 | description = '' 143 | Configuration files which explicitly should not be deleted on each generation, if `overrideConfig` is enabled. 144 | ''; 145 | }; 146 | immutableByDefault = lib.mkEnableOption "" // { 147 | description = "Whether to make keys written by plasma-manager immutable by default."; 148 | }; 149 | }; 150 | 151 | imports = [ 152 | (lib.mkRenamedOptionModule 153 | [ 154 | "programs" 155 | "plasma" 156 | "files" 157 | ] 158 | [ 159 | "programs" 160 | "plasma" 161 | "configFile" 162 | ] 163 | ) 164 | (lib.mkRenamedOptionModule 165 | [ 166 | "programs" 167 | "plasma" 168 | "overrideConfigFiles" 169 | ] 170 | [ 171 | "programs" 172 | "plasma" 173 | "resetFiles" 174 | ] 175 | ) 176 | (lib.mkRenamedOptionModule 177 | [ 178 | "programs" 179 | "plasma" 180 | "overrideConfigExclude" 181 | ] 182 | [ 183 | "programs" 184 | "plasma" 185 | "resetFilesExclude" 186 | ] 187 | ) 188 | ]; 189 | 190 | config.home.activation = lib.mkIf plasmaCfg.enable { 191 | configure-plasma = ( 192 | lib.hm.dag.entryAfter [ "writeBoundary" ] '' 193 | $DRY_RUN_CMD ${script} 194 | '' 195 | ); 196 | }; 197 | } 198 | -------------------------------------------------------------------------------- /modules/fonts.nix: -------------------------------------------------------------------------------- 1 | { lib, config, ... }: 2 | let 3 | inherit (lib) mkIf mkOption types; 4 | qfont = import ../lib/qfont.nix { inherit lib; }; 5 | 6 | styleStrategyType = types.submodule { 7 | options = with qfont.styleStrategy; { 8 | prefer = mkOption { 9 | type = prefer; 10 | default = "default"; 11 | description = '' 12 | Which type of font is preferred by the font when finding an appropriate default family. 13 | 14 | `default`, `bitmap`, `device`, `outline`, `forceOutline` correspond to the 15 | `PreferDefault`, `PreferBitmap`, `PreferDevice`, `PreferOutline`, `ForceOutline` enum flags 16 | respectively. 17 | ''; 18 | }; 19 | matchingPrefer = mkOption { 20 | type = matchingPrefer; 21 | default = "default"; 22 | description = '' 23 | Whether the font matching process prefers exact matches, or best quality matches. 24 | 25 | `default` corresponds to not setting any enum flag, and `exact` and `quality` 26 | correspond to `PreferMatch` and `PreferQuality` enum flags respectively. 27 | ''; 28 | }; 29 | antialiasing = mkOption { 30 | type = antialiasing; 31 | default = "default"; 32 | description = '' 33 | Whether antialiasing is preferred for this font. 34 | 35 | `default` corresponds to not setting any enum flag, and `prefer` and `disable` 36 | correspond to `PreferAntialias` and `NoAntialias` enum flags respectively. 37 | ''; 38 | }; 39 | noSubpixelAntialias = mkOption { 40 | type = types.bool; 41 | default = false; 42 | description = '' 43 | If set to `true`, this font will try to avoid subpixel antialiasing. 44 | 45 | Corresponds to the `NoSubpixelAntialias` enum flag. 46 | ''; 47 | }; 48 | noFontMerging = mkOption { 49 | type = types.bool; 50 | default = false; 51 | description = '' 52 | If set to `true`, this font will not try to find a substitute font when encountering missing glyphs. 53 | 54 | Corresponds to the `NoFontMerging` enum flag. 55 | ''; 56 | }; 57 | preferNoShaping = mkOption { 58 | type = types.bool; 59 | default = false; 60 | description = '' 61 | If set to true, this font will not try to apply shaping rules that may be required for some scripts 62 | (e.g. Indic scripts), increasing performance if these rules are not required. 63 | 64 | Corresponds to the `PreferNoShaping` enum flag. 65 | ''; 66 | }; 67 | }; 68 | }; 69 | 70 | fontType = types.submodule { 71 | options = { 72 | family = mkOption { 73 | type = types.str; 74 | description = "The font family of this font."; 75 | example = "Noto Sans"; 76 | }; 77 | pointSize = mkOption { 78 | type = types.nullOr types.numbers.positive; 79 | default = null; 80 | description = '' 81 | The point size of this font. 82 | 83 | Could be a decimal, but usually an integer. Mutually exclusive with pixel size. 84 | ''; 85 | }; 86 | pixelSize = mkOption { 87 | type = types.nullOr types.ints.u16; 88 | default = null; 89 | description = '' 90 | The pixel size of this font. 91 | 92 | Mutually exclusive with point size. 93 | ''; 94 | }; 95 | styleHint = mkOption { 96 | type = qfont.styleHint; 97 | default = "anyStyle"; 98 | description = '' 99 | The style hint of this font. 100 | 101 | See https://doc.qt.io/qt-6/qfont.html#StyleHint-enum for more. 102 | ''; 103 | }; 104 | weight = mkOption { 105 | type = types.either (types.ints.between 1 1000) qfont.weight; 106 | default = "normal"; 107 | description = '' 108 | The weight of the font, either as a number between 1 to 1000 or as a pre-defined weight string. 109 | 110 | See https://doc.qt.io/qt-6/qfont.html#Weight-enum for more. 111 | ''; 112 | }; 113 | style = mkOption { 114 | type = qfont.style; 115 | default = "normal"; 116 | description = "The style of the font."; 117 | }; 118 | underline = mkOption { 119 | type = types.bool; 120 | default = false; 121 | description = "Whether the font is underlined."; 122 | }; 123 | strikeOut = mkOption { 124 | type = types.bool; 125 | default = false; 126 | description = "Whether the font is struck out."; 127 | }; 128 | fixedPitch = mkOption { 129 | type = types.bool; 130 | default = false; 131 | description = "Whether the font has a fixed pitch."; 132 | }; 133 | capitalization = mkOption { 134 | type = qfont.capitalization; 135 | default = "mixedCase"; 136 | description = '' 137 | The capitalization settings for this font. 138 | 139 | See https://doc.qt.io/qt-6/qfont.html#Capitalization-enum for more. 140 | ''; 141 | }; 142 | letterSpacingType = mkOption { 143 | type = qfont.spacingType; 144 | default = "percentage"; 145 | description = '' 146 | Whether to use percentage or absolute spacing for this font. 147 | 148 | See https://doc.qt.io/qt-6/qfont.html#SpacingType-enum for more. 149 | ''; 150 | }; 151 | letterSpacing = mkOption { 152 | type = types.number; 153 | default = 0; 154 | description = '' 155 | The amount of letter spacing for this font. 156 | 157 | Could be a percentage or an absolute spacing change (positive increases spacing, negative decreases spacing), 158 | based on the selected `letterSpacingType`. 159 | ''; 160 | }; 161 | wordSpacing = mkOption { 162 | type = types.number; 163 | default = 0; 164 | description = '' 165 | The amount of word spacing for this font, in pixels. 166 | 167 | Positive values increase spacing while negative ones decrease spacing. 168 | ''; 169 | }; 170 | stretch = mkOption { 171 | type = types.either (types.ints.between 1 4000) qfont.stretch; 172 | default = "anyStretch"; 173 | description = '' 174 | The stretch factor for this font, as an integral percentage (i.e. 150 means a 150% stretch), 175 | or as a pre-defined stretch factor string. 176 | ''; 177 | }; 178 | styleStrategy = mkOption { 179 | type = styleStrategyType; 180 | default = { }; 181 | description = '' 182 | The strategy for matching similar fonts to this font. 183 | 184 | See https://doc.qt.io/qt-6/qfont.html#StyleStrategy-enum for more. 185 | ''; 186 | }; 187 | styleName = mkOption { 188 | type = types.nullOr types.str; 189 | default = null; 190 | description = '' 191 | The style name of this font, overriding the `style` and `weight` parameters when set. 192 | Used for special fonts that have styles beyond traditional settings. 193 | ''; 194 | }; 195 | }; 196 | }; 197 | 198 | inherit (config.programs.plasma) enable; 199 | cfg = lib.filterAttrs (_: v: v != null) config.programs.plasma.fonts; 200 | in 201 | { 202 | options.programs.plasma.fonts = { 203 | general = mkOption { 204 | type = types.nullOr fontType; 205 | default = null; 206 | description = "The main font for the Plasma desktop."; 207 | example = lib.literalExpression '' 208 | { 209 | family = "Noto Sans"; 210 | pointSize = 11; 211 | } 212 | ''; 213 | }; 214 | fixedWidth = mkOption { 215 | type = types.nullOr fontType; 216 | default = null; 217 | description = "The fixed width or monospace font for the Plasma desktop."; 218 | example = lib.literalExpression '' 219 | { 220 | family = "Iosevka"; 221 | pointSize = 11; 222 | } 223 | ''; 224 | }; 225 | small = mkOption { 226 | type = types.nullOr fontType; 227 | default = null; 228 | description = "The font used for very small text."; 229 | example = lib.literalExpression '' 230 | { 231 | family = "Noto Sans"; 232 | pointSize = 8; 233 | } 234 | ''; 235 | }; 236 | toolbar = mkOption { 237 | type = types.nullOr fontType; 238 | default = null; 239 | description = "The font used for toolbars."; 240 | example = lib.literalExpression '' 241 | { 242 | family = "Noto Sans"; 243 | pointSize = 10; 244 | } 245 | ''; 246 | }; 247 | menu = mkOption { 248 | type = types.nullOr fontType; 249 | default = null; 250 | description = "The font used for menus."; 251 | example = lib.literalExpression '' 252 | { 253 | family = "Noto Sans"; 254 | pointSize = 10; 255 | } 256 | ''; 257 | }; 258 | windowTitle = mkOption { 259 | type = types.nullOr fontType; 260 | default = null; 261 | description = "The font used for window titles."; 262 | example = lib.literalExpression '' 263 | { 264 | family = "Noto Sans"; 265 | pointSize = 10; 266 | } 267 | ''; 268 | }; 269 | }; 270 | 271 | config.programs.plasma.configFile.kdeglobals = 272 | let 273 | mkFont = f: mkIf (enable && builtins.hasAttr f cfg) (qfont.fontToString cfg.${f}); 274 | in 275 | { 276 | General = { 277 | font = mkFont "general"; 278 | fixed = mkFont "fixedWidth"; 279 | smallestReadableFont = mkFont "small"; 280 | toolBarFont = mkFont "toolbar"; 281 | menuFont = mkFont "menu"; 282 | }; 283 | WM.activeFont = mkFont "windowTitle"; 284 | }; 285 | } 286 | -------------------------------------------------------------------------------- /modules/hotkeys.nix: -------------------------------------------------------------------------------- 1 | # Global hotkeys (user-defined keyboard shortcuts): 2 | { 3 | pkgs, 4 | config, 5 | lib, 6 | ... 7 | }: 8 | let 9 | cfg = config.programs.plasma; 10 | 11 | commandString = command: (builtins.replaceStrings [ "%" ] [ "%%" ] command); 12 | 13 | group = rec { 14 | name = "plasma-manager-commands"; 15 | desktop = "${name}.desktop"; 16 | description = "Plasma Manager"; 17 | }; 18 | 19 | commandType = 20 | { name, ... }: 21 | { 22 | options = { 23 | name = lib.mkOption { 24 | type = lib.types.str; 25 | default = name; 26 | description = "Command hotkey name."; 27 | }; 28 | 29 | comment = lib.mkOption { 30 | type = lib.types.str; 31 | default = name; 32 | description = "Optional comment to display in the System Settings app."; 33 | }; 34 | 35 | key = lib.mkOption { 36 | type = lib.types.str; 37 | description = "The key combination that triggers the action."; 38 | default = ""; 39 | }; 40 | 41 | keys = lib.mkOption { 42 | type = with lib.types; listOf str; 43 | description = "The key combinations that trigger the action."; 44 | default = [ ]; 45 | }; 46 | 47 | command = lib.mkOption { 48 | type = lib.types.str; 49 | description = "The command to execute."; 50 | }; 51 | 52 | logs = { 53 | enabled = lib.mkOption { 54 | type = lib.types.bool; 55 | default = true; 56 | description = "Connect the command's `stdin` and `stdout` to the systemd journal with `systemd-cat`."; 57 | }; 58 | 59 | identifier = lib.mkOption { 60 | type = lib.types.str; 61 | default = lib.trivial.pipe name [ 62 | lib.strings.toLower 63 | (builtins.replaceStrings [ " " ] [ "-" ]) 64 | (n: "${group.name}-${n}") 65 | ]; 66 | description = "Identifier passed down to `systemd-cat`."; 67 | }; 68 | 69 | extraArgs = lib.mkOption { 70 | type = lib.types.str; 71 | default = ""; 72 | description = "Additional arguments provided to `systemd-cat`."; 73 | }; 74 | }; 75 | }; 76 | }; 77 | in 78 | { 79 | options.programs.plasma.hotkeys = { 80 | commands = lib.mkOption { 81 | type = with lib.types; attrsOf (submodule commandType); 82 | default = { }; 83 | description = "Commands triggered by a keyboard shortcut."; 84 | }; 85 | }; 86 | 87 | config = lib.mkIf (cfg.enable && builtins.length (builtins.attrNames cfg.hotkeys.commands) != 0) { 88 | xdg.desktopEntries."${group.name}" = { 89 | name = group.description; 90 | noDisplay = true; 91 | type = "Application"; 92 | actions = lib.mapAttrs (_: command: { 93 | name = command.name; 94 | exec = 95 | if command.logs.enabled then 96 | "${pkgs.systemd}/bin/systemd-cat --identifier=${command.logs.identifier} ${command.logs.extraArgs} ${commandString command.command}" 97 | else 98 | (commandString command.command); 99 | }) cfg.hotkeys.commands; 100 | }; 101 | 102 | programs.plasma.configFile."kglobalshortcutsrc"."${group.desktop}" = 103 | { 104 | _k_friendly_name = group.description; 105 | } 106 | // lib.mapAttrs ( 107 | _: command: 108 | let 109 | keys = command.keys ++ lib.optionals (command.key != "") [ command.key ]; 110 | in 111 | lib.concatStringsSep "," [ 112 | (lib.concatStringsSep "\t" (map (lib.escape [ "," ]) keys)) 113 | "" # List of default keys, not needed. 114 | command.comment 115 | ] 116 | ) cfg.hotkeys.commands; 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /modules/krunner.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | let 3 | cfg = config.programs.plasma; 4 | in 5 | { 6 | options.programs.plasma.krunner = { 7 | position = lib.mkOption { 8 | type = 9 | with lib.types; 10 | nullOr (enum [ 11 | "top" 12 | "center" 13 | ]); 14 | default = null; 15 | example = "center"; 16 | description = "Set KRunner's position on the screen."; 17 | }; 18 | activateWhenTypingOnDesktop = lib.mkOption { 19 | type = with lib.types; nullOr bool; 20 | default = null; 21 | example = true; 22 | description = "Whether to activate KRunner when typing on the desktop."; 23 | }; 24 | historyBehavior = lib.mkOption { 25 | type = 26 | with lib.types; 27 | nullOr (enum [ 28 | "disabled" 29 | "enableSuggestions" 30 | "enableAutoComplete" 31 | ]); 32 | default = null; 33 | example = "disabled"; 34 | description = "Set the behavior of KRunner’s history."; 35 | }; 36 | 37 | shortcuts = { 38 | launch = lib.mkOption { 39 | type = with lib.types; nullOr (either str (listOf str)); 40 | default = null; 41 | example = "Meta"; 42 | description = "Set the shortcut to launch KRunner."; 43 | }; 44 | runCommandOnClipboard = lib.mkOption { 45 | type = with lib.types; nullOr (either str (listOf str)); 46 | default = null; 47 | example = "Meta+Shift"; 48 | description = "Set the shortcut to run the command on the clipboard contents."; 49 | }; 50 | }; 51 | }; 52 | 53 | config.programs.plasma = { 54 | configFile.krunnerrc = lib.mkMerge [ 55 | (lib.mkIf (cfg.krunner.position != null) { 56 | General.FreeFloating = cfg.krunner.position == "center"; 57 | }) 58 | (lib.mkIf (cfg.krunner.activateWhenTypingOnDesktop != null) { 59 | General.ActivateWhenTypingOnDesktop = cfg.krunner.activateWhenTypingOnDesktop; 60 | }) 61 | (lib.mkIf (cfg.krunner.historyBehavior != null) { 62 | General.historyBehavior = ( 63 | if cfg.krunner.historyBehavior == "enableSuggestions" then 64 | "CompletionSuggestion" 65 | else if cfg.krunner.historyBehavior == "enableAutoComplete" then 66 | "ImmediateCompletion" 67 | else 68 | "Disabled" 69 | ); 70 | }) 71 | ]; 72 | 73 | shortcuts."services/org.kde.krunner.desktop" = lib.mkMerge [ 74 | (lib.mkIf (cfg.krunner.shortcuts.launch != null) { 75 | _launch = cfg.krunner.shortcuts.launch; 76 | }) 77 | 78 | (lib.mkIf (cfg.krunner.shortcuts.runCommandOnClipboard != null) { 79 | RunClipboard = cfg.krunner.shortcuts.runCommandOnClipboard; 80 | }) 81 | ]; 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /modules/kscreenlocker.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | let 3 | cfg = config.programs.plasma; 4 | inherit (import ../lib/wallpapers.nix { inherit lib; }) 5 | wallpaperPictureOfTheDayType 6 | wallpaperSlideShowType 7 | ; 8 | in 9 | { 10 | options.programs.plasma.kscreenlocker = { 11 | autoLock = lib.mkOption { 12 | type = with lib.types; nullOr bool; 13 | default = null; 14 | example = true; 15 | description = '' 16 | Whether the screen will be locked after the specified time. 17 | ''; 18 | }; 19 | lockOnResume = lib.mkOption { 20 | type = with lib.types; nullOr bool; 21 | default = null; 22 | example = false; 23 | description = '' 24 | Whether to lock the screen when the system resumes from sleep. 25 | ''; 26 | }; 27 | 28 | timeout = lib.mkOption { 29 | type = with lib.types; nullOr ints.unsigned; 30 | default = null; 31 | example = 5; 32 | description = '' 33 | Sets the timeout in minutes after which the screen will be locked. 34 | ''; 35 | }; 36 | 37 | passwordRequired = lib.mkOption { 38 | type = with lib.types; nullOr bool; 39 | default = null; 40 | example = true; 41 | description = '' 42 | Whether the user password is required to unlock the screen. 43 | ''; 44 | }; 45 | 46 | passwordRequiredDelay = lib.mkOption { 47 | type = with lib.types; nullOr ints.unsigned; 48 | default = null; 49 | example = 5; 50 | description = '' 51 | The time it takes in seconds for the password to be required after the screen is locked. 52 | ''; 53 | }; 54 | 55 | lockOnStartup = lib.mkOption { 56 | type = with lib.types; nullOr bool; 57 | default = null; 58 | example = false; 59 | description = '' 60 | Whether to lock the screen on startup. 61 | 62 | **Note:** This option is not provided in the System Settings app. 63 | ''; 64 | }; 65 | 66 | appearance = { 67 | alwaysShowClock = lib.mkOption { 68 | type = with lib.types; nullOr bool; 69 | default = null; 70 | example = false; 71 | description = '' 72 | Whether to always show the clock on the lockscreen, even if the unlock dialog is not shown. 73 | ''; 74 | }; 75 | showMediaControls = lib.mkOption { 76 | type = with lib.types; nullOr bool; 77 | default = null; 78 | example = false; 79 | description = '' 80 | Whether to show media controls on the lockscreen. 81 | ''; 82 | }; 83 | 84 | wallpaper = lib.mkOption { 85 | type = with lib.types; nullOr path; 86 | default = null; 87 | example = lib.literalExpression ''"''${pkgs.kdePackages.plasma-workspace-wallpapers}/share/wallpapers/Kay/contents/images/1080x1920.png"''; 88 | description = '' 89 | The wallpaper for the lockscreen. Can be either the path to an image file or a KPackage. 90 | ''; 91 | }; 92 | wallpaperPictureOfTheDay = lib.mkOption { 93 | type = lib.types.nullOr wallpaperPictureOfTheDayType; 94 | default = null; 95 | example = { 96 | provider = "apod"; 97 | }; 98 | description = '' 99 | Which plugin to fetch the Picture of the Day from. 100 | ''; 101 | }; 102 | wallpaperSlideShow = lib.mkOption { 103 | type = lib.types.nullOr wallpaperSlideShowType; 104 | default = null; 105 | example = lib.literalExpression ''{ path = "''${pkgs.kdePackages.plasma-workspace-wallpapers}/share/wallpapers/"; }''; 106 | description = '' 107 | Allows you to set the wallpaper using the slideshow plugin. Needs the path 108 | to at least one directory with wallpaper images. 109 | ''; 110 | }; 111 | wallpaperPlainColor = lib.mkOption { 112 | type = lib.types.nullOr lib.types.str; 113 | default = null; 114 | example = "0,64,174,256"; 115 | description = '' 116 | Set the wallpaper using a plain color. Color is a comma-seperated R,G,B,A string. The alpha is optional (default is 256). 117 | ''; 118 | }; 119 | }; 120 | }; 121 | 122 | imports = [ 123 | (lib.mkRenamedOptionModule 124 | [ 125 | "programs" 126 | "plasma" 127 | "kscreenlocker" 128 | "wallpaper" 129 | ] 130 | [ 131 | "programs" 132 | "plasma" 133 | "kscreenlocker" 134 | "appearance" 135 | "wallpaper" 136 | ] 137 | ) 138 | (lib.mkRenamedOptionModule 139 | [ 140 | "programs" 141 | "plasma" 142 | "kscreenlocker" 143 | "wallpaperPictureOfTheDay" 144 | ] 145 | [ 146 | "programs" 147 | "plasma" 148 | "kscreenlocker" 149 | "appearance" 150 | "wallpaperPictureOfTheDay" 151 | ] 152 | ) 153 | (lib.mkRenamedOptionModule 154 | [ 155 | "programs" 156 | "plasma" 157 | "kscreenlocker" 158 | "wallpaperSlideShow" 159 | ] 160 | [ 161 | "programs" 162 | "plasma" 163 | "kscreenlocker" 164 | "appearance" 165 | "wallpaperSlideShow" 166 | ] 167 | ) 168 | (lib.mkRenamedOptionModule 169 | [ 170 | "programs" 171 | "plasma" 172 | "kscreenlocker" 173 | "wallpaperPlainColor" 174 | ] 175 | [ 176 | "programs" 177 | "plasma" 178 | "kscreenlocker" 179 | "appearance" 180 | "wallpaperPlainColor" 181 | ] 182 | ) 183 | ]; 184 | 185 | config = { 186 | assertions = [ 187 | { 188 | assertion = 189 | let 190 | wallpapers = with cfg.kscreenlocker.appearance; [ 191 | wallpaperSlideShow 192 | wallpaper 193 | wallpaperPictureOfTheDay 194 | wallpaperPlainColor 195 | ]; 196 | in 197 | lib.count (x: x != null) wallpapers <= 1; 198 | message = "Can set only one of wallpaper, wallpaperSlideShow, wallpaperPictureOfTheDay, and wallpaperPlainColor for kscreenlocker."; 199 | } 200 | ]; 201 | programs.plasma.configFile.kscreenlockerrc = ( 202 | lib.mkMerge [ 203 | (lib.mkIf (cfg.kscreenlocker.appearance.wallpaper != null) { 204 | Greeter.WallpaperPlugin = "org.kde.image"; 205 | "Greeter/Wallpaper/org.kde.image/General".Image = ( 206 | builtins.toString cfg.kscreenlocker.appearance.wallpaper 207 | ); 208 | }) 209 | (lib.mkIf (cfg.kscreenlocker.appearance.wallpaperPictureOfTheDay != null) { 210 | Greeter.WallpaperPlugin = "org.kde.potd"; 211 | "Greeter/Wallpaper/org.kde.potd/General" = { 212 | Provider = cfg.kscreenlocker.appearance.wallpaperPictureOfTheDay.provider; 213 | UpdateOverMeteredConnection = 214 | with cfg.kscreenlocker.appearance.wallpaperPictureOfTheDay; 215 | (lib.mkIf (updateOverMeteredConnection != null) (if updateOverMeteredConnection then 1 else 0)); 216 | }; 217 | }) 218 | (lib.mkIf (cfg.kscreenlocker.appearance.wallpaperSlideShow != null) { 219 | Greeter.WallpaperPlugin = "org.kde.slideshow"; 220 | "Greeter/Wallpaper/org.kde.slideshow/General" = { 221 | SlidePaths = 222 | with cfg.kscreenlocker.appearance.wallpaperSlideShow; 223 | ( 224 | if ((builtins.isPath path) || (builtins.isString path)) then 225 | (builtins.toString cfg.kscreenlocker.appearance.wallpaperSlideShow.path) 226 | else 227 | (builtins.concatStringsSep "," cfg.kscreenlocker.appearance.wallpaperSlideShow.path) 228 | ); 229 | SlideInterval = cfg.kscreenlocker.appearance.wallpaperSlideShow.interval; 230 | }; 231 | }) 232 | (lib.mkIf (cfg.kscreenlocker.appearance.wallpaperPlainColor != null) { 233 | Greeter.WallpaperPlugin = "org.kde.color"; 234 | "Greeter/Wallpaper/org.kde.color/General".Color = cfg.kscreenlocker.appearance.wallpaperPlainColor; 235 | }) 236 | 237 | (lib.mkIf (cfg.kscreenlocker.appearance.alwaysShowClock != null) { 238 | "Greeter/LnF/General".alwaysShowClock = cfg.kscreenlocker.appearance.alwaysShowClock; 239 | }) 240 | (lib.mkIf (cfg.kscreenlocker.appearance.showMediaControls != null) { 241 | "Greeter/LnF/General".showMediaControls = cfg.kscreenlocker.appearance.showMediaControls; 242 | }) 243 | 244 | (lib.mkIf (cfg.kscreenlocker.autoLock != null) { Daemon.Autolock = cfg.kscreenlocker.autoLock; }) 245 | 246 | (lib.mkIf (cfg.kscreenlocker.lockOnResume != null) { 247 | Daemon.LockOnResume = cfg.kscreenlocker.lockOnResume; 248 | }) 249 | 250 | (lib.mkIf (cfg.kscreenlocker.timeout != null) { Daemon.Timeout = cfg.kscreenlocker.timeout; }) 251 | 252 | (lib.mkIf (cfg.kscreenlocker.passwordRequiredDelay != null) { 253 | Daemon.LockGrace = cfg.kscreenlocker.passwordRequiredDelay; 254 | }) 255 | 256 | (lib.mkIf (cfg.kscreenlocker.passwordRequired != null) { 257 | Daemon.RequirePassword = cfg.kscreenlocker.passwordRequired; 258 | }) 259 | 260 | (lib.mkIf (cfg.kscreenlocker.lockOnStartup != null) { 261 | Daemon.LockOnStart = cfg.kscreenlocker.lockOnStartup; 262 | }) 263 | ] 264 | ); 265 | }; 266 | } 267 | -------------------------------------------------------------------------------- /modules/session.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | let 3 | cfg = config.programs.plasma; 4 | in 5 | { 6 | options.programs.plasma.session = { 7 | general = { 8 | askForConfirmationOnLogout = lib.mkOption { 9 | type = with lib.types; nullOr bool; 10 | default = null; 11 | example = true; 12 | description = "Whether to ask for confirmation when shutting down, restarting or logging out"; 13 | }; 14 | }; 15 | sessionRestore = { 16 | restoreOpenApplicationsOnLogin = 17 | let 18 | options = { 19 | onLastLogout = "restorePreviousLogout"; 20 | whenSessionWasManuallySaved = "restoreSavedSession"; 21 | startWithEmptySession = "emptySession"; 22 | }; 23 | in 24 | lib.mkOption { 25 | type = with lib.types; nullOr (enum (builtins.attrNames options)); 26 | default = null; 27 | example = "startWithEmptySession"; 28 | description = '' 29 | Controls how applications are restored on login: 30 | - "onLastLogout": Restores applications that were open during the last logout. 31 | - "whenSessionWasManuallySaved": Restores applications based on a manually saved session. 32 | - "startWithEmptySession": Starts with a clean, empty session each time. 33 | ''; 34 | apply = option: if option == null then null else options.${option}; 35 | }; 36 | excludeApplications = lib.mkOption { 37 | type = with lib.types; nullOr (listOf str); 38 | default = null; 39 | example = [ 40 | "firefox" 41 | "xterm" 42 | ]; 43 | description = "List of applications to exclude from session restore"; 44 | apply = apps: if apps == null then null else builtins.concatStringsSep "," apps; 45 | }; 46 | }; 47 | }; 48 | 49 | config.programs.plasma.configFile."ksmserverrc".General = lib.mkMerge [ 50 | (lib.mkIf (cfg.session.general.askForConfirmationOnLogout != null) { 51 | confirmLogout = cfg.session.general.askForConfirmationOnLogout; 52 | }) 53 | (lib.mkIf (cfg.session.sessionRestore.excludeApplications != null) { 54 | excludeApps = cfg.session.sessionRestore.excludeApplications; 55 | }) 56 | (lib.mkIf (cfg.session.sessionRestore.restoreOpenApplicationsOnLogin != null) { 57 | loginMode = cfg.session.sessionRestore.restoreOpenApplicationsOnLogin; 58 | }) 59 | ]; 60 | } 61 | -------------------------------------------------------------------------------- /modules/shortcuts.nix: -------------------------------------------------------------------------------- 1 | # Global keyboard shortcuts: 2 | { config, lib, ... }: 3 | 4 | let 5 | cfg = config.programs.plasma; 6 | 7 | # Checks if the shortcut is in the "service" group, in which case we need to 8 | # write the values a little differently. 9 | isService = 10 | group: 11 | let 12 | startString = "services/"; 13 | in 14 | (builtins.substring 0 (builtins.stringLength startString) group) == startString; 15 | 16 | # Convert one shortcut into a settings attribute set. 17 | shortcutToConfigValue = 18 | group: _action: skey: 19 | let 20 | # Keys are expected to be a list: 21 | keys = 22 | if builtins.isList skey then 23 | (if ((builtins.length skey) == 0) then [ "none" ] else skey) 24 | else 25 | [ skey ]; 26 | 27 | # Don't allow un-escaped commas: 28 | escape = lib.escape [ "," ]; 29 | keysStr = ( 30 | if ((builtins.length keys) == 1) then 31 | (escape (builtins.head keys)) 32 | else 33 | builtins.concatStringsSep "\t" (map escape keys) 34 | ); 35 | in 36 | ( 37 | if (isService group) then 38 | keysStr 39 | else 40 | (lib.concatStringsSep "," [ 41 | keysStr 42 | "" # List of default keys, not needed. 43 | "" # Display string, not needed. 44 | ]) 45 | ); 46 | 47 | shortcutsToSettings = 48 | groups: lib.mapAttrs (group: attrs: (lib.mapAttrs (shortcutToConfigValue group) attrs)) groups; 49 | in 50 | { 51 | options.programs.plasma.shortcuts = lib.mkOption { 52 | type = 53 | with lib.types; 54 | attrsOf ( 55 | attrsOf (oneOf [ 56 | (listOf str) 57 | str 58 | ]) 59 | ); 60 | default = { }; 61 | description = '' 62 | An attribute set where the keys are application groups and the 63 | values are shortcuts. 64 | ''; 65 | }; 66 | 67 | config = lib.mkIf cfg.enable { 68 | programs.plasma.configFile."kglobalshortcutsrc" = shortcutsToSettings cfg.shortcuts; 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /modules/spectacle.nix: -------------------------------------------------------------------------------- 1 | { config, lib, ... }: 2 | 3 | let 4 | cfg = config.programs.plasma; 5 | in 6 | { 7 | options.programs.plasma.spectacle.shortcuts = { 8 | captureActiveWindow = lib.mkOption { 9 | type = 10 | with lib.types; 11 | nullOr (oneOf [ 12 | (listOf str) 13 | str 14 | ]); 15 | default = null; 16 | example = "Meta+Print"; 17 | description = '' 18 | The shortcut for capturing the active window. 19 | ''; 20 | }; 21 | 22 | captureCurrentMonitor = lib.mkOption { 23 | type = 24 | with lib.types; 25 | nullOr (oneOf [ 26 | (listOf str) 27 | str 28 | ]); 29 | default = null; 30 | example = "Print"; 31 | description = '' 32 | The shortcut for capturing the current monitor. 33 | ''; 34 | }; 35 | 36 | captureEntireDesktop = lib.mkOption { 37 | type = 38 | with lib.types; 39 | nullOr (oneOf [ 40 | (listOf str) 41 | str 42 | ]); 43 | default = null; 44 | example = "Shift+Print"; 45 | description = '' 46 | The shortcut for capturing the entire desktop. 47 | ''; 48 | }; 49 | 50 | captureRectangularRegion = lib.mkOption { 51 | type = 52 | with lib.types; 53 | nullOr (oneOf [ 54 | (listOf str) 55 | str 56 | ]); 57 | default = null; 58 | example = "Meta+Shift+S"; 59 | description = '' 60 | The shortcut for capturing a rectangular region. 61 | ''; 62 | }; 63 | 64 | captureWindowUnderCursor = lib.mkOption { 65 | type = 66 | with lib.types; 67 | nullOr (oneOf [ 68 | (listOf str) 69 | str 70 | ]); 71 | default = null; 72 | example = "Meta+Ctrl+Print"; 73 | description = '' 74 | The shortcut for capturing the window under the cursor. 75 | ''; 76 | }; 77 | 78 | launch = lib.mkOption { 79 | type = 80 | with lib.types; 81 | nullOr (oneOf [ 82 | (listOf str) 83 | str 84 | ]); 85 | default = null; 86 | example = "Meta+S"; 87 | description = '' 88 | The shortcut for launching Spectacle. 89 | ''; 90 | }; 91 | 92 | launchWithoutCapturing = lib.mkOption { 93 | type = 94 | with lib.types; 95 | nullOr (oneOf [ 96 | (listOf str) 97 | str 98 | ]); 99 | default = null; 100 | example = "Meta+Alt+S"; 101 | description = '' 102 | The shortcut for launching Spectacle without capturing. 103 | ''; 104 | }; 105 | 106 | recordRegion = lib.mkOption { 107 | type = 108 | with lib.types; 109 | nullOr (oneOf [ 110 | (listOf str) 111 | str 112 | ]); 113 | default = null; 114 | example = "Meta+Shift+R"; 115 | description = '' 116 | The shortcut for recording a region on the screen. 117 | ''; 118 | }; 119 | 120 | recordScreen = lib.mkOption { 121 | type = 122 | with lib.types; 123 | nullOr (oneOf [ 124 | (listOf str) 125 | str 126 | ]); 127 | default = null; 128 | example = "Meta+Alt+R"; 129 | description = '' 130 | The shortcut for selecting a screen to record. 131 | ''; 132 | }; 133 | 134 | recordWindow = lib.mkOption { 135 | type = 136 | with lib.types; 137 | nullOr (oneOf [ 138 | (listOf str) 139 | str 140 | ]); 141 | default = null; 142 | example = "Meta+Ctrl+R"; 143 | description = '' 144 | The shortcut for selecting a window to record. 145 | ''; 146 | }; 147 | }; 148 | 149 | config = lib.mkIf cfg.enable { 150 | programs.plasma.shortcuts."services/org.kde.spectacle.desktop" = lib.mkMerge [ 151 | (lib.mkIf (cfg.spectacle.shortcuts.captureActiveWindow != null) { 152 | ActiveWindowScreenShot = cfg.spectacle.shortcuts.captureActiveWindow; 153 | }) 154 | (lib.mkIf (cfg.spectacle.shortcuts.captureCurrentMonitor != null) { 155 | CurrentMonitorScreenShot = cfg.spectacle.shortcuts.captureCurrentMonitor; 156 | }) 157 | (lib.mkIf (cfg.spectacle.shortcuts.captureEntireDesktop != null) { 158 | FullScreenScreenShot = cfg.spectacle.shortcuts.captureEntireDesktop; 159 | }) 160 | (lib.mkIf (cfg.spectacle.shortcuts.captureRectangularRegion != null) { 161 | RectangularRegionScreenShot = cfg.spectacle.shortcuts.captureRectangularRegion; 162 | }) 163 | (lib.mkIf (cfg.spectacle.shortcuts.captureWindowUnderCursor != null) { 164 | WindowUnderCursorScreenShot = cfg.spectacle.shortcuts.captureWindowUnderCursor; 165 | }) 166 | (lib.mkIf (cfg.spectacle.shortcuts.launch != null) { _launch = cfg.spectacle.shortcuts.launch; }) 167 | (lib.mkIf (cfg.spectacle.shortcuts.launchWithoutCapturing != null) { 168 | OpenWithoutScreenshot = cfg.spectacle.shortcuts.launchWithoutCapturing; 169 | }) 170 | (lib.mkIf (cfg.spectacle.shortcuts.recordRegion != null) { 171 | RecordRegion = cfg.spectacle.shortcuts.recordRegion; 172 | }) 173 | (lib.mkIf (cfg.spectacle.shortcuts.recordScreen != null) { 174 | RecordScreen = cfg.spectacle.shortcuts.recordScreen; 175 | }) 176 | (lib.mkIf (cfg.spectacle.shortcuts.recordWindow != null) { 177 | RecordWindow = cfg.spectacle.shortcuts.recordWindow; 178 | }) 179 | ]; 180 | }; 181 | } 182 | -------------------------------------------------------------------------------- /modules/startup.nix: -------------------------------------------------------------------------------- 1 | # Allows to run commands/scripts at startup (this is used by some of the other 2 | # modules, which may need to do this, but can also be used on its own) 3 | { config, lib, ... }: 4 | let 5 | cfg = config.programs.plasma; 6 | topScriptName = "run_all.sh"; 7 | 8 | textOption = lib.mkOption { 9 | type = lib.types.str; 10 | description = "The content of the startup script."; 11 | }; 12 | priorityOption = lib.mkOption { 13 | type = (lib.types.ints.between 0 8); 14 | default = 0; 15 | description = "The priority for the execution of the script. Lower priority means earlier execution."; 16 | }; 17 | restartServicesOption = lib.mkOption { 18 | type = with lib.types; listOf str; 19 | default = [ ]; 20 | description = "Services to restart after the script has been run."; 21 | }; 22 | runAlwaysOption = lib.mkOption { 23 | type = lib.types.bool; 24 | default = false; 25 | example = true; 26 | description = '' 27 | When enabled the script will run even if no changes have been made 28 | since last successful run. 29 | ''; 30 | }; 31 | 32 | startupScriptType = lib.types.submodule { 33 | options = { 34 | text = textOption; 35 | priority = priorityOption; 36 | restartServices = restartServicesOption; 37 | runAlways = runAlwaysOption; 38 | }; 39 | }; 40 | desktopScriptType = lib.types.submodule { 41 | options = { 42 | text = textOption; 43 | priority = priorityOption; 44 | restartServices = restartServicesOption; 45 | runAlways = runAlwaysOption; 46 | preCommands = lib.mkOption { 47 | type = lib.types.str; 48 | description = "Commands to run before the desktop script lines."; 49 | default = ""; 50 | }; 51 | postCommands = lib.mkOption { 52 | type = lib.types.str; 53 | description = "Commands to run after the desktop script lines."; 54 | default = ""; 55 | }; 56 | }; 57 | }; 58 | 59 | createScriptContentRunOnce = name: sha256sumFile: script: text: '' 60 | last_update="$(sha256sum ${sha256sumFile})" 61 | last_update_file=${config.xdg.dataHome}/plasma-manager/last_run_${name} 62 | if [ -f "$last_update_file" ]; then 63 | stored_last_update=$(cat "$last_update_file") 64 | fi 65 | 66 | if ! [ "$last_update" = "$stored_last_update" ]; then 67 | echo "Running script: ${name}" 68 | success=1 69 | trap 'success=0' ERR 70 | ${text} 71 | if [ $success -eq 1 ]; then 72 | echo "$last_update" > "$last_update_file" 73 | ${ 74 | builtins.concatStringsSep "\n" ( 75 | map ( 76 | s: "echo ${s} >> ${config.xdg.dataHome}/plasma-manager/services_to_restart" 77 | ) script.restartServices 78 | ) 79 | } 80 | fi 81 | fi 82 | ''; 83 | 84 | createScriptContentRunAlways = name: text: '' 85 | echo "Running script: ${name}" 86 | ${text} 87 | ''; 88 | 89 | createScriptContent = name: sha256sumFile: script: text: { 90 | "plasma-manager/${cfg.startup.scriptsDir}/${builtins.toString script.priority}_${name}.sh" = { 91 | text = '' 92 | #!/bin/sh 93 | ${ 94 | if script.runAlways then 95 | (createScriptContentRunAlways name text) 96 | else 97 | (createScriptContentRunOnce name sha256sumFile script text) 98 | } 99 | ''; 100 | executable = true; 101 | }; 102 | }; 103 | in 104 | { 105 | options.programs.plasma.startup = { 106 | startupScript = lib.mkOption { 107 | type = lib.types.attrsOf startupScriptType; 108 | default = { }; 109 | description = "Commands/scripts to be run at startup."; 110 | }; 111 | desktopScript = lib.mkOption { 112 | type = lib.types.attrsOf desktopScriptType; 113 | default = { }; 114 | description = '' 115 | Plasma desktop scripts to be run exactly once at startup. See 116 | the [KDE Documentation](https://develop.kde.org/docs/plasma/scripting) for details on Plasma 117 | desktop scripts. 118 | ''; 119 | }; 120 | dataFile = lib.mkOption { 121 | type = with lib.types; attrsOf str; 122 | default = { }; 123 | description = "Datafiles, typically for use in autostart scripts."; 124 | }; 125 | scriptsDir = lib.mkOption { 126 | type = lib.types.str; 127 | default = "scripts"; 128 | description = "The name of the subdirectory where the scripts should be."; 129 | }; 130 | dataDir = lib.mkOption { 131 | type = lib.types.str; 132 | default = "data"; 133 | description = "The name of the subdirectory where the datafiles should be."; 134 | }; 135 | }; 136 | 137 | config.xdg = 138 | lib.mkIf 139 | ( 140 | cfg.enable 141 | && ( 142 | builtins.length (builtins.attrNames cfg.startup.startupScript) != 0 143 | || (builtins.length (builtins.attrNames cfg.startup.desktopScript)) != 0 144 | ) 145 | ) 146 | { 147 | dataFile = lib.mkMerge [ 148 | # Autostart scripts 149 | (lib.mkMerge ( 150 | lib.mapAttrsToList ( 151 | name: script: createScriptContent "script_${name}" "$0" script script.text 152 | ) cfg.startup.startupScript 153 | )) 154 | # Desktop scripts 155 | (lib.mkMerge ( 156 | (lib.mapAttrsToList ( 157 | name: script: 158 | let 159 | layoutScriptPath = "${config.xdg.dataHome}/plasma-manager/${cfg.startup.dataDir}/desktop_script_${name}.js"; 160 | in 161 | createScriptContent "desktop_script_${name}" layoutScriptPath script '' 162 | ${script.preCommands} 163 | qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "$(cat ${layoutScriptPath})" 164 | ${script.postCommands} 165 | '' 166 | ) cfg.startup.desktopScript) 167 | ++ (lib.mapAttrsToList (name: content: { 168 | "plasma-manager/${cfg.startup.dataDir}/desktop_script_${name}.js" = { 169 | text = content.text; 170 | }; 171 | }) cfg.startup.desktopScript) 172 | )) 173 | # Datafiles 174 | (lib.mkMerge ( 175 | lib.mapAttrsToList (name: content: { 176 | "plasma-manager/${cfg.startup.dataDir}/${name}" = { 177 | text = content; 178 | }; 179 | }) cfg.startup.dataFile 180 | )) 181 | # Autostart script runner 182 | { 183 | "plasma-manager/${topScriptName}" = { 184 | text = '' 185 | #!/bin/sh 186 | 187 | services_restart_file="${config.xdg.dataHome}/plasma-manager/services_to_restart" 188 | 189 | # Reset the file keeping track of which scripts to restart. 190 | # Technically can be put at the end as well (maybe better, at 191 | # least assuming the file hasn't been tampered with of some sort). 192 | if [ -f $services_restart_file ]; then rm $services_restart_file; fi 193 | 194 | for script in ${config.xdg.dataHome}/plasma-manager/${cfg.startup.scriptsDir}/*.sh; do 195 | [ -x "$script" ] && $script 196 | done 197 | 198 | # Restart the services 199 | if [ -f $services_restart_file ]; then 200 | for service in $(sort $services_restart_file | uniq); do 201 | systemctl --user restart $service 202 | done 203 | fi 204 | ''; 205 | executable = true; 206 | }; 207 | } 208 | ]; 209 | 210 | configFile."autostart/plasma-manager-autostart.desktop".text = '' 211 | [Desktop Entry] 212 | Type=Application 213 | Name=Plasma Manager theme application 214 | Exec=${config.xdg.dataHome}/plasma-manager/${topScriptName} 215 | X-KDE-autostart-condition=ksmserver 216 | ''; 217 | }; 218 | 219 | # Due to the fact that running certain desktop-scripts can reset what has 220 | # been applied by other desktop-script (for example running the panel 221 | # desktop-script will reset the wallpaper), we make it so that if any of the 222 | # desktop-scripts have been modified, that we must re-run all the 223 | # desktop-scripts, not just the ones who have been changed. 224 | config.programs.plasma.startup.startupScript."reset_lastrun_desktopscripts" = 225 | lib.mkIf (cfg.startup.desktopScript != { }) 226 | { 227 | text = '' 228 | should_reset=0 229 | for ds in ${config.xdg.dataHome}/plasma-manager/data/desktop_script_*.js; do 230 | ds_name="$(basename $ds)" 231 | ds_name="''${ds_name%.js}" 232 | ds_shafile="${config.xdg.dataHome}/plasma-manager/last_run_"$ds_name 233 | 234 | if ! [ -f "$ds_shafile" ]; then 235 | echo "Resetting desktop-script last_run-files since $ds_name is a new desktop-script" 236 | should_reset=1 237 | elif ! [ "$(cat $ds_shafile)" = "$(sha256sum $ds)" ]; then 238 | echo "Resetting desktop-script last_run-files since $ds_name has changed content" 239 | should_reset=1 240 | fi 241 | done 242 | 243 | [ $should_reset = 1 ] && rm ${config.xdg.dataHome}/plasma-manager/last_run_desktop_script_* 244 | ''; 245 | runAlways = true; 246 | }; 247 | } 248 | -------------------------------------------------------------------------------- /modules/widgets/app-menu.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (import ./lib.nix { inherit lib; }) configValueType; 4 | inherit (import ./default.nix { inherit lib; }) positionType sizeType; 5 | 6 | mkBoolOption = 7 | description: 8 | lib.mkOption { 9 | type = with lib.types; nullOr bool; 10 | default = null; 11 | example = true; 12 | inherit description; 13 | }; 14 | in 15 | { 16 | appMenu = { 17 | opts = { 18 | position = lib.mkOption { 19 | type = positionType; 20 | example = { 21 | horizontal = 100; 22 | vertical = 300; 23 | }; 24 | description = "The position of the widget. (Only for desktop widget)"; 25 | }; 26 | size = lib.mkOption { 27 | type = sizeType; 28 | example = { 29 | width = 500; 30 | height = 50; 31 | }; 32 | description = "The size of the widget. (Only for desktop widget)"; 33 | }; 34 | compactView = mkBoolOption "Whether to show the app menu in a compact view"; 35 | settings = lib.mkOption { 36 | type = configValueType; 37 | default = null; 38 | example = { 39 | Appearance = { 40 | compactView = true; 41 | }; 42 | }; 43 | description = '' 44 | Extra configuration for the widget 45 | ''; 46 | apply = settings: if settings == null then { } else settings; 47 | }; 48 | }; 49 | 50 | convert = 51 | { compactView, settings, ... }: 52 | { 53 | name = "org.kde.plasma.appmenu"; 54 | config = lib.recursiveUpdate { 55 | General = lib.filterAttrs (_: v: v != null) { inherit compactView; }; 56 | } settings; 57 | }; 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /modules/widgets/battery.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (import ./lib.nix { inherit lib; }) configValueType; 4 | inherit (import ./default.nix { inherit lib; }) positionType sizeType; 5 | in 6 | { 7 | battery = { 8 | description = "The battery indicator widget."; 9 | 10 | # See https://invent.kde.org/plasma/plasma-workspace/-/blob/master/applets/batterymonitor/package/contents/config/main.xml for the accepted raw options 11 | opts = { 12 | position = lib.mkOption { 13 | type = positionType; 14 | example = { 15 | horizontal = 250; 16 | vertical = 50; 17 | }; 18 | description = "The position of the widget. (Only for desktop widget)"; 19 | }; 20 | size = lib.mkOption { 21 | type = sizeType; 22 | example = { 23 | width = 500; 24 | height = 500; 25 | }; 26 | description = "The size of the widget. (Only for desktop widget)"; 27 | }; 28 | showPercentage = lib.mkOption { 29 | type = with lib.types; nullOr bool; 30 | default = null; 31 | example = true; 32 | description = "Enable to show the battery percentage as a small label over the battery icon."; 33 | }; 34 | settings = lib.mkOption { 35 | type = configValueType; 36 | default = null; 37 | example = { 38 | General = { 39 | showPercentage = true; 40 | }; 41 | }; 42 | apply = settings: if settings == null then { } else settings; 43 | }; 44 | }; 45 | 46 | convert = 47 | { showPercentage, settings, ... }: 48 | { 49 | name = "org.kde.plasma.battery"; 50 | config = lib.recursiveUpdate { 51 | General = lib.filterAttrs (_: v: v != null) { inherit showPercentage; }; 52 | } settings; 53 | }; 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /modules/widgets/default.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }@args: 2 | let 3 | args' = args // { 4 | widgets = self; 5 | }; 6 | 7 | sources = lib.attrsets.mergeAttrsList ( 8 | map (s: import s args') [ 9 | ./app-menu.nix 10 | ./application-title-bar.nix 11 | ./battery.nix 12 | ./digital-clock.nix 13 | ./icon-tasks.nix 14 | ./keyboard-layout.nix 15 | ./kicker.nix 16 | ./kickerdash.nix 17 | ./kickoff.nix 18 | ./pager.nix 19 | ./panel-spacer.nix 20 | ./plasma-panel-colorizer.nix 21 | ./plasmusic-toolbar.nix 22 | ./system-monitor.nix 23 | ./system-tray.nix 24 | ] 25 | ); 26 | 27 | positionType = lib.types.submodule { 28 | options = { 29 | horizontal = lib.mkOption { 30 | type = lib.types.ints.unsigned; 31 | example = 500; 32 | description = "The horizontal position of the widget."; 33 | }; 34 | vertical = lib.mkOption { 35 | type = lib.types.ints.unsigned; 36 | example = 500; 37 | description = "The vertical position of the widget."; 38 | }; 39 | }; 40 | }; 41 | 42 | sizeType = lib.types.submodule { 43 | options = { 44 | width = lib.mkOption { 45 | type = lib.types.ints.unsigned; 46 | example = 500; 47 | description = "The width of the widget."; 48 | }; 49 | height = lib.mkOption { 50 | type = lib.types.ints.unsigned; 51 | example = 500; 52 | description = "The height of the widget."; 53 | }; 54 | }; 55 | }; 56 | 57 | compositeWidgetType = lib.pipe sources [ 58 | (builtins.mapAttrs ( 59 | _: s: 60 | lib.mkOption { 61 | inherit (s) description; 62 | type = lib.types.submodule (submoduleArgs: { 63 | options = if builtins.isFunction s.opts then s.opts submoduleArgs else s.opts; 64 | }); 65 | } 66 | )) 67 | lib.types.attrTag 68 | ]; 69 | 70 | simpleWidgetType = lib.types.submodule { 71 | options = { 72 | name = lib.mkOption { 73 | type = lib.types.str; 74 | example = "org.kde.plasma.kickoff"; 75 | description = "The name of the widget to add."; 76 | }; 77 | config = lib.mkOption { 78 | type = (import ./lib.nix (args // { widgets = self; })).configValueType; 79 | default = null; 80 | example = { 81 | General.icon = "nix-snowflake-white"; 82 | }; 83 | description = '' 84 | Configuration options for the widget. 85 | 86 | See https://develop.kde.org/docs/plasma/scripting/keys/ for an (incomplete) list of options 87 | that can be set here. 88 | ''; 89 | }; 90 | extraConfig = lib.mkOption { 91 | type = lib.types.lines; 92 | default = ""; 93 | example = '' 94 | (widget) => { 95 | widget.currentConfigGroup = ["General"]; 96 | widget.writeConfig("title", "My widget"); 97 | } 98 | ''; 99 | description = '' 100 | Extra configuration for the widget in JavaScript. 101 | 102 | Should be a lambda/anonymous function that takes the widget as its sole argument, 103 | which can then be called by the script. 104 | ''; 105 | }; 106 | }; 107 | }; 108 | 109 | desktopSimpleWidgetType = lib.types.submodule { 110 | options = { 111 | name = lib.mkOption { 112 | type = lib.types.str; 113 | example = "org.kde.plasma.kickoff"; 114 | description = "The name of the widget to add."; 115 | }; 116 | position = lib.mkOption { 117 | type = positionType; 118 | example = { 119 | horizontal = 500; 120 | vertical = 500; 121 | }; 122 | description = "The position of the widget."; 123 | }; 124 | size = lib.mkOption { 125 | type = sizeType; 126 | example = { 127 | width = 500; 128 | height = 500; 129 | }; 130 | description = "The size of the widget."; 131 | }; 132 | config = lib.mkOption { 133 | type = (import ./lib.nix (args // { widgets = self; })).configValueType; 134 | default = null; 135 | example = { 136 | General.icon = "nix-snowflake-white"; 137 | }; 138 | description = '' 139 | Configuration options for the widget. 140 | 141 | See https://develop.kde.org/docs/plasma/scripting/keys/ for an (incomplete) list of options 142 | that can be set here. 143 | ''; 144 | }; 145 | extraConfig = lib.mkOption { 146 | type = lib.types.lines; 147 | default = ""; 148 | example = '' 149 | (widget) => { 150 | widget.currentConfigGroup = ["General"]; 151 | widget.writeConfig("title", "My widget"); 152 | } 153 | ''; 154 | description = '' 155 | Extra configuration for the widget in JavaScript. 156 | 157 | Should be a lambda/anonymous function that takes the widget as its sole argument, 158 | which can then be called by the script. 159 | ''; 160 | }; 161 | }; 162 | }; 163 | 164 | isKnownWidget = lib.flip builtins.hasAttr sources; 165 | 166 | self = { 167 | inherit isKnownWidget positionType sizeType; 168 | 169 | type = lib.types.oneOf [ 170 | lib.types.str 171 | compositeWidgetType 172 | simpleWidgetType 173 | ]; 174 | desktopType = lib.types.oneOf [ 175 | compositeWidgetType 176 | desktopSimpleWidgetType 177 | ]; 178 | 179 | lib = import ./lib.nix (args // { widgets = self; }); 180 | 181 | desktopConvert = 182 | widget: 183 | let 184 | inherit (builtins) 185 | length 186 | head 187 | attrNames 188 | mapAttrs 189 | isAttrs 190 | ; 191 | keys = attrNames widget; 192 | type = head keys; 193 | 194 | base = { 195 | config = null; 196 | extraConfig = ""; 197 | }; 198 | converters = mapAttrs (_: s: s.convert) sources; 199 | in 200 | if isAttrs widget && length keys == 1 && isKnownWidget type then 201 | let 202 | convertedWidget = converters.${type} widget.${type}; 203 | in 204 | base 205 | // convertedWidget 206 | // { 207 | position = 208 | if isAttrs widget.${type}.position then 209 | widget.${type}.position 210 | else 211 | (throw "Desktop widget requires a position"); 212 | size = 213 | if isAttrs widget.${type}.size then 214 | widget.${type}.size 215 | else 216 | (throw "Desktop widget requires a size"); 217 | } 218 | else 219 | widget; # not a known composite type 220 | 221 | convert = 222 | widget: 223 | let 224 | inherit (builtins) 225 | length 226 | head 227 | attrNames 228 | mapAttrs 229 | isAttrs 230 | isString 231 | ; 232 | keys = attrNames widget; 233 | type = head keys; 234 | 235 | base = { 236 | config = null; 237 | extraConfig = ""; 238 | }; 239 | converters = mapAttrs (_: s: s.convert) sources; 240 | in 241 | if isString widget then 242 | base // { name = widget; } 243 | else if isAttrs widget && length keys == 1 && isKnownWidget type then 244 | base // converters.${type} widget.${type} 245 | else 246 | widget; # not a known composite type 247 | }; 248 | in 249 | self 250 | -------------------------------------------------------------------------------- /modules/widgets/keyboard-layout.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (import ./lib.nix { inherit lib; }) configValueType; 4 | inherit (import ./default.nix { inherit lib; }) positionType sizeType; 5 | 6 | getIndexFromEnum = 7 | enum: value: 8 | if value == null then 9 | null 10 | else 11 | lib.lists.findFirstIndex (x: x == value) 12 | (throw "getIndexFromEnum (keyboard-layout widget): Value ${value} isn't present in the enum. This is a bug") 13 | enum; 14 | in 15 | { 16 | keyboardLayout = { 17 | description = "The keyboard layout indicator widget."; 18 | 19 | opts = { 20 | position = lib.mkOption { 21 | type = positionType; 22 | example = { 23 | horizontal = 250; 24 | vertical = 50; 25 | }; 26 | description = "The position of the widget. (Only for desktop widget)"; 27 | }; 28 | size = lib.mkOption { 29 | type = sizeType; 30 | example = { 31 | width = 500; 32 | height = 500; 33 | }; 34 | description = "The size of the widget. (Only for desktop widget)"; 35 | }; 36 | displayStyle = 37 | let 38 | enumVals = [ 39 | "label" 40 | "flag" 41 | "labelOverFlag" 42 | ]; 43 | in 44 | lib.mkOption { 45 | type = with lib.types; nullOr (enum enumVals); 46 | default = null; 47 | example = "labelOverFlag"; 48 | description = "Keyboard layout indicator display style."; 49 | apply = getIndexFromEnum enumVals; 50 | }; 51 | settings = lib.mkOption { 52 | type = configValueType; 53 | default = null; 54 | example = { 55 | General = { 56 | displayStyle = 1; 57 | }; 58 | }; 59 | apply = settings: if settings == null then { } else settings; 60 | }; 61 | }; 62 | 63 | convert = 64 | { displayStyle, settings, ... }: 65 | { 66 | name = "org.kde.plasma.keyboardlayout"; 67 | config = lib.recursiveUpdate { 68 | General = lib.filterAttrs (_: v: v != null) { inherit displayStyle; }; 69 | } settings; 70 | }; 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /modules/widgets/kicker.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (lib) mkOption types; 4 | inherit (import ./lib.nix { inherit lib; }) configValueType; 5 | inherit (import ./default.nix { inherit lib; }) positionType sizeType; 6 | 7 | mkBoolOption = 8 | description: 9 | mkOption { 10 | type = with types; nullOr bool; 11 | default = null; 12 | example = true; 13 | inherit description; 14 | }; 15 | 16 | getIndexFromEnum = 17 | enum: value: 18 | if value == null then 19 | null 20 | else 21 | lib.lists.findFirstIndex (x: x == value) 22 | (throw "getIndexFromEnum (kicker widget): Value ${value} isn't present in the enum. This is a bug") 23 | enum; 24 | 25 | checkPath = 26 | path: 27 | if path == null then 28 | null 29 | else if lib.strings.hasPrefix "/" path then 30 | path 31 | else 32 | throw "checkPath (kicker widget): Path ${path} is not an absolute path."; 33 | in 34 | { 35 | kicker = { 36 | description = '' 37 | Kicker is a launcher, which is also known as Application Menu. 38 | Kicker does not have fancy features, like the other launchers, 39 | but provides a tightly arranged interface. 40 | ''; 41 | 42 | opts = { 43 | position = mkOption { 44 | type = positionType; 45 | example = { 46 | horizontal = 250; 47 | vertical = 50; 48 | }; 49 | description = "The position of the widget. (Only for desktop widget)"; 50 | }; 51 | size = mkOption { 52 | type = sizeType; 53 | example = { 54 | width = 500; 55 | height = 500; 56 | }; 57 | description = "The size of the widget. (Only for desktop widget)"; 58 | }; 59 | icon = mkOption { 60 | type = types.nullOr types.str; 61 | default = null; 62 | example = "start-here-kde-symbolic"; 63 | description = "The icon to use for the kickoff button."; 64 | }; 65 | customButtonImage = mkOption { 66 | type = types.nullOr types.str; 67 | default = null; 68 | example = "/home/user/pictures/custom-button.png"; 69 | description = "The absolute path image to use for the custom button."; 70 | apply = checkPath; 71 | }; 72 | applicationNameFormat = 73 | let 74 | enumVals = [ 75 | "nameOnly" 76 | "genericNameOnly" 77 | "nameAndGenericName" 78 | "genericNameAndName" 79 | ]; 80 | in 81 | mkOption { 82 | type = with types; nullOr (enum enumVals); 83 | default = null; 84 | example = "nameOnly"; 85 | description = "The format of the application name to display."; 86 | apply = getIndexFromEnum enumVals; 87 | }; 88 | behavior = { 89 | sortAlphabetically = mkBoolOption "Whether to sort the applications alphabetically."; 90 | flattenCategories = mkBoolOption "Whether to flatten top-level menu categories to a single level instead of displaying sub-categories."; 91 | showIconsOnRootLevel = mkBoolOption "Whether to show icons on the root level of the menu."; 92 | }; 93 | categories = { 94 | show = { 95 | recentApplications = mkBoolOption "Whether to show recent applications."; 96 | recentFiles = mkBoolOption "Whether to show recent files."; 97 | }; 98 | order = 99 | let 100 | enumVals = [ 101 | "recentFirst" 102 | "popularFirst" 103 | ]; 104 | in 105 | mkOption { 106 | type = with types; nullOr (enum enumVals); 107 | default = null; 108 | example = "recentFirst"; 109 | description = "The order in which to show the categories."; 110 | apply = getIndexFromEnum enumVals; 111 | }; 112 | }; 113 | search = { 114 | alignResultsToBottom = mkBoolOption "Whether to align the search results to the bottom of the screen."; 115 | expandSearchResults = mkBoolOption "Whether to expand the search results to bookmarks, files and emails."; 116 | }; 117 | settings = mkOption { 118 | type = configValueType; 119 | default = null; 120 | example = { 121 | General = { 122 | icon = "nix-snowflake-white"; 123 | }; 124 | }; 125 | description = "Extra configuration options for the widget."; 126 | apply = settings: if settings == null then { } else settings; 127 | }; 128 | }; 129 | convert = 130 | { 131 | icon, 132 | customButtonImage, 133 | applicationNameFormat, 134 | behavior, 135 | categories, 136 | search, 137 | settings, 138 | ... 139 | }: 140 | { 141 | name = "org.kde.plasma.kicker"; 142 | config = lib.recursiveUpdate { 143 | General = lib.filterAttrs (_: v: v != null) { 144 | inherit icon customButtonImage; 145 | inherit (search) alignResultsToBottom; 146 | 147 | useCustomButtonImage = (customButtonImage != null); 148 | 149 | appNameFormat = applicationNameFormat; 150 | 151 | alphaSort = behavior.sortAlphabetically; 152 | limitDepth = behavior.flattenCategories; 153 | showIconsRootLevel = behavior.showIconsOnRootLevel; 154 | 155 | showRecentApps = categories.show.recentApplications; 156 | showRecentDocs = categories.show.recentFiles; 157 | recentOrdering = categories.order; 158 | 159 | useExtraRunners = search.expandSearchResults; 160 | }; 161 | } settings; 162 | }; 163 | }; 164 | } 165 | -------------------------------------------------------------------------------- /modules/widgets/kickerdash.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (lib) mkOption types; 4 | inherit (import ./lib.nix { inherit lib; }) configValueType; 5 | inherit (import ./default.nix { inherit lib; }) positionType sizeType; 6 | 7 | mkBoolOption = 8 | description: 9 | mkOption { 10 | type = with types; nullOr bool; 11 | default = null; 12 | example = true; 13 | inherit description; 14 | }; 15 | 16 | getIndexFromEnum = 17 | enum: value: 18 | if value == null then 19 | null 20 | else 21 | lib.lists.findFirstIndex (x: x == value) 22 | (throw "getIndexFromEnum (kickerdash widget): Value ${value} isn't present in the enum. This is a bug") 23 | enum; 24 | 25 | checkPath = 26 | path: 27 | if path == null then 28 | null 29 | else if lib.strings.hasPrefix "/" path then 30 | path 31 | else 32 | throw "checkPath (kickerdash widget): Path ${path} is not an absolute path."; 33 | in 34 | { 35 | kickerdash = { 36 | description = "Application Dashboard (kickerdash) is an alternative launcher which fills the whole desktop."; 37 | 38 | opts = { 39 | position = mkOption { 40 | type = positionType; 41 | example = { 42 | horizontal = 250; 43 | vertical = 50; 44 | }; 45 | description = "The position of the widget. (Only for desktop widget)"; 46 | }; 47 | size = mkOption { 48 | type = sizeType; 49 | example = { 50 | width = 500; 51 | height = 500; 52 | }; 53 | description = "The size of the widget. (Only for desktop widget)"; 54 | }; 55 | icon = mkOption { 56 | type = types.nullOr types.str; 57 | default = null; 58 | example = "start-here-kde-symbolic"; 59 | description = "The icon to use for the kickoff button."; 60 | }; 61 | customButtonImage = mkOption { 62 | type = types.nullOr types.str; 63 | default = null; 64 | example = "/home/user/pictures/custom-button.png"; 65 | description = "The absolute path image to use for the custom button."; 66 | apply = checkPath; 67 | }; 68 | applicationNameFormat = 69 | let 70 | enumVals = [ 71 | "nameOnly" 72 | "genericNameOnly" 73 | "nameAndGenericName" 74 | "genericNameAndName" 75 | ]; 76 | in 77 | mkOption { 78 | type = with types; nullOr (enum enumVals); 79 | default = null; 80 | example = "nameOnly"; 81 | description = "The format of the application name to display."; 82 | apply = getIndexFromEnum enumVals; 83 | }; 84 | behavior = { 85 | sortAlphabetically = mkBoolOption "Whether to sort the applications alphabetically."; 86 | }; 87 | categories = { 88 | show = { 89 | recentApplications = mkBoolOption "Whether to show recent applications."; 90 | recentFiles = mkBoolOption "Whether to show recent files."; 91 | }; 92 | order = 93 | let 94 | enumVals = [ 95 | "recentFirst" 96 | "popularFirst" 97 | ]; 98 | in 99 | mkOption { 100 | type = with types; nullOr (enum enumVals); 101 | default = null; 102 | example = "recentFirst"; 103 | description = "The order in which to show the categories."; 104 | apply = getIndexFromEnum enumVals; 105 | }; 106 | }; 107 | search = { 108 | expandSearchResults = mkBoolOption "Whether to expand the search results to bookmarks, files and emails."; 109 | }; 110 | settings = mkOption { 111 | type = configValueType; 112 | default = null; 113 | example = { 114 | General = { 115 | icon = "nix-snowflake-white"; 116 | }; 117 | }; 118 | description = "Extra configuration options for the widget."; 119 | apply = settings: if settings == null then { } else settings; 120 | }; 121 | }; 122 | convert = 123 | { 124 | icon, 125 | customButtonImage, 126 | applicationNameFormat, 127 | behavior, 128 | categories, 129 | search, 130 | settings, 131 | ... 132 | }: 133 | { 134 | name = "org.kde.plasma.kickerdash"; 135 | config = lib.recursiveUpdate { 136 | General = lib.filterAttrs (_: v: v != null) { 137 | inherit icon customButtonImage; 138 | useCustomButtonImage = (customButtonImage != null); 139 | 140 | appNameFormat = applicationNameFormat; 141 | 142 | alphaSort = behavior.sortAlphabetically; 143 | 144 | showRecentApps = categories.show.recentApplications; 145 | showRecentDocs = categories.show.recentFiles; 146 | recentOrdering = categories.order; 147 | 148 | useExtraRunners = search.expandSearchResults; 149 | }; 150 | } settings; 151 | }; 152 | }; 153 | } 154 | -------------------------------------------------------------------------------- /modules/widgets/kickoff.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (import ./lib.nix { inherit lib; }) configValueType; 4 | inherit (import ./default.nix { inherit lib; }) positionType sizeType; 5 | 6 | mkBoolOption = 7 | description: 8 | lib.mkOption { 9 | type = with lib.types; nullOr bool; 10 | default = null; 11 | example = true; 12 | inherit description; 13 | }; 14 | 15 | getIndexFromEnum = 16 | enum: value: 17 | if value == null then 18 | null 19 | else 20 | lib.lists.findFirstIndex (x: x == value) 21 | (throw "getIndexFromEnum (kickoff widget): Value ${value} isn't present in the enum. This is a bug") 22 | enum; 23 | 24 | convertSidebarPosition = 25 | sidebarPosition: 26 | let 27 | mappings = { 28 | left = false; 29 | right = true; 30 | }; 31 | in 32 | if sidebarPosition == null then 33 | null 34 | else 35 | mappings.${sidebarPosition} or (throw "Invalid sidebar position: ${sidebarPosition}"); 36 | in 37 | { 38 | kickoff = { 39 | description = "Kickoff is the default application launcher of the Plasma desktop."; 40 | 41 | opts = { 42 | position = lib.mkOption { 43 | type = positionType; 44 | example = { 45 | horizontal = 250; 46 | vertical = 50; 47 | }; 48 | description = "The position of the widget. (Only for desktop widget)"; 49 | }; 50 | size = lib.mkOption { 51 | type = sizeType; 52 | example = { 53 | width = 500; 54 | height = 500; 55 | }; 56 | description = "The size of the widget. (Only for desktop widget)"; 57 | }; 58 | icon = lib.mkOption { 59 | type = with lib.types; nullOr str; 60 | default = null; 61 | example = "start-here-kde-symbolic"; 62 | description = '' 63 | The icon to use for the kickoff button. 64 | 65 | This can also be used to specify a custom image for the kickoff button. 66 | To do this, set the value to a absolute path to the image file. 67 | ''; 68 | }; 69 | label = lib.mkOption { 70 | type = with lib.types; nullOr str; 71 | default = null; 72 | example = "Menu"; 73 | description = "The label to use for the kickoff button."; 74 | }; 75 | sortAlphabetically = mkBoolOption "Whether to sort menu contents alphabetically or use manual/system sort order."; 76 | compactDisplayStyle = mkBoolOption "Whether to use a compact display style for list items."; 77 | sidebarPosition = lib.mkOption { 78 | type = 79 | with lib.types; 80 | nullOr (enum [ 81 | "left" 82 | "right" 83 | ]); 84 | default = null; 85 | example = "left"; 86 | description = "The position of the sidebar."; 87 | apply = convertSidebarPosition; 88 | }; 89 | favoritesDisplayMode = 90 | let 91 | enumVals = [ 92 | "grid" 93 | "list" 94 | ]; 95 | in 96 | lib.mkOption { 97 | type = with lib.types; nullOr (enum enumVals); 98 | default = null; 99 | example = "list"; 100 | description = "How to display favorites."; 101 | apply = getIndexFromEnum enumVals; 102 | }; 103 | applicationsDisplayMode = 104 | let 105 | enumVals = [ 106 | "grid" 107 | "list" 108 | ]; 109 | in 110 | lib.mkOption { 111 | type = with lib.types; nullOr (enum enumVals); 112 | default = null; 113 | example = "grid"; 114 | description = "How to display applications."; 115 | apply = getIndexFromEnum enumVals; 116 | }; 117 | showButtonsFor = 118 | let 119 | enumVals = [ 120 | "power" 121 | "session" 122 | "powerAndSession" 123 | ]; 124 | buttonsEnum = [ 125 | "lock-screen" 126 | "logout" 127 | "save-session" 128 | "switch-user" 129 | "suspend" 130 | "hibernate" 131 | "reboot" 132 | "shutdown" 133 | ]; 134 | in 135 | lib.mkOption { 136 | type = 137 | with lib.types; 138 | nullOr ( 139 | either (enum enumVals) (submodule { 140 | options.custom = lib.mkOption { 141 | type = listOf (enum buttonsEnum); 142 | example = [ 143 | "shutdown" 144 | "reboot" 145 | ]; 146 | description = "The custom buttons to show"; 147 | }; 148 | }) 149 | ); 150 | default = null; 151 | example = { 152 | custom = [ 153 | "shutdown" 154 | "reboot" 155 | "logout" 156 | ]; 157 | }; 158 | description = "Which actions should be displayed in the footer."; 159 | apply = 160 | value: 161 | if value == null then 162 | { } 163 | else if value ? custom then 164 | { 165 | primaryActions = 2; 166 | systemFavorites = builtins.concatStringsSep ''\\,'' value.custom; 167 | } 168 | else 169 | { 170 | primaryActions = 171 | builtins.elemAt 172 | [ 173 | 0 174 | 1 175 | 3 176 | ] 177 | ( 178 | lib.lists.findFirstIndex ( 179 | x: x == value 180 | ) (throw "kickoff: non-existent value ${value}! This is a bug!") enumVals 181 | ); 182 | systemFavorites = 183 | if value == "session" then 184 | builtins.concatStringsSep ''\\,'' ( 185 | builtins.filter (v: v != null) (lib.imap0 (i: v: if i < 4 then v else null) buttonsEnum) 186 | ) 187 | else if value == "power" then 188 | builtins.concatStringsSep ''\\,'' ( 189 | builtins.filter (v: v != null) (lib.imap0 (i: v: if i > 3 then v else null) buttonsEnum) 190 | ) 191 | else 192 | builtins.concatStringsSep ''\\,'' buttonsEnum; 193 | }; 194 | }; 195 | showActionButtonCaptions = mkBoolOption "Whether to display captions ('shut down', 'log out', etc.) for the footer action buttons"; 196 | pin = mkBoolOption "Whether the popup should remain open when another window is activated."; 197 | popupHeight = lib.mkOption { 198 | type = with lib.types; nullOr ints.positive; 199 | default = null; 200 | example = 500; 201 | }; 202 | popupWidth = lib.mkOption { 203 | type = with lib.types; nullOr ints.positive; 204 | default = null; 205 | example = 700; 206 | }; 207 | settings = lib.mkOption { 208 | type = configValueType; 209 | default = null; 210 | example = { 211 | General = { 212 | icon = "nix-snowflake-white"; 213 | }; 214 | popupHeight = 500; 215 | }; 216 | description = "Extra configuration options for the widget."; 217 | apply = settings: if settings == null then { } else settings; 218 | }; 219 | }; 220 | convert = 221 | { 222 | icon, 223 | label, 224 | sortAlphabetically, 225 | compactDisplayStyle, 226 | sidebarPosition, 227 | favoritesDisplayMode, 228 | applicationsDisplayMode, 229 | showButtonsFor, 230 | showActionButtonCaptions, 231 | pin, 232 | popupHeight, 233 | popupWidth, 234 | settings, 235 | ... 236 | }: 237 | { 238 | name = "org.kde.plasma.kickoff"; 239 | config = lib.recursiveUpdate (lib.filterAttrsRecursive (_: v: v != null) { 240 | popupHeight = popupHeight; 241 | popupWidth = popupWidth; 242 | 243 | General = { 244 | inherit icon pin showActionButtonCaptions; 245 | 246 | menuLabel = label; 247 | alphaSort = sortAlphabetically; 248 | compactMode = compactDisplayStyle; 249 | paneSwap = sidebarPosition; 250 | favoritesDisplay = favoritesDisplayMode; 251 | applicationsDisplay = applicationsDisplayMode; 252 | } // showButtonsFor; 253 | }) settings; 254 | }; 255 | }; 256 | } 257 | -------------------------------------------------------------------------------- /modules/widgets/lib.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (lib) 4 | optionalString 5 | concatMapStringsSep 6 | concatStringsSep 7 | mapAttrsToList 8 | filterAttrs 9 | splitString 10 | ; 11 | 12 | configValueTypes = 13 | with lib.types; 14 | oneOf [ 15 | bool 16 | float 17 | int 18 | str 19 | ]; 20 | configValueTypeInner = with lib.types; either configValueTypes (listOf configValueTypes); 21 | configValueType = 22 | with lib.types; 23 | nullOr (attrsOf (either configValueTypeInner (attrsOf configValueTypeInner))); 24 | 25 | # any value or null -> string -> string 26 | # If value is null, returns the empty string, otherwise returns the provided string 27 | stringIfNotNull = v: optionalString (v != null); 28 | 29 | # Converts each datatype into an expression which can be parsed in JavaScript 30 | valToJS = 31 | v: 32 | if (builtins.isString v) then 33 | ''"${v}"'' 34 | else if (builtins.isBool v) then 35 | (lib.boolToString v) 36 | else 37 | (builtins.toString v); 38 | 39 | # Converts a list of to a single string, that can be parsed as a string list in JavaScript 40 | toJSList = values: ''[${concatMapStringsSep ", " valToJS values}]''; 41 | 42 | setWidgetSettings = 43 | var: settings: 44 | let 45 | perConfig = 46 | key: value: 47 | ''${var}.writeConfig("${key}", ${ 48 | if builtins.isList value then toJSList value else valToJS value 49 | });''; 50 | 51 | perGroup = group: configs: '' 52 | ${var}.currentConfigGroup = ${toJSList (splitString "/" group)}; 53 | ${concatStringsSep "\n" (mapAttrsToList perConfig configs)} 54 | ''; 55 | 56 | groups = (filterAttrs (_: value: builtins.isAttrs value) settings); 57 | topLevel = (filterAttrs (_: value: !builtins.isAttrs value) settings); 58 | in 59 | concatStringsSep "\n" ( 60 | (lib.optional (topLevel != { }) "${var}.currentConfigGroup = [];") 61 | ++ (mapAttrsToList perConfig topLevel) 62 | ++ 63 | 64 | (mapAttrsToList perGroup groups) 65 | ); 66 | 67 | addWidgetStmts = 68 | containment: var: ws: 69 | let 70 | widgetConfigsToStmts = 71 | { name, config, ... }: 72 | '' 73 | var w = ${var}["${name}"]; 74 | ${setWidgetSettings "w" config} 75 | ''; 76 | 77 | addStmt = 78 | { 79 | name, 80 | config, 81 | extraConfig, 82 | }@widget: 83 | '' 84 | ${var}["${name}"] = ${containment}.addWidget("${name}"); 85 | ${stringIfNotNull config (widgetConfigsToStmts widget)} 86 | ${lib.optionalString (extraConfig != "") '' 87 | (${extraConfig})(${var}["${name}"]); 88 | ''} 89 | ''; 90 | in 91 | '' 92 | const ${var} = {}; 93 | ${lib.concatMapStringsSep "\n" addStmt ws} 94 | ''; 95 | 96 | addDesktopWidgetStmts = 97 | containment: var: ws: 98 | let 99 | widgetConfigsToStmts = 100 | { name, config, ... }: 101 | '' 102 | var w = ${var}["${name}"]; 103 | ${setWidgetSettings "w" config} 104 | ''; 105 | 106 | addStmt = 107 | { 108 | name, 109 | position, 110 | size, 111 | config, 112 | extraConfig, 113 | }@widget: 114 | '' 115 | ${var}["${name}"] = ${containment}.addWidget("${name}", ${toString position.horizontal}, ${toString position.vertical}, ${toString size.width}, ${toString size.height}); 116 | ${stringIfNotNull config (widgetConfigsToStmts widget)} 117 | ${lib.optionalString (extraConfig != "") '' 118 | (${extraConfig})(${var}["${name}"]); 119 | ''} 120 | ''; 121 | in 122 | '' 123 | const ${var} = {}; 124 | ${lib.concatMapStringsSep "\n" addStmt ws} 125 | ''; 126 | in 127 | { 128 | inherit 129 | stringIfNotNull 130 | setWidgetSettings 131 | addWidgetStmts 132 | addDesktopWidgetStmts 133 | configValueType 134 | ; 135 | } 136 | -------------------------------------------------------------------------------- /modules/widgets/pager.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (import ./lib.nix { inherit lib; }) configValueType; 4 | inherit (import ./default.nix { inherit lib; }) positionType sizeType; 5 | 6 | mkBoolOption = 7 | description: 8 | lib.mkOption { 9 | type = with lib.types; nullOr bool; 10 | default = null; 11 | example = true; 12 | inherit description; 13 | }; 14 | 15 | capitalizeWord = 16 | word: 17 | with lib.strings; 18 | if word == null then 19 | null 20 | else 21 | concatImapStrings (pos: char: if pos == 1 then toUpper char else char) (stringToCharacters word); 22 | in 23 | { 24 | pager = { 25 | description = "The desktop pager is a plasma widget that helps you to organize virtual desktops."; 26 | 27 | opts = { 28 | position = lib.mkOption { 29 | type = positionType; 30 | example = { 31 | horizontal = 250; 32 | vertical = 50; 33 | }; 34 | description = "The position of the widget. (Only for desktop widget)"; 35 | }; 36 | size = lib.mkOption { 37 | type = sizeType; 38 | example = { 39 | width = 500; 40 | height = 500; 41 | }; 42 | description = "The size of the widget. (Only for desktop widget)"; 43 | }; 44 | general = { 45 | showWindowOutlines = mkBoolOption "Whether to show window outlines"; 46 | showApplicationIconsOnWindowOutlines = mkBoolOption "Whether to show application icons on window outlines"; 47 | showOnlyCurrentScreen = mkBoolOption "Whether to limit the Pager to the set of windows and the geometry of the screen the widget resides on"; 48 | navigationWrapsAround = mkBoolOption "Whether to wrap around when navigating the desktops"; 49 | displayedText = 50 | let 51 | options = { 52 | none = "None"; 53 | desktopNumber = "Number"; 54 | desktopName = "Name"; 55 | }; 56 | in 57 | lib.mkOption { 58 | type = with lib.types; nullOr (enum (builtins.attrNames options)); 59 | default = null; 60 | example = "desktopNumber"; 61 | description = "The text to show inside the desktop rectangles"; 62 | apply = option: if option == null then null else options.${option}; 63 | }; 64 | selectingCurrentVirtualDesktop = lib.mkOption { 65 | type = 66 | with lib.types; 67 | nullOr (enum [ 68 | "doNothing" 69 | "showDesktop" 70 | ]); 71 | default = null; 72 | example = "showDesktop"; 73 | description = "What to do on left-mouse click on a desktop rectangle"; 74 | apply = capitalizeWord; 75 | }; 76 | }; 77 | settings = lib.mkOption { 78 | type = configValueType; 79 | default = null; 80 | example = { 81 | General = { 82 | showWindowOutlines = true; 83 | }; 84 | }; 85 | description = "Extra configuration options for the widget."; 86 | apply = settings: if settings == null then { } else settings; 87 | }; 88 | }; 89 | convert = 90 | { general, settings, ... }: 91 | { 92 | name = "org.kde.plasma.pager"; 93 | config = lib.recursiveUpdate { 94 | General = lib.filterAttrs (_: v: v != null) { 95 | showWindowOutlines = general.showWindowOutlines; 96 | showWindowIcons = general.showApplicationIconsOnWindowOutlines; 97 | showOnlyCurrentScreen = general.showOnlyCurrentScreen; 98 | wrapPage = general.navigationWrapsAround; 99 | displayedText = general.displayedText; 100 | currentDesktopSelected = general.selectingCurrentVirtualDesktop; 101 | }; 102 | } settings; 103 | }; 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /modules/widgets/panel-spacer.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (import ./lib.nix { inherit lib; }) configValueType; 4 | inherit (import ./default.nix { inherit lib; }) positionType sizeType; 5 | 6 | mkBoolOption = 7 | description: 8 | lib.mkOption { 9 | type = with lib.types; nullOr bool; 10 | default = null; 11 | example = true; 12 | inherit description; 13 | }; 14 | in 15 | { 16 | panelSpacer = { 17 | opts = { 18 | position = lib.mkOption { 19 | type = positionType; 20 | example = { 21 | horizontal = 100; 22 | vertical = 300; 23 | }; 24 | description = "The position of the widget. (Only for desktop widget)"; 25 | }; 26 | size = lib.mkOption { 27 | type = sizeType; 28 | example = { 29 | width = 500; 30 | height = 50; 31 | }; 32 | description = "The size of the widget. (Only for desktop widget)"; 33 | }; 34 | expanding = mkBoolOption "Whether the spacer should expand to fill the available space."; 35 | length = lib.mkOption { 36 | type = lib.types.nullOr lib.types.int; 37 | default = null; 38 | example = 50; 39 | description = '' 40 | The length of the spacer. 41 | If expanding is set to true, this value is ignored. 42 | ''; 43 | }; 44 | settings = lib.mkOption { 45 | type = configValueType; 46 | default = null; 47 | example = { 48 | General = { 49 | expanding = true; 50 | }; 51 | }; 52 | description = '' 53 | Extra configuration for the widget 54 | ''; 55 | apply = settings: if settings == null then { } else settings; 56 | }; 57 | }; 58 | 59 | convert = 60 | { 61 | expanding, 62 | length, 63 | settings, 64 | ... 65 | }: 66 | { 67 | name = "org.kde.plasma.panelspacer"; 68 | config = lib.recursiveUpdate { 69 | General = lib.filterAttrs (_: v: v != null) { inherit expanding length; }; 70 | } settings; 71 | }; 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /modules/widgets/system-monitor.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | let 3 | inherit (lib) mkOption types; 4 | inherit (import ./lib.nix { inherit lib; }) configValueType; 5 | inherit (import ./default.nix { inherit lib; }) positionType sizeType; 6 | 7 | # KDE expects a key/value pair like this: 8 | # ```ini 9 | # highPrioritySensorIds=["cpu/all/usage", "cpu/all/averageTemperature"] 10 | # ``` 11 | # 12 | # Which is **different** to what would happen if you pass a list of strings to the JS script: 13 | # ```ini 14 | # highPrioritySensorIds=cpu/all/usage,cpu/all/averageTemperature 15 | # ``` 16 | # 17 | # So, to satisfy the expected format we must quote the ENTIRE string as a valid JS string, 18 | # which means constructing a string that looks like this in the source code: 19 | # "[\"cpu/all/usage\", \"cpu/all/averageTemperature\"]" 20 | toEscapedList = 21 | ids: if ids != null then "[${lib.concatMapStringsSep ", " (x: ''\"${x}\"'') ids}]" else null; 22 | 23 | mkListOption = mkOption { 24 | type = with types; nullOr (listOf str); 25 | default = null; 26 | apply = toEscapedList; 27 | }; 28 | 29 | # {name, color} -> {name, value} 30 | # Convert the sensor attrset into a name-value pair expected by listToAttrs 31 | toColorKV = 32 | { name, color, ... }: 33 | { 34 | inherit name; 35 | value = color; 36 | }; 37 | toLabelKV = 38 | { name, label, ... }: 39 | { 40 | inherit name; 41 | value = label; 42 | }; 43 | in 44 | { 45 | systemMonitor = { 46 | description = "A system monitor widget."; 47 | 48 | opts = { 49 | # See https://invent.kde.org/plasma/plasma-workspace/-/blob/master/applets/systemmonitor/systemmonitor/package/contents/config/main.xml for the accepted raw options 50 | 51 | position = mkOption { 52 | type = positionType; 53 | example = { 54 | horizontal = 250; 55 | vertical = 50; 56 | }; 57 | description = "The position of the widget. (Only for desktop widget)"; 58 | }; 59 | size = mkOption { 60 | type = sizeType; 61 | example = { 62 | width = 500; 63 | height = 500; 64 | }; 65 | description = "The size of the widget. (Only for desktop widget)"; 66 | }; 67 | title = mkOption { 68 | type = with types; nullOr str; 69 | default = null; 70 | description = "The title of this system monitor."; 71 | }; 72 | showTitle = mkOption { 73 | type = with types; nullOr bool; 74 | default = null; 75 | description = "Show or hide the title."; 76 | }; 77 | showLegend = mkOption { 78 | type = with types; nullOr bool; 79 | default = null; 80 | description = "Show or hide the legend."; 81 | }; 82 | displayStyle = mkOption { 83 | type = with types; nullOr str; 84 | default = null; 85 | example = "org.kde.ksysguard.barchart"; 86 | description = "The display style of the chart. Uses the internal plugin name."; 87 | }; 88 | sensors = mkOption { 89 | type = 90 | with types; 91 | nullOr ( 92 | listOf (submodule { 93 | options = { 94 | name = mkOption { 95 | type = str; 96 | example = "cpu/all/usage"; 97 | description = "The name of the sensor."; 98 | }; 99 | color = mkOption { 100 | type = str; # TODO maybe use a better type 101 | example = "255,255,255"; 102 | description = "The color of the sensor, as a string containing 8-bit integral RGB values separated by commas"; 103 | }; 104 | label = mkOption { 105 | type = str; 106 | example = "CPU %"; 107 | description = "The label of the sensor."; 108 | }; 109 | }; 110 | }) 111 | ); 112 | default = null; 113 | example = [ 114 | { 115 | name = "gpu/gpu1/usage"; 116 | color = "180,190,254"; 117 | label = "GPU %"; 118 | } 119 | ]; 120 | description = '' 121 | The list of sensors displayed as a part of the graph/chart. 122 | ''; 123 | apply = 124 | sensors: 125 | lib.optionalAttrs (sensors != null) { 126 | SensorColors = builtins.listToAttrs (map toColorKV sensors); 127 | SensorLabels = builtins.listToAttrs (map toLabelKV sensors); 128 | Sensors.highPrioritySensorIds = toEscapedList (map (s: s.name) sensors); 129 | }; 130 | }; 131 | 132 | totalSensors = mkListOption // { 133 | example = [ "cpu/all/usage" ]; 134 | description = '' 135 | The list of "total sensors" displayed on top of the graph/chart. 136 | ''; 137 | }; 138 | textOnlySensors = mkListOption // { 139 | example = [ 140 | "cpu/all/averageTemperature" 141 | "cpu/all/averageFrequency" 142 | ]; 143 | description = '' 144 | The list of text-only sensors, displayed in the pop-up upon clicking the widget. 145 | ''; 146 | }; 147 | range = { 148 | from = mkOption { 149 | type = with lib.types; nullOr (ints.between 0 100); 150 | default = null; 151 | description = "The lower range the sensors can take."; 152 | }; 153 | to = mkOption { 154 | type = with lib.types; nullOr (ints.between 0 100); 155 | default = null; 156 | description = "The upper range the sensors can take."; 157 | }; 158 | }; 159 | settings = mkOption { 160 | type = configValueType; 161 | default = null; 162 | description = "Extra configuration options for the widget."; 163 | apply = settings: if settings == null then { } else settings; 164 | }; 165 | }; 166 | 167 | convert = 168 | { 169 | title, 170 | showTitle, 171 | showLegend, 172 | displayStyle, 173 | totalSensors, 174 | sensors, 175 | textOnlySensors, 176 | range, 177 | settings, 178 | ... 179 | }: 180 | { 181 | name = "org.kde.plasma.systemmonitor"; 182 | config = lib.filterAttrsRecursive (_: v: v != null) ( 183 | lib.recursiveUpdate { 184 | Appearance = { 185 | inherit title; 186 | inherit showTitle; 187 | chartFace = displayStyle; 188 | }; 189 | Sensors = { 190 | lowPrioritySensorIds = textOnlySensors; 191 | totalSensors = totalSensors; 192 | }; 193 | "org.kde.ksysguard.piechart/General" = { 194 | inherit showLegend; 195 | rangeAuto = (range.from == null && range.to == null); 196 | rangeFrom = range.from; 197 | rangeTo = range.to; 198 | }; 199 | } (lib.recursiveUpdate sensors settings) 200 | ); 201 | }; 202 | }; 203 | } 204 | -------------------------------------------------------------------------------- /modules/widgets/system-tray.nix: -------------------------------------------------------------------------------- 1 | { lib, widgets, ... }: 2 | let 3 | inherit (lib) mkOption types; 4 | inherit (import ./lib.nix { inherit lib; }) configValueType; 5 | inherit (import ./default.nix { inherit lib; }) positionType sizeType; 6 | 7 | mkBoolOption = 8 | description: 9 | mkOption { 10 | type = with types; nullOr bool; 11 | default = null; 12 | inherit description; 13 | }; 14 | in 15 | { 16 | systemTray = { 17 | description = "A system tray of other widgets/plasmoids"; 18 | 19 | opts = ( 20 | { options, ... }: 21 | { 22 | # See https://invent.kde.org/plasma/plasma-workspace/-/blob/master/applets/systemtray/package/contents/config/main.xml for the accepted raw options. 23 | 24 | position = mkOption { 25 | type = positionType; 26 | example = { 27 | horizontal = 250; 28 | vertical = 50; 29 | }; 30 | description = "The position of the widget. (Only for desktop widget)"; 31 | }; 32 | size = mkOption { 33 | type = sizeType; 34 | example = { 35 | width = 500; 36 | height = 500; 37 | }; 38 | description = "The size of the widget. (Only for desktop widget)"; 39 | }; 40 | pin = mkBoolOption "Whether the popup should remain open when another window is activated."; 41 | 42 | icons = { 43 | spacing = 44 | let 45 | enum = [ 46 | "small" 47 | "medium" 48 | "large" 49 | ]; 50 | in 51 | mkOption { 52 | type = types.nullOr (types.either (types.enum enum) types.ints.positive); 53 | default = null; 54 | description = '' 55 | The spacing between icons. 56 | 57 | Could be an integer unit, or "small" (1 unit), "medium" (2 units) or "large" (6 units). 58 | ''; 59 | apply = 60 | spacing: 61 | ( 62 | if (spacing == null) then 63 | null 64 | else 65 | ( 66 | if builtins.isInt spacing then 67 | spacing 68 | else 69 | builtins.elemAt 70 | [ 71 | 1 72 | 2 73 | 6 74 | ] 75 | ( 76 | lib.lists.findFirstIndex ( 77 | x: x == spacing 78 | ) (throw "systemTray: nonexistent spacing ${spacing}! This is a bug!") enum 79 | ) 80 | ) 81 | ); 82 | }; 83 | scaleToFit = mkBoolOption '' 84 | Whether to automatically scale System Tray icons to fix the available thickness of the panel. 85 | 86 | If false, tray icons will be capped at the smallMedium size (22px) and become a two-row/column 87 | layout when the panel is thick. 88 | ''; 89 | }; 90 | 91 | items = { 92 | showAll = mkBoolOption "If true, all system tray entries will always be in the main bar, outside the popup."; 93 | 94 | hidden = mkOption { 95 | type = types.nullOr (types.listOf types.str); 96 | default = null; 97 | example = [ 98 | # Plasmoid plugin example 99 | "org.kde.plasma.brightness" 100 | 101 | # StatusNotifier example 102 | "org.kde.plasma.addons.katesessions" 103 | ]; 104 | description = '' 105 | List of widgets that should be hidden from the main bar, only visible in the popup. 106 | 107 | Expects a list of plasmoid plugin IDs or StatusNotifier IDs. 108 | ''; 109 | }; 110 | 111 | shown = mkOption { 112 | type = types.nullOr (types.listOf types.str); 113 | default = null; 114 | example = [ 115 | # Plasmoid plugin example 116 | "org.kde.plasma.battery" 117 | 118 | # StatusNotifier example 119 | "org.kde.plasma.addons.katesessions" 120 | ]; 121 | description = '' 122 | List of widgets that should be shown in the main bar. 123 | 124 | Expects a list of plasmoid plugin IDs or StatusNotifier IDs. 125 | ''; 126 | }; 127 | 128 | extra = mkOption { 129 | type = types.nullOr (types.listOf types.str); 130 | default = null; 131 | example = [ "org.kde.plasma.battery" ]; 132 | description = '' 133 | List of extra widgets that are explicitly enabled in the system tray. 134 | 135 | Expects a list of plasmoid plugin IDs. 136 | ''; 137 | }; 138 | 139 | configs = mkOption { 140 | # The type here is deliberately NOT modelled exactly correctly, 141 | # to allow the apply function to provide better errors with the richer option and type system. 142 | type = types.attrsOf (types.attrsOf types.anything); 143 | default = { }; 144 | example = { 145 | # Example of a widget-specific config 146 | battery.showPercentage = true; 147 | keyboardLayout.displayStyle = "label"; 148 | 149 | # Example of raw config for an untyped widget 150 | "org.kde.plasma.devicenotifier".config.General = { 151 | removableDevices = false; 152 | nonRemovableDevices = true; 153 | }; 154 | }; 155 | description = '' 156 | Configurations for each widget in the tray. 157 | 158 | Uses widget-specific configs if the key is a known widget type, 159 | otherwise uses raw configs that's not specifically checked to be valid, 160 | or even idiomatic in Nix! 161 | ''; 162 | 163 | # You might be asking yourself... WTH is this? 164 | # Simply put, this thing allows us to apply the same defaults as defined by the options, 165 | # Instead of forcing downstream converters to provide defaults to everything *again*. 166 | # The way to do this is kind of cursed and honestly it might be easier if `lib.evalOptionValue` 167 | # is not recommended for public use. Oh well. 168 | apply = lib.mapAttrsToList ( 169 | name: config: 170 | let 171 | isKnownWidget = widgets.isKnownWidget name; 172 | # Raw widgets aren't wrapped in an extra attrset layer, unlike known ones 173 | # We wrap them back up to ensure the path is accurate 174 | loc = options.items.configs.loc ++ lib.optional (!isKnownWidget) name; 175 | in 176 | widgets.convert 177 | (lib.mergeDefinitions loc widgets.type [ 178 | { 179 | file = builtins.head options.items.configs.files; 180 | # Looks a bit funny, does the job just right. 181 | value = if isKnownWidget then { ${name} = config; } else config // { inherit name; }; 182 | } 183 | ]).mergedValue 184 | ); 185 | }; 186 | }; 187 | settings = mkOption { 188 | type = configValueType; 189 | default = null; 190 | description = "Extra configuration options for the widget."; 191 | apply = settings: if settings == null then { } else settings; 192 | }; 193 | } 194 | ); 195 | 196 | convert = 197 | { 198 | pin, 199 | icons, 200 | items, 201 | settings, 202 | ... 203 | }: 204 | let 205 | sets = { 206 | General = lib.filterAttrs (_: v: v != null) { 207 | inherit pin; 208 | extraItems = items.extra; 209 | hiddenItems = items.hidden; 210 | shownItems = items.shown; 211 | showAllItems = items.showAll; 212 | 213 | scaleIconsToFit = icons.scaleToFit; 214 | iconSpacing = icons.spacing; 215 | }; 216 | }; 217 | mergedSettings = lib.recursiveUpdate sets settings; 218 | in 219 | { 220 | name = "org.kde.plasma.systemtray"; 221 | extraConfig = '' 222 | (widget) => { 223 | const tray = desktopById(widget.readConfig("SystrayContainmentId")); 224 | if (!tray) return; // if somehow the containment doesn't exist 225 | 226 | ${widgets.lib.setWidgetSettings "tray" mergedSettings} 227 | ${widgets.lib.addWidgetStmts "tray" "trayWidgets" items.configs} 228 | } 229 | ''; 230 | }; 231 | }; 232 | } 233 | -------------------------------------------------------------------------------- /modules/window-rules.nix: -------------------------------------------------------------------------------- 1 | { lib, config, ... }: 2 | with lib.types; 3 | let 4 | inherit (builtins) 5 | length 6 | listToAttrs 7 | foldl' 8 | toString 9 | attrNames 10 | getAttr 11 | concatStringsSep 12 | add 13 | isAttrs 14 | ; 15 | inherit (lib) mkOption mkIf; 16 | inherit (lib.trivial) mergeAttrs; 17 | inherit (lib.lists) imap0; 18 | inherit (lib.attrsets) optionalAttrs filterAttrs mapAttrsToList; 19 | cfg = config.programs.plasma; 20 | applyRules = { 21 | "do-not-affect" = 1; 22 | "force" = 2; 23 | "initially" = 3; 24 | "remember" = 4; 25 | }; 26 | matchRules = { 27 | "exact" = 1; 28 | "substring" = 2; 29 | "regex" = 3; 30 | }; 31 | windowTypes = { 32 | normal = 1; 33 | desktop = 2; 34 | dock = 4; 35 | toolbar = 8; 36 | torn-of-menu = 16; 37 | dialog = 32; 38 | menubar = 128; 39 | utility = 256; 40 | spash = 512; 41 | osd = 65536; 42 | }; 43 | matchNameMap = { 44 | "window-class" = "wmclass"; 45 | "window-types" = "types"; 46 | "window-role" = "windowrole"; 47 | }; 48 | matchOptionType = 49 | hasMatchWhole: 50 | submodule { 51 | options = 52 | { 53 | value = mkOption { 54 | type = str; 55 | description = "Name to match."; 56 | }; 57 | type = mkOption { 58 | type = enum (attrNames matchRules); 59 | default = "exact"; 60 | description = "Name match type."; 61 | }; 62 | } 63 | // optionalAttrs hasMatchWhole { 64 | match-whole = mkOption { 65 | type = bool; 66 | default = true; 67 | description = "Match whole name."; 68 | }; 69 | }; 70 | }; 71 | basicValueType = oneOf [ 72 | bool 73 | float 74 | int 75 | str 76 | ]; 77 | applyOptionType = submodule { 78 | options = { 79 | value = mkOption { 80 | type = basicValueType; 81 | description = "Value to set."; 82 | }; 83 | apply = mkOption { 84 | type = enum (attrNames applyRules); 85 | default = "initially"; 86 | description = "How to apply the value."; 87 | }; 88 | }; 89 | }; 90 | mkMatchOption = 91 | name: hasMatchWhole: 92 | mkOption { 93 | type = nullOr (coercedTo str (value: { inherit value; }) (matchOptionType hasMatchWhole)); 94 | default = null; 95 | description = "${name} matching."; 96 | }; 97 | fixMatchName = name: matchNameMap.${name} or name; 98 | buildMatchRule = 99 | name: rule: 100 | ( 101 | { 102 | "${fixMatchName name}" = rule.value; 103 | "${fixMatchName name}match" = getAttr rule.type matchRules; 104 | } 105 | // optionalAttrs (rule ? match-whole) { "${fixMatchName name}complete" = rule.match-whole; } 106 | ); 107 | buildApplyRule = name: rule: { 108 | "${name}" = rule.value; 109 | "${name}rule" = getAttr rule.apply applyRules; 110 | }; 111 | buildWindowRule = 112 | rule: 113 | let 114 | matchOptions = filterAttrs (_name: isAttrs) rule.match; 115 | matchRules = mapAttrsToList buildMatchRule matchOptions; 116 | applyRules = mapAttrsToList buildApplyRule rule.apply; 117 | combinedRules = foldl' mergeAttrs { } (matchRules ++ applyRules); 118 | in 119 | { 120 | Description = rule.description; 121 | } 122 | // optionalAttrs (rule.match.window-types != 0) { types = rule.match.window-types; } 123 | // combinedRules; 124 | windowRules = listToAttrs ( 125 | imap0 (i: rule: { 126 | name = toString (i + 1); 127 | value = buildWindowRule rule; 128 | }) cfg.window-rules 129 | ); 130 | in 131 | { 132 | options.programs.plasma = { 133 | window-rules = mkOption { 134 | type = listOf (submodule { 135 | options = { 136 | match = mkOption { 137 | type = submodule { 138 | options = { 139 | window-class = mkMatchOption "Window class" true; 140 | window-role = mkMatchOption "Window role" false; 141 | title = mkMatchOption "Title" false; 142 | machine = mkMatchOption "clientmachine" false; 143 | window-types = mkOption { 144 | type = listOf (enum (attrNames windowTypes)); 145 | default = [ ]; 146 | description = "Window types to match."; 147 | apply = values: foldl' add 0 (map (val: getAttr val windowTypes) values); 148 | }; 149 | }; 150 | }; 151 | }; 152 | apply = mkOption { 153 | type = attrsOf (coercedTo basicValueType (value: { inherit value; }) applyOptionType); 154 | default = { }; 155 | description = "Values to apply."; 156 | }; 157 | description = mkOption { 158 | type = str; 159 | description = "Value to set."; 160 | }; 161 | }; 162 | }); 163 | description = "KWin window rules."; 164 | default = [ ]; 165 | }; 166 | }; 167 | 168 | config = mkIf (length cfg.window-rules > 0) { 169 | programs.plasma.configFile = { 170 | kwinrulesrc = { 171 | General = { 172 | count = length cfg.window-rules; 173 | rules = concatStringsSep "," (attrNames windowRules); 174 | }; 175 | } // windowRules; 176 | }; 177 | }; 178 | } 179 | -------------------------------------------------------------------------------- /modules/windows.nix: -------------------------------------------------------------------------------- 1 | # Window configuration: 2 | { config, lib, ... }: 3 | 4 | let 5 | cfg = config.programs.plasma; 6 | in 7 | { 8 | options.programs.plasma.windows = { 9 | allowWindowsToRememberPositions = lib.mkOption { 10 | type = with lib.types; nullOr bool; 11 | default = null; 12 | description = '' 13 | Allow apps to remember the positions of their own windows, if 14 | they support it. 15 | ''; 16 | }; 17 | }; 18 | 19 | config = ( 20 | lib.mkIf (cfg.enable && cfg.windows.allowWindowsToRememberPositions != null) { 21 | programs.plasma.configFile = { 22 | kdeglobals = { 23 | General.AllowKDEAppsToRememberWindowPositions = cfg.windows.allowWindowsToRememberPositions; 24 | }; 25 | }; 26 | } 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /script/rc2nix.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ################################################################################ 4 | # 5 | # This file is part of the package Plasma Manager. It is subject to 6 | # the license terms in the LICENSE file found in the top-level 7 | # directory of this distribution and at: 8 | # 9 | # https://github.com/nix-community/plasma-manager 10 | # 11 | # No part of this package, including this file, may be copied, 12 | # modified, propagated, or distributed except according to the terms 13 | # contained in the LICENSE file. 14 | # 15 | ################################################################################ 16 | require("optparse") 17 | require("pathname") 18 | 19 | ################################################################################ 20 | module Rc2Nix 21 | 22 | ############################################################################## 23 | # The root directory where configuration files are stored. 24 | XDG_CONFIG_HOME = File.expand_path(ENV["XDG_CONFIG_HOME"] || "~/.config") 25 | 26 | ############################################################################## 27 | # Files that we'll scan by default. 28 | KNOWN_FILES = [ 29 | "kcminputrc", 30 | "kglobalshortcutsrc", 31 | "kactivitymanagerdrc", 32 | "ksplashrc", 33 | "kwin_rules_dialogrc", 34 | "kmixrc", 35 | "kwalletrc", 36 | "kgammarc", 37 | "krunnerrc", 38 | "klaunchrc", 39 | "plasmanotifyrc", 40 | "systemsettingsrc", 41 | "kscreenlockerrc", 42 | "kwinrulesrc", 43 | "khotkeysrc", 44 | "ksmserverrc", 45 | "kded5rc", 46 | "plasmarc", 47 | "kwinrc", 48 | "kdeglobals", 49 | "baloofilerc", 50 | "dolphinrc", 51 | "klipperrc", 52 | "plasma-localerc", 53 | "kxkbrc", 54 | "ffmpegthumbsrc", 55 | "kservicemenurc", 56 | "kiorc", 57 | ].map {|f| File.expand_path(f, XDG_CONFIG_HOME)}.freeze 58 | 59 | ############################################################################## 60 | class RcFile 61 | 62 | ############################################################################ 63 | # Any group that matches a listed regular expression is blocked 64 | # from being passed through to the settings attribute. 65 | # 66 | # This is necessary because KDE currently stores application state 67 | # in configuration files. 68 | GROUP_BLOCK_LIST = [ 69 | /^(ConfigDialog|FileDialogSize|ViewPropertiesDialog|KPropertiesDialog)$/, 70 | /^\$Version$/, 71 | /^ColorEffects:/, 72 | /^Colors:/, 73 | /^DoNotDisturb$/, 74 | /^LegacySession:/, 75 | /^MainWindow$/, 76 | /^PlasmaViews/, 77 | /^ScreenConnectors$/, 78 | /^Session:/, 79 | ] 80 | 81 | ############################################################################ 82 | # Similar to the GROUP_BLOCK_LIST but for setting keys. 83 | KEY_BLOCK_LIST = [ 84 | /^activate widget \d+$/, # Depends on state :( 85 | /^ColorScheme(Hash)?$/, 86 | /^History Items/, 87 | /^LookAndFeelPackage$/, 88 | /^Recent (Files|URLs)/, 89 | /^Theme$/i, 90 | /^Version$/, 91 | /State$/, 92 | /Timestamp$/, 93 | ] 94 | 95 | ############################################################################ 96 | # List of functions that get called with a group name and a key 97 | # name. If the function returns +true+ then block that key. 98 | BLOCK_LIST_LAMBDA = [ 99 | ->(group, key) { group == "org.kde.kdecoration2" && key == "library" } 100 | ] 101 | 102 | ############################################################################ 103 | attr_reader(:file_name, :settings) 104 | 105 | ############################################################################ 106 | def initialize(file_name) 107 | @file_name = file_name 108 | @settings = {} 109 | @last_group = nil 110 | end 111 | 112 | ############################################################################ 113 | def parse 114 | File.open(@file_name) do |file| 115 | file.each do |line| 116 | case line 117 | when /^\s*$/ 118 | next 119 | when /^\s*(\[[^\]]+\]){1,}\s*$/ 120 | @last_group = parse_group(line.strip) 121 | when /^\s*([^=]+)=?(.*)\s*$/ 122 | key = $1.strip 123 | val = $2.strip 124 | 125 | if @last_group.nil? 126 | raise("#{@file_name}: setting outside of group: #{line}") 127 | end 128 | 129 | # Reasons to skip this group or key: 130 | next if GROUP_BLOCK_LIST.any? {|re| @last_group.match(re)} 131 | next if KEY_BLOCK_LIST.any? {|re| key.match(re)} 132 | next if BLOCK_LIST_LAMBDA.any? {|fn| fn.call(@last_group, key)} 133 | next if File.basename(@file_name) == "plasmanotifyrc" && key == "Seen" 134 | 135 | @settings[@last_group] ||= {} 136 | @settings[@last_group][key] = val 137 | else 138 | raise("#{@file_name}: can't parse line: #{line}") 139 | end 140 | end 141 | end 142 | end 143 | 144 | ############################################################################ 145 | def parse_group(line) 146 | line.gsub("/", "\\\\\\/").gsub(/\s*\[([^\]]+)\]\s*/) do |match| 147 | $1 + "/" 148 | end.sub(/\/$/, '') 149 | end 150 | end 151 | 152 | ############################################################################## 153 | class App 154 | 155 | ############################################################################ 156 | def initialize(args) 157 | @files = KNOWN_FILES.dup 158 | 159 | OptionParser.new do |p| 160 | p.on("-h", "--help", "This message") {$stdout.puts(p); exit} 161 | 162 | p.on("-c", "--clear", "Clear the file list") do 163 | @files = [] 164 | end 165 | 166 | p.on("-a", "--add=FILE", "Add a file to the scan list") do |file| 167 | @files << File.expand_path(file) 168 | end 169 | end.parse!(args) 170 | end 171 | 172 | ############################################################################ 173 | def run 174 | settings = {} 175 | 176 | @files.each do |file| 177 | next unless File.exist?(file) 178 | 179 | rc = RcFile.new(file) 180 | rc.parse 181 | 182 | path = Pathname.new(file).relative_path_from(XDG_CONFIG_HOME) 183 | settings[File.path(path)] = rc.settings 184 | end 185 | 186 | puts("{") 187 | puts(" programs.plasma = {") 188 | puts(" enable = true;") 189 | puts(" shortcuts = {") 190 | pp_shortcuts(settings["kglobalshortcutsrc"], 6) 191 | puts(" };") 192 | puts(" configFile = {") 193 | pp_settings(settings, 6) 194 | puts(" };") 195 | puts(" };") 196 | puts("}") 197 | end 198 | 199 | ############################################################################ 200 | def pp_settings(settings, indent) 201 | settings.keys.sort.each do |file| 202 | settings[file].keys.sort.each do |group| 203 | settings[file][group].keys.sort.each do |key| 204 | next if file == "kglobalshortcutsrc" && key != "_k_friendly_name" 205 | 206 | print(" " * indent) 207 | print("\"#{file}\".") 208 | print("\"#{group}\".") 209 | print("\"#{key}\" = ") 210 | print(nix_val(settings[file][group][key])) 211 | print(";\n") 212 | end 213 | end 214 | end 215 | end 216 | 217 | ############################################################################ 218 | def pp_shortcuts(groups, indent) 219 | return if groups.nil? 220 | 221 | groups.keys.sort.each do |group| 222 | groups[group].keys.sort.each do |action| 223 | next if action == "_k_friendly_name" 224 | 225 | print(" " * indent) 226 | print("\"#{group}\".") 227 | print("\"#{action}\" = ") 228 | 229 | keys = groups[group][action]. 230 | split(/(? 1 238 | print("[" + keys.map {|k| nix_val(k)}.join(" ") + "]") 239 | elsif keys.first == "none" 240 | print("[ ]") 241 | else 242 | print(nix_val(keys.first)) 243 | end 244 | 245 | print(";\n") 246 | end 247 | end 248 | end 249 | 250 | ############################################################################ 251 | def nix_val(str) 252 | case str 253 | when NilClass 254 | "null" 255 | when /^true|false$/i 256 | str.downcase 257 | when /^[0-9]+(\.[0-9]+)?$/ 258 | str 259 | else 260 | '"' + str.gsub(/(?&2 "ERROR: $@: expected $want but got $actual" 26 | exit 1 27 | fi 28 | } 29 | 30 | assert_eq false --group KDE --key SingleClick 31 | # Set with shorthand 32 | assert_eq 1 --group group --key key1 33 | # Set with longhand and immutable 34 | assert_eq 2 --group group --key key2 35 | # Nested groups, with group containing / 36 | assert_eq 3 --group escaped/nested --group group --key key3 37 | # Value and key have leading space 38 | assert_eq " leading space" --group group --key " leading space" 39 | # Set outside plasma-manager, value has leading space, group contains / 40 | assert_eq " value" --group escaped/nested --group group --key untouched 41 | # Escaped key with shell expansion 42 | assert_eq "/home/fake" --group group --key 'escaped[$i]' 43 | ''; 44 | in 45 | testers.nixosTest { 46 | name = "plasma-basic"; 47 | 48 | nodes.machine = { 49 | environment.systemPackages = [ script ]; 50 | imports = [ home-manager-module ]; 51 | 52 | users.users.fake = { 53 | createHome = true; 54 | isNormalUser = true; 55 | }; 56 | 57 | home-manager.users.fake = 58 | { lib, ... }: 59 | { 60 | home.stateVersion = "23.11"; 61 | imports = [ plasma-module ]; 62 | programs.plasma = { 63 | enable = true; 64 | workspace.clickItemTo = "select"; 65 | # Test a variety of weird keys and groups 66 | configFile.kdeglobals = { 67 | group = { 68 | " leading space" = " leading space"; 69 | key1 = 1; 70 | key2 = { 71 | value = 2; 72 | immutable = true; 73 | }; 74 | "escaped[$i]" = { 75 | value = "\${HOME}"; 76 | shellExpand = true; 77 | }; 78 | }; 79 | "escaped\\/nested/group" = { 80 | key3 = 3; 81 | }; 82 | }; 83 | }; 84 | home.activation.preseed = lib.hm.dag.entryBefore [ "configure-plasma" ] '' 85 | mkdir -p ~/.config 86 | cat <> ~/.config/kdeglobals 87 | [escaped/nested][group] 88 | untouched = \svalue 89 | EOF 90 | ''; 91 | }; 92 | }; 93 | 94 | testScript = '' 95 | # Boot: 96 | start_all() 97 | machine.wait_for_unit("multi-user.target") 98 | machine.wait_for_unit("nix-daemon.socket") 99 | 100 | machine.wait_until_succeeds( 101 | "systemctl show -p ActiveState --value home-manager-fake.service | grep -q 'inactive' && " + 102 | "systemctl show -p Result --value home-manager-fake.service | grep -q 'success'" 103 | ) 104 | 105 | # Run tests: 106 | machine.succeed("test -e /home/fake/.config/kdeglobals") 107 | machine.succeed("su - fake -c plasma-basic-test") 108 | ''; 109 | } 110 | -------------------------------------------------------------------------------- /test/demo.nix: -------------------------------------------------------------------------------- 1 | { home-manager-module, plasma-module }: 2 | 3 | { modulesPath, ... }: 4 | { 5 | imports = [ 6 | (modulesPath + "/profiles/qemu-guest.nix") 7 | (modulesPath + "/virtualisation/qemu-vm.nix") 8 | home-manager-module 9 | ]; 10 | 11 | config = { 12 | networking.hostName = "plasma-demo"; 13 | 14 | fileSystems."/" = { 15 | device = "/dev/disk/by-label/nixos"; 16 | fsType = "ext4"; 17 | autoResize = true; 18 | }; 19 | 20 | boot = { 21 | growPartition = true; 22 | loader.timeout = 5; 23 | kernelParams = [ 24 | "console=ttyS0" 25 | "boot.shell_on_fail" 26 | ]; 27 | }; 28 | 29 | virtualisation.forwardPorts = [ 30 | { 31 | from = "host"; 32 | host.port = 2222; 33 | guest.port = 22; 34 | } 35 | ]; 36 | 37 | services.xserver.enable = true; 38 | services.displayManager = { 39 | autoLogin.user = "fake"; 40 | autoLogin.enable = true; 41 | defaultSession = "plasma"; 42 | sddm.enable = true; 43 | }; 44 | services.desktopManager.plasma6.enable = true; 45 | 46 | system.stateVersion = "23.11"; 47 | 48 | users.users.fake = { 49 | createHome = true; 50 | isNormalUser = true; 51 | password = "password"; 52 | group = "users"; 53 | }; 54 | 55 | home-manager.users.fake = { 56 | home.stateVersion = "22.05"; 57 | imports = [ plasma-module ]; 58 | }; 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /test/rc2nix/test_data/kcminputrc: -------------------------------------------------------------------------------- 1 | [Keyboard] 2 | KeyboardRepeating=0 3 | NumLock=2 4 | RepeatDelay=250 5 | RepeatRate=30 6 | 7 | [Mouse] 8 | X11LibInputXAccelProfileFlat=true 9 | XLbInptPointerAcceleration=1 10 | cursorTheme=Oxygen_White 11 | 12 | [Tmp] 13 | update_info=delete_cursor_old_default_size.upd:DeleteCursorOldDefaultSize,kcminputrc_repeat.upd:kcminputrc_migrate_repeat_value 14 | 15 | [Libinput][2][14][ETPS/2 Elantech Touchpad] 16 | NaturalScroll=true 17 | -------------------------------------------------------------------------------- /test/rc2nix/test_data/kglobalshortcutsrc: -------------------------------------------------------------------------------- 1 | [kwin] 2 | Switch to Desktop 1=Meta+1,, 3 | Switch to Desktop 2=Meta+2,, 4 | Switch to Desktop 3=Meta+3,, 5 | Switch to Desktop 4=Meta+4,, 6 | Switch to Desktop 5=Meta+5,, 7 | Switch to Desktop 6=Meta+6,, 8 | Switch to Desktop 7=Meta+7,, 9 | Switch to Desktop 8=Meta+8,, 10 | -------------------------------------------------------------------------------- /test/rc2nix/test_data/kglobalshortcutsrc.bak: -------------------------------------------------------------------------------- 1 | [kwin] 2 | Switch to Desktop 1=Meta+1,, 3 | Switch to Desktop 2=Meta+2,, 4 | Switch to Desktop 3=Meta+3,, 5 | Switch to Desktop 4=Meta+4,, 6 | Switch to Desktop 5=Meta+5,, 7 | Switch to Desktop 6=Meta+6,, 8 | Switch to Desktop 7=Meta+7,, 9 | Switch to Desktop 8=Meta+8,, 10 | Window to Desktop 1=Meta+!,, 11 | Window to Desktop 2=Meta+",, 12 | Window to Desktop 3=Meta+#,, 13 | Window to Desktop 4=Meta+¤,, 14 | Window to Desktop 5=Meta+%,, 15 | Window to Desktop 6=Meta+&,, 16 | Window to Desktop 7=Meta+/,, 17 | Window to Desktop 8=Meta+(,, 18 | -------------------------------------------------------------------------------- /test/rc2nix/test_data/krunnerrc: -------------------------------------------------------------------------------- 1 | [General] 2 | FreeFloating=true 3 | -------------------------------------------------------------------------------- /test/rc2nix/test_data/kscreenlockerrc: -------------------------------------------------------------------------------- 1 | [Greeter] 2 | WallpaperPlugin=org.kde.potd 3 | 4 | [Greeter][Wallpaper][org.kde.potd][General] 5 | Provider=bing 6 | UpdateOverMeteredConnection=0 7 | -------------------------------------------------------------------------------- /test/rc2nix/test_data/kwinrc: -------------------------------------------------------------------------------- 1 | [Desktops] 2 | Number=8 3 | Rows=2 4 | 5 | [Effect-overview] 6 | BorderActivate=9 7 | 8 | [Plugins] 9 | shakecursorEnabled=true 10 | -------------------------------------------------------------------------------- /test/rc2nix/test_rc2nix.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix 2 | #! nix shell nixpkgs#python3Packages.python nixpkgs#ruby -c python3 3 | import os 4 | import subprocess 5 | import unittest 6 | 7 | 8 | def red(s: str) -> str: 9 | return "\033[91m" + s + "\033[0m" 10 | 11 | 12 | def green(s: str) -> str: 13 | return "\033[32m" + s + "\033[0m" 14 | 15 | 16 | def gray(s: str) -> str: 17 | return "\033[90m" + s + "\033[0m" 18 | 19 | 20 | current_dir = os.path.dirname(os.path.abspath(__file__)) 21 | 22 | 23 | def path(relative_path: str) -> str: 24 | return os.path.abspath(os.path.join(current_dir, relative_path)) 25 | 26 | 27 | rc2nix_py = path("../../script/rc2nix.py") 28 | rc2nix_rb = path("../../script/rc2nix.rb") 29 | 30 | 31 | class TestRc2nix(unittest.TestCase): 32 | 33 | def test(self): 34 | def run_script(*command: str) -> str: 35 | rst = subprocess.run( 36 | command, 37 | env={ 38 | "XDG_CONFIG_HOME": path("./test_data"), 39 | "PATH": os.environ["PATH"], 40 | }, 41 | stdout=subprocess.PIPE, 42 | stderr=subprocess.PIPE, 43 | text=True, 44 | ) 45 | print(red(rst.stderr)) 46 | rst.check_returncode() 47 | return rst.stdout 48 | 49 | rst_py = run_script(rc2nix_py) 50 | rst_rb = run_script(rc2nix_rb) 51 | 52 | self.assertEqual(rst_py.splitlines(), rst_rb.splitlines()) 53 | 54 | 55 | if __name__ == "__main__": # pragma: no cover 56 | _ = unittest.main() 57 | -------------------------------------------------------------------------------- /treefmt.toml: -------------------------------------------------------------------------------- 1 | [formatter.nixfmt-rfc-style] 2 | command = "nixfmt" 3 | includes = ["*.nix"] 4 | 5 | [formatter.black] 6 | command = "black" 7 | includes = ["*.py", "*.pyi"] 8 | 9 | [formatter.isort] 10 | command = "isort" 11 | includes = ["*.py", "*.pyi"] 12 | --------------------------------------------------------------------------------