1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.xserver.desktopManager.phosh; 7 8 # Based on https://source.puri.sm/Librem5/librem5-base/-/blob/4596c1056dd75ac7f043aede07887990fd46f572/default/sm.puri.OSK0.desktop 9 oskItem = pkgs.makeDesktopItem { 10 name = "sm.puri.OSK0"; 11 desktopName = "On-screen keyboard"; 12 exec = "${pkgs.squeekboard}/bin/squeekboard"; 13 categories = [ "GNOME" "Core" ]; 14 onlyShowIn = [ "GNOME" ]; 15 noDisplay = true; 16 extraConfig = { 17 X-GNOME-Autostart-Phase = "Panel"; 18 X-GNOME-Provides = "inputmethod"; 19 X-GNOME-Autostart-Notify = "true"; 20 X-GNOME-AutoRestart = "true"; 21 }; 22 }; 23 24 phocConfigType = types.submodule { 25 options = { 26 xwayland = mkOption { 27 description = lib.mdDoc '' 28 Whether to enable XWayland support. 29 30 To start XWayland immediately, use `immediate`. 31 ''; 32 type = types.enum [ "true" "false" "immediate" ]; 33 default = "false"; 34 }; 35 cursorTheme = mkOption { 36 description = lib.mdDoc '' 37 Cursor theme to use in Phosh. 38 ''; 39 type = types.str; 40 default = "default"; 41 }; 42 outputs = mkOption { 43 description = lib.mdDoc '' 44 Output configurations. 45 ''; 46 type = types.attrsOf phocOutputType; 47 default = { 48 DSI-1 = { 49 scale = 2; 50 }; 51 }; 52 }; 53 }; 54 }; 55 56 phocOutputType = types.submodule { 57 options = { 58 modeline = mkOption { 59 description = lib.mdDoc '' 60 One or more modelines. 61 ''; 62 type = types.either types.str (types.listOf types.str); 63 default = []; 64 example = [ 65 "87.25 720 776 848 976 1440 1443 1453 1493 -hsync +vsync" 66 "65.13 768 816 896 1024 1024 1025 1028 1060 -HSync +VSync" 67 ]; 68 }; 69 mode = mkOption { 70 description = lib.mdDoc '' 71 Default video mode. 72 ''; 73 type = types.nullOr types.str; 74 default = null; 75 example = "768x1024"; 76 }; 77 scale = mkOption { 78 description = lib.mdDoc '' 79 Display scaling factor. 80 ''; 81 type = types.nullOr ( 82 types.addCheck 83 (types.either types.int types.float) 84 (x : x > 0) 85 ) // { 86 description = "null or positive integer or float"; 87 }; 88 default = null; 89 example = 2; 90 }; 91 rotate = mkOption { 92 description = lib.mdDoc '' 93 Screen transformation. 94 ''; 95 type = types.enum [ 96 "90" "180" "270" "flipped" "flipped-90" "flipped-180" "flipped-270" null 97 ]; 98 default = null; 99 }; 100 }; 101 }; 102 103 optionalKV = k: v: if v == null then "" else "${k} = ${builtins.toString v}"; 104 105 renderPhocOutput = name: output: let 106 modelines = if builtins.isList output.modeline 107 then output.modeline 108 else [ output.modeline ]; 109 renderModeline = l: "modeline = ${l}"; 110 in '' 111 [output:${name}] 112 ${concatStringsSep "\n" (map renderModeline modelines)} 113 ${optionalKV "mode" output.mode} 114 ${optionalKV "scale" output.scale} 115 ${optionalKV "rotate" output.rotate} 116 ''; 117 118 renderPhocConfig = phoc: let 119 outputs = mapAttrsToList renderPhocOutput phoc.outputs; 120 in '' 121 [core] 122 xwayland = ${phoc.xwayland} 123 ${concatStringsSep "\n" outputs} 124 [cursor] 125 theme = ${phoc.cursorTheme} 126 ''; 127in 128 129{ 130 options = { 131 services.xserver.desktopManager.phosh = { 132 enable = mkOption { 133 type = types.bool; 134 default = false; 135 description = lib.mdDoc "Enable the Phone Shell."; 136 }; 137 138 package = mkOption { 139 type = types.package; 140 default = pkgs.phosh; 141 defaultText = literalExpression "pkgs.phosh"; 142 example = literalExpression "pkgs.phosh"; 143 description = lib.mdDoc '' 144 Package that should be used for Phosh. 145 ''; 146 }; 147 148 user = mkOption { 149 description = lib.mdDoc "The user to run the Phosh service."; 150 type = types.str; 151 example = "alice"; 152 }; 153 154 group = mkOption { 155 description = lib.mdDoc "The group to run the Phosh service."; 156 type = types.str; 157 example = "users"; 158 }; 159 160 phocConfig = mkOption { 161 description = lib.mdDoc '' 162 Configurations for the Phoc compositor. 163 ''; 164 type = types.oneOf [ types.lines types.path phocConfigType ]; 165 default = {}; 166 }; 167 }; 168 }; 169 170 config = mkIf cfg.enable { 171 systemd.defaultUnit = "graphical.target"; 172 # Inspired by https://gitlab.gnome.org/World/Phosh/phosh/-/blob/main/data/phosh.service 173 systemd.services.phosh = { 174 wantedBy = [ "graphical.target" ]; 175 serviceConfig = { 176 ExecStart = "${cfg.package}/bin/phosh"; 177 User = cfg.user; 178 Group = cfg.group; 179 PAMName = "login"; 180 WorkingDirectory = "~"; 181 Restart = "always"; 182 183 TTYPath = "/dev/tty7"; 184 TTYReset = "yes"; 185 TTYVHangup = "yes"; 186 TTYVTDisallocate = "yes"; 187 188 # Fail to start if not controlling the tty. 189 StandardInput = "tty-fail"; 190 StandardOutput = "journal"; 191 StandardError = "journal"; 192 193 # Log this user with utmp, letting it show up with commands 'w' and 'who'. 194 UtmpIdentifier = "tty7"; 195 UtmpMode = "user"; 196 }; 197 }; 198 199 environment.systemPackages = [ 200 pkgs.phoc 201 cfg.package 202 pkgs.squeekboard 203 oskItem 204 ]; 205 206 systemd.packages = [ cfg.package ]; 207 208 programs.feedbackd.enable = true; 209 210 security.pam.services.phosh = {}; 211 212 hardware.opengl.enable = mkDefault true; 213 214 services.gnome.core-shell.enable = true; 215 services.gnome.core-os-services.enable = true; 216 services.xserver.displayManager.sessionPackages = [ cfg.package ]; 217 218 environment.etc."phosh/phoc.ini".source = 219 if builtins.isPath cfg.phocConfig then cfg.phocConfig 220 else if builtins.isString cfg.phocConfig then pkgs.writeText "phoc.ini" cfg.phocConfig 221 else pkgs.writeText "phoc.ini" (renderPhocConfig cfg.phocConfig); 222 }; 223}