diff --git a/modules/modules.nix b/modules/modules.nix
index 26d7692f3..e992e43d0 100644
--- a/modules/modules.nix
+++ b/modules/modules.nix
@@ -419,6 +419,7 @@ let
./services/safeeyes.nix
./services/screen-locker.nix
./services/sctd.nix
+ ./services/shikane.nix
./services/signaturepdf.nix
./services/skhd.nix
./services/snixembed.nix
diff --git a/modules/services/shikane.nix b/modules/services/shikane.nix
new file mode 100644
index 000000000..8d44a1dfa
--- /dev/null
+++ b/modules/services/shikane.nix
@@ -0,0 +1,91 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+let
+ cfg = config.services.shikane;
+ tomlFormat = pkgs.formats.toml { };
+in
+{
+ meta.maintainers = [ lib.maintainers.therealr5 ];
+
+ options.services.shikane = {
+
+ enable = lib.mkEnableOption "shikane, A dynamic output configuration tool that automatically detects and configures connected outputs based on a set of profiles.";
+
+ package = lib.mkPackageOption pkgs "shikane" { };
+
+ settings = lib.mkOption {
+ type = tomlFormat.type;
+ default = { };
+ example = lib.literalExpression ''
+ {
+ profile = [
+ {
+ name = "external-monitor-default";
+ output = [
+ {
+ match = "eDP-1";
+ enable = true;
+ }
+ {
+ match = "HDMI-A-1";
+ enable = true;
+ position = {
+ x = 1920;
+ y = 0;
+ };
+ }
+ ];
+ }
+ {
+ name = "builtin-monitor-only";
+ output = [
+ {
+ match = "eDP-1";
+ enable = true;
+ }
+ ];
+ }
+ ];
+ }
+ '';
+ description = ''
+ Configuration written to
+ $XDG_CONFIG_HOME/shikane/config.toml.
+
+ See
+ for more information.
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ assertions = [
+ (lib.hm.assertions.assertPlatform "services.shikane" pkgs lib.platforms.linux)
+ ];
+
+ xdg.configFile."shikane/config.toml" = lib.mkIf (cfg.settings != { }) {
+ source = tomlFormat.generate "shikane-config" cfg.settings;
+ };
+
+ systemd.user.services.shikane = {
+ Unit = {
+ Description = "Dynamic output configuration tool";
+ Documentation = "man:shikane(1)";
+ After = [ config.wayland.systemd.target ];
+ PartOf = [ config.wayland.systemd.target ];
+ };
+
+ Service = {
+ ExecStart = lib.getExe cfg.package;
+ };
+
+ Install = {
+ WantedBy = [ config.wayland.systemd.target ];
+ };
+ };
+ };
+}
diff --git a/tests/default.nix b/tests/default.nix
index 88b7ea2ac..48d3a2671 100644
--- a/tests/default.nix
+++ b/tests/default.nix
@@ -617,6 +617,7 @@ import nmtSrc {
./modules/services/remmina
./modules/services/restic
./modules/services/screen-locker
+ ./modules/services/shikane
./modules/services/signaturepdf
./modules/services/snixembed
./modules/services/swayidle
diff --git a/tests/modules/services/shikane/basic-configuration.nix b/tests/modules/services/shikane/basic-configuration.nix
new file mode 100644
index 000000000..4c01e44be
--- /dev/null
+++ b/tests/modules/services/shikane/basic-configuration.nix
@@ -0,0 +1,47 @@
+{ config, ... }:
+{
+ config = {
+ services.shikane = {
+ enable = true;
+ package = config.lib.test.mkStubPackage { };
+ settings = {
+ profile = [
+ {
+ name = "external-monitor-default";
+ output = [
+ {
+ match = "eDP-1";
+ enable = true;
+ }
+ {
+ match = "HDMI-A-1";
+ enable = true;
+ position = {
+ x = 1920;
+ y = 0;
+ };
+ }
+ ];
+ }
+ {
+ name = "builtin";
+ output = [
+ {
+ match = "eDP-1";
+ enable = true;
+ }
+ ];
+ }
+ ];
+ };
+ };
+
+ nmt.script = ''
+ serviceFile=home-files/.config/systemd/user/shikane.service
+ assertFileExists $serviceFile
+
+ assertFileExists home-files/.config/shikane/config.toml
+ assertFileContent home-files/.config/shikane/config.toml ${./expected.toml}
+ '';
+ };
+}
diff --git a/tests/modules/services/shikane/default.nix b/tests/modules/services/shikane/default.nix
new file mode 100644
index 000000000..9047cd9b5
--- /dev/null
+++ b/tests/modules/services/shikane/default.nix
@@ -0,0 +1 @@
+{ shikane-basic-configuration = ./basic-configuration.nix; }
diff --git a/tests/modules/services/shikane/expected.toml b/tests/modules/services/shikane/expected.toml
new file mode 100644
index 000000000..042243101
--- /dev/null
+++ b/tests/modules/services/shikane/expected.toml
@@ -0,0 +1,21 @@
+[[profile]]
+name = "external-monitor-default"
+
+[[profile.output]]
+enable = true
+match = "eDP-1"
+
+[[profile.output]]
+enable = true
+match = "HDMI-A-1"
+
+[profile.output.position]
+x = 1920
+y = 0
+
+[[profile]]
+name = "builtin"
+
+[[profile.output]]
+enable = true
+match = "eDP-1"