at master 5.5 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 json = pkgs.formats.json { }; 9 transitionType = lib.types.submodule { 10 freeformType = json.type; 11 options.type = lib.mkOption { 12 type = lib.types.enum [ 13 "delay" 14 "event" 15 "exec" 16 ]; 17 description = '' 18 Type of transition. Determines how bonsaid interprets the other options in this transition. 19 ''; 20 }; 21 options.command = lib.mkOption { 22 type = lib.types.nullOr (lib.types.listOf lib.types.str); 23 default = null; 24 description = '' 25 Command to run when this transition is taken. 26 This is executed inline by `bonsaid` and blocks handling of any other events until completion. 27 To perform the command asynchronously, specify it like `[ "setsid" "-f" "my-command" ]`. 28 29 Only effects transitions with `type = "exec"`. 30 ''; 31 }; 32 options.delay_duration = lib.mkOption { 33 type = lib.types.nullOr lib.types.int; 34 default = null; 35 description = '' 36 Nanoseconds to wait after the previous state change before performing this transition. 37 This can be placed at the same level as a `type = "event"` transition to achieve a 38 timeout mechanism. 39 40 Only effects transitions with `type = "delay"`. 41 ''; 42 }; 43 options.event_name = lib.mkOption { 44 type = lib.types.nullOr lib.types.str; 45 default = null; 46 description = '' 47 Name of the event which should trigger this transition when received by `bonsaid`. 48 Events are sent to `bonsaid` by running `bonsaictl -e <event_name>`. 49 50 Only effects transitions with `type = "event"`. 51 ''; 52 }; 53 options.transitions = lib.mkOption { 54 type = lib.types.listOf transitionType; 55 default = [ ]; 56 description = '' 57 List of transitions out of this state. 58 If left empty, then this state is considered a terminal state and entering it will 59 trigger an immediate transition back to the root state (after processing side effects). 60 ''; 61 visible = "shallow"; 62 }; 63 }; 64 cfg = config.services.bonsaid; 65in 66{ 67 meta.maintainers = [ lib.maintainers.colinsane ]; 68 69 options.services.bonsaid = { 70 enable = lib.mkEnableOption "bonsaid"; 71 package = lib.mkPackageOption pkgs "bonsai" { }; 72 extraFlags = lib.mkOption { 73 type = lib.types.listOf lib.types.str; 74 default = [ ]; 75 description = '' 76 Extra flags to pass to `bonsaid`, such as `[ "-v" ]` to enable verbose logging. 77 ''; 78 }; 79 settings = lib.mkOption { 80 type = lib.types.listOf transitionType; 81 description = '' 82 State transition definitions. See the upstream [README](https://git.sr.ht/~stacyharper/bonsai) 83 for extended documentation and a more complete example. 84 ''; 85 example = [ 86 { 87 type = "event"; 88 event_name = "power_button_pressed"; 89 transitions = [ 90 { 91 # Hold power button for 600ms to trigger a command 92 type = "delay"; 93 delay_duration = 600000000; 94 transitions = [ 95 { 96 type = "exec"; 97 command = [ 98 "swaymsg" 99 "--" 100 "output" 101 "*" 102 "power" 103 "off" 104 ]; 105 # `transitions = []` marks this as a terminal state, 106 # so bonsai will return to the root state immediately after executing the above command. 107 transitions = [ ]; 108 } 109 ]; 110 } 111 { 112 # If the power button is released before the 600ms elapses, return to the root state. 113 type = "event"; 114 event_name = "power_button_released"; 115 transitions = [ ]; 116 } 117 ]; 118 } 119 ]; 120 }; 121 configFile = lib.mkOption { 122 type = lib.types.path; 123 description = '' 124 Path to a .json file specifying the state transitions. 125 You don't need to set this unless you prefer to provide the json file 126 yourself instead of using the `settings` option. 127 ''; 128 }; 129 }; 130 131 config = lib.mkIf cfg.enable { 132 services.bonsaid.configFile = 133 let 134 filterNulls = 135 v: 136 if lib.isAttrs v then 137 lib.mapAttrs (_: filterNulls) (lib.filterAttrs (_: a: a != null) v) 138 else if lib.isList v then 139 lib.map filterNulls (lib.filter (a: a != null) v) 140 else 141 v; 142 in 143 lib.mkDefault (json.generate "bonsai_tree.json" (filterNulls cfg.settings)); 144 145 # bonsaid is controlled by bonsaictl, so place the latter in the environment by default. 146 # bonsaictl is typically invoked by scripts or a DE so this isn't strictly necessary, 147 # but it's helpful while administering the service generally. 148 environment.systemPackages = [ cfg.package ]; 149 150 systemd.user.services.bonsaid = { 151 description = "Bonsai Finite State Machine daemon"; 152 documentation = [ "https://git.sr.ht/~stacyharper/bonsai" ]; 153 wantedBy = [ "multi-user.target" ]; 154 serviceConfig = { 155 ExecStart = lib.escapeShellArgs ( 156 [ 157 (lib.getExe' cfg.package "bonsaid") 158 "-t" 159 cfg.configFile 160 ] 161 ++ cfg.extraFlags 162 ); 163 Restart = "on-failure"; 164 RestartSec = "5s"; 165 }; 166 }; 167 }; 168}