diff --git a/modules/misc/news/2025/11/2025-11-06_12-50-53.nix b/modules/misc/news/2025/11/2025-11-06_12-50-53.nix new file mode 100644 index 000000000..6ed52dee1 --- /dev/null +++ b/modules/misc/news/2025/11/2025-11-06_12-50-53.nix @@ -0,0 +1,9 @@ +{ + time = "2025-11-06T11:50:53+00:00"; + condition = true; + message = '' + A new module is available: 'services.tomat'. + + tomat is a Pomodoro timer for status bars and the command line. It is easily configurable and support both desktop and sound notifications. + ''; +} diff --git a/modules/programs/aerospace.nix b/modules/programs/aerospace.nix index 1b5e4ab92..5db064f10 100644 --- a/modules/programs/aerospace.nix +++ b/modules/programs/aerospace.nix @@ -99,194 +99,7 @@ in }; userSettings = mkOption { - type = types.submodule { - freeformType = tomlFormat.type; - options = { - after-startup-command = mkOption { - type = with types; listOf str; - default = [ ]; - description = '' - A list of AeroSpace commands to execute immediately after the AeroSpace application starts. - These commands are written to your `aerospace.toml` config file and are run after the `after-login-command` sequence. - - A list of all available commands can be found at . - - While this module checks for valid command names, using incorrect *arguments* can still cause issues. - If AeroSpace is not behaving correctly after startup, check the logs for errors with `cat /tmp/aerospace.err.log`. - ''; - example = [ - "exec-and-forget open -n /System/Applications/Utilities/Terminal.app" - "layout tiles accordion" - ]; - }; - enable-normalization-flatten-containers = mkOption { - type = types.bool; - default = true; - description = ''Containers that have only one child are "flattened".''; - }; - enable-normalization-opposite-orientation-for-nested-containers = mkOption { - type = types.bool; - default = true; - description = "Containers that nest into each other must have opposite orientations."; - }; - accordion-padding = mkOption { - type = types.int; - default = 30; - description = "Padding between windows in an accordion container."; - }; - default-root-container-layout = mkOption { - type = types.enum [ - "tiles" - "accordion" - ]; - default = "tiles"; - description = "Default layout for the root container."; - }; - default-root-container-orientation = mkOption { - type = types.enum [ - "horizontal" - "vertical" - "auto" - ]; - default = "auto"; - description = "Default orientation for the root container."; - }; - on-window-detected = mkOption { - type = types.listOf ( - types.submodule { - options = { - "if" = mkOption { - type = types.submodule { - options = { - app-id = mkOption { - type = with types; nullOr str; - default = null; - description = "The application ID to match (optional)."; - }; - workspace = mkOption { - type = with types; nullOr str; - default = null; - description = "The workspace name to match (optional)."; - }; - window-title-regex-substring = mkOption { - type = with types; nullOr str; - default = null; - description = "Substring to match in the window title (optional)."; - }; - app-name-regex-substring = mkOption { - type = with types; nullOr str; - default = null; - description = "Regex substring to match the app name (optional)."; - }; - during-aerospace-startup = mkOption { - type = with types; nullOr bool; - default = null; - description = "Whether to match during aerospace startup (optional)."; - }; - }; - }; - default = { }; - description = "Conditions for detecting a window."; - }; - check-further-callbacks = mkOption { - type = with types; nullOr bool; - default = null; - description = "Whether to check further callbacks after this rule (optional)."; - }; - run = mkOption { - type = - with types; - oneOf [ - str - (listOf str) - ]; - example = [ - "move-node-to-workspace m" - "resize-node" - ]; - description = "Commands to execute when the conditions match (required)."; - }; - }; - } - ); - default = [ ]; - example = [ - { - "if" = { - app-id = "Another.Cool.App"; - workspace = "cool-workspace"; - window-title-regex-substring = "Title"; - app-name-regex-substring = "CoolApp"; - during-aerospace-startup = false; - }; - check-further-callbacks = false; - run = [ - "move-node-to-workspace m" - "resize-node" - ]; - } - ]; - description = "Commands to run every time a new window is detected with optional conditions."; - }; - workspace-to-monitor-force-assignment = mkOption { - type = - with types; - nullOr ( - attrsOf (oneOf [ - int - str - (listOf str) - ]) - ); - default = null; - description = '' - Map workspaces to specific monitors. - Left-hand side is the workspace name, and right-hand side is the monitor pattern. - ''; - example = { - "1" = 1; # First monitor from left to right. - "2" = "main"; # Main monitor. - "3" = "secondary"; # Secondary monitor (non-main). - "4" = "built-in"; # Built-in display. - "5" = "^built-in retina display$"; # Regex for the built-in retina display. - "6" = [ - "secondary" - "dell" - ]; # Match first pattern in the list. - }; - }; - on-focus-changed = mkOption { - type = with types; listOf str; - default = [ ]; - example = [ "move-mouse monitor-lazy-center" ]; - description = "Commands to run every time focused window or workspace changes."; - }; - on-focused-monitor-changed = mkOption { - type = with types; listOf str; - default = [ "move-mouse monitor-lazy-center" ]; - description = "Commands to run every time focused monitor changes."; - }; - exec-on-workspace-change = mkOption { - type = with types; listOf str; - default = [ ]; - example = [ - "/bin/bash" - "-c" - "sketchybar --trigger aerospace_workspace_change FOCUSED_WORKSPACE=$AEROSPACE_FOCUSED_WORKSPACE" - ]; - description = "Commands to run every time workspace changes."; - }; - key-mapping.preset = mkOption { - type = types.enum [ - "qwerty" - "dvorak" - "colemak" - ]; - default = "qwerty"; - description = "Keymapping preset."; - }; - }; - }; + inherit (tomlFormat) type; default = { }; example = lib.literalExpression '' { diff --git a/modules/programs/fish.nix b/modules/programs/fish.nix index 49922ef50..1bc5b54c5 100644 --- a/modules/programs/fish.nix +++ b/modules/programs/fish.nix @@ -153,6 +153,17 @@ let }; }; + completionModule = types.submodule { + options = { + body = mkOption { + type = types.lines; + description = '' + The completion file's body. + ''; + }; + }; + }; + abbrModule = types.submodule { options = { expansion = mkOption { @@ -556,6 +567,28 @@ in . ''; }; + + programs.fish.completions = mkOption { + type = with types; attrsOf (either lines completionModule); + default = { }; + example = literalExpression '' + { + my-prog = ''' + complete -c myprog -s o -l output + '''; + + my-app = { + body = ''' + complete -c myapp -s -v + '''; + }; + } + ''; + description = '' + Custom fish completions. For more information see + . + ''; + }; }; config = mkIf cfg.enable ( @@ -734,6 +767,20 @@ in }; }) cfg.functions; } + { + xdg.configFile = lib.mapAttrs' (name: def: { + name = "fish/completions/${name}.fish"; + value = { + source = + let + body = if isAttrs def then def.body else def; + in + fishIndent "${name}.fish" '' + ${lib.strings.removeSuffix "\n" body} + ''; + }; + }) cfg.completions; + } # Each plugin gets a corresponding conf.d/plugin-NAME.fish file to load # in the paths and any initialization scripts. diff --git a/modules/programs/lazygit.nix b/modules/programs/lazygit.nix index 0244ae217..e7535ae84 100644 --- a/modules/programs/lazygit.nix +++ b/modules/programs/lazygit.nix @@ -103,13 +103,11 @@ in ''; fishIntegration = '' - function ${cfg.shellWrapperName} - set -x LAZYGIT_NEW_DIR_FILE ${lazygitNewDirFilePath} - command lazygit $argv - if test -f $LAZYGIT_NEW_DIR_FILE - cd (cat $LAZYGIT_NEW_DIR_FILE) - rm -f $LAZYGIT_NEW_DIR_FILE - end + set -x LAZYGIT_NEW_DIR_FILE ${lazygitNewDirFilePath} + command lazygit $argv + if test -f $LAZYGIT_NEW_DIR_FILE + cd (cat $LAZYGIT_NEW_DIR_FILE) + rm -f $LAZYGIT_NEW_DIR_FILE end ''; diff --git a/modules/services/tomat.nix b/modules/services/tomat.nix new file mode 100644 index 000000000..95189cca1 --- /dev/null +++ b/modules/services/tomat.nix @@ -0,0 +1,74 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.tomat; + tomlFormat = pkgs.formats.toml { }; +in +{ + meta.maintainers = with lib.maintainers; [ jolars ]; + + options.services.tomat = { + enable = lib.mkEnableOption "Tomat Pomodoro server"; + + package = lib.mkPackageOption pkgs "tomat" { }; + + settings = lib.mkOption { + type = lib.types.submodule { freeformType = tomlFormat.type; }; + + default = { }; + + example = { + timer = { + work = 25; + break = 5; + auto_advance = false; + }; + + sound = { + enabled = true; + }; + + notification = { + enabled = true; + }; + }; + + description = '' + Tomat configuration. + See for supported values. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + home.packages = [ cfg.package ]; + + xdg.configFile = { + "tomat/config.toml" = lib.mkIf (cfg.settings != { }) { + source = tomlFormat.generate "tomat-config.toml" cfg.settings; + }; + }; + + systemd.user.services.tomat = { + Unit = { + Description = "Tomat Pomodoro server"; + After = [ "graphical.target" ]; + }; + + Service = { + ExecStart = "${lib.getExe cfg.package} daemon run"; + Restart = "always"; + RestartSec = 5; + }; + + Install = { + WantedBy = [ "default.target" ]; + }; + }; + }; +} diff --git a/tests/modules/programs/aerospace/colemak-settings-expected.toml b/tests/modules/programs/aerospace/colemak-settings-expected.toml index 1ed14e998..2b440a6bc 100644 --- a/tests/modules/programs/aerospace/colemak-settings-expected.toml +++ b/tests/modules/programs/aerospace/colemak-settings-expected.toml @@ -1,14 +1,3 @@ -accordion-padding = 30 -after-startup-command = [] -default-root-container-layout = "tiles" -default-root-container-orientation = "auto" -enable-normalization-flatten-containers = true -enable-normalization-opposite-orientation-for-nested-containers = true -exec-on-workspace-change = [] -on-focus-changed = [] -on-focused-monitor-changed = ["move-mouse monitor-lazy-center"] -on-window-detected = [] - [gaps.outer] bottom = 8 left = 8 diff --git a/tests/modules/programs/aerospace/settings-expected.toml b/tests/modules/programs/aerospace/settings-expected.toml index b46ebad41..29d9e3fbb 100644 --- a/tests/modules/programs/aerospace/settings-expected.toml +++ b/tests/modules/programs/aerospace/settings-expected.toml @@ -1,14 +1,4 @@ -accordion-padding = 30 after-login-command = [] -after-startup-command = [] -default-root-container-layout = "tiles" -default-root-container-orientation = "auto" -enable-normalization-flatten-containers = true -enable-normalization-opposite-orientation-for-nested-containers = true -exec-on-workspace-change = [] -on-focus-changed = [] -on-focused-monitor-changed = ["move-mouse monitor-lazy-center"] -on-window-detected = [] start-at-login = false [gaps.outer] @@ -17,9 +7,6 @@ left = 8 right = 8 top = 8 -[key-mapping] -preset = "qwerty" - [mode.main.binding] alt-h = "focus left" alt-j = "focus down" diff --git a/tests/modules/programs/fish/completions.nix b/tests/modules/programs/fish/completions.nix new file mode 100644 index 000000000..069255d27 --- /dev/null +++ b/tests/modules/programs/fish/completions.nix @@ -0,0 +1,45 @@ +{ lib, pkgs, ... }: +let + myProg = pkgs.writeText "my-prog.fish" '' + complete -c myprog -s o -l output + ''; + + myApp = pkgs.writeText "my-app.fish" '' + complete -c myapp -s -v + ''; +in +{ + config = { + programs.fish = { + enable = true; + + completions = { + my-prog = '' + complete -c myprog -s o -l output + ''; + my-app = { + body = '' + complete -c myapp -s -v + ''; + }; + }; + }; + + xdg.dataFile."fish/home-manager_generated_completions".source = lib.mkForce ( + builtins.toFile "empty" "" + ); + + nmt = { + description = "if fish.completions is set, check file exists and contents match"; + script = '' + assertFileExists home-files/.config/fish/completions/my-prog.fish + echo ${myProg} + assertFileContent home-files/.config/fish/completions/my-prog.fish ${myProg} + + assertFileExists home-files/.config/fish/completions/my-app.fish + echo ${myApp} + assertFileContent home-files/.config/fish/completions/my-app.fish ${myApp} + ''; + }; + }; +} diff --git a/tests/modules/programs/fish/default.nix b/tests/modules/programs/fish/default.nix index ff1bc7582..6ee6aa2a7 100644 --- a/tests/modules/programs/fish/default.nix +++ b/tests/modules/programs/fish/default.nix @@ -2,6 +2,7 @@ fish-abbrs = ./abbrs.nix; fish-format-scripts = ./format-scripts.nix; fish-functions = ./functions.nix; + fish-completions = ./completions.nix; fish-no-functions = ./no-functions.nix; fish-plugins = ./plugins.nix; fish-manpage = ./manpage.nix; diff --git a/tests/modules/programs/lazygit/default.nix b/tests/modules/programs/lazygit/default.nix new file mode 100644 index 000000000..106c564a7 --- /dev/null +++ b/tests/modules/programs/lazygit/default.nix @@ -0,0 +1,3 @@ +{ + lazygit-fish-integration-enabled = ./fish-integration-enabled.nix; +} diff --git a/tests/modules/programs/lazygit/fish-integration-enabled.nix b/tests/modules/programs/lazygit/fish-integration-enabled.nix new file mode 100644 index 000000000..f7183ca4c --- /dev/null +++ b/tests/modules/programs/lazygit/fish-integration-enabled.nix @@ -0,0 +1,18 @@ +{ config, ... }: + +{ + programs.fish.enable = true; + + home.preferXdgDirectories = false; + + programs.lazygit = { + enable = true; + shellWrapperName = "lg"; + enableFishIntegration = true; + }; + + nmt.script = '' + assertFileContent home-files/.config/fish/functions/${config.programs.lazygit.shellWrapperName}.fish \ + ${./fish-integration-expected.fish} + ''; +} diff --git a/tests/modules/programs/lazygit/fish-integration-expected.fish b/tests/modules/programs/lazygit/fish-integration-expected.fish new file mode 100644 index 000000000..84d64a2f6 --- /dev/null +++ b/tests/modules/programs/lazygit/fish-integration-expected.fish @@ -0,0 +1,8 @@ +function lg + set -x LAZYGIT_NEW_DIR_FILE ~/.lazygit/newdir + command lazygit $argv + if test -f $LAZYGIT_NEW_DIR_FILE + cd (cat $LAZYGIT_NEW_DIR_FILE) + rm -f $LAZYGIT_NEW_DIR_FILE + end +end diff --git a/tests/modules/services/tomat/default.nix b/tests/modules/services/tomat/default.nix new file mode 100644 index 000000000..589fc5291 --- /dev/null +++ b/tests/modules/services/tomat/default.nix @@ -0,0 +1,5 @@ +{ lib, pkgs, ... }: + +lib.optionalAttrs pkgs.stdenv.hostPlatform.isLinux { + tomat-service = ./tomat.nix; +} diff --git a/tests/modules/services/tomat/expected-config.toml b/tests/modules/services/tomat/expected-config.toml new file mode 100644 index 000000000..8bf29ac8e --- /dev/null +++ b/tests/modules/services/tomat/expected-config.toml @@ -0,0 +1,3 @@ +[timer] +break = 10 +work = 60 diff --git a/tests/modules/services/tomat/expected.service b/tests/modules/services/tomat/expected.service new file mode 100644 index 000000000..dce396a2d --- /dev/null +++ b/tests/modules/services/tomat/expected.service @@ -0,0 +1,11 @@ +[Install] +WantedBy=default.target + +[Service] +ExecStart=@tomat@/bin/tomat daemon run +Restart=always +RestartSec=5 + +[Unit] +After=graphical.target +Description=Tomat Pomodoro server diff --git a/tests/modules/services/tomat/tomat.nix b/tests/modules/services/tomat/tomat.nix new file mode 100644 index 000000000..c0c6ade88 --- /dev/null +++ b/tests/modules/services/tomat/tomat.nix @@ -0,0 +1,25 @@ +{ + services.tomat = { + enable = true; + + settings = { + timer = { + break = 10; + work = 60; + }; + }; + }; + + nmt.script = + let + serviceFile = "home-files/.config/systemd/user/tomat.service"; + configFile = "home-files/.config/tomat/config.toml"; + in + '' + assertFileExists "${serviceFile}" + assertFileExists "${configFile}" + + assertFileContent "${serviceFile}" ${./expected.service} + assertFileContent "${configFile}" ${./expected-config.toml} + ''; +}