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 = '' 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 = '' 37 Cursor theme to use in Phosh. 38 ''; 39 type = types.str; 40 default = "default"; 41 }; 42 outputs = mkOption { 43 description = '' 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 = '' 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 = '' 71 Default video mode. 72 ''; 73 type = types.nullOr types.str; 74 default = null; 75 example = "768x1024"; 76 }; 77 scale = mkOption { 78 description = '' 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 = '' 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: optionalString (v != null) "${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 = "Enable the Phone Shell."; 136 }; 137 138 package = mkPackageOption pkgs "phosh" { }; 139 140 user = mkOption { 141 description = "The user to run the Phosh service."; 142 type = types.str; 143 example = "alice"; 144 }; 145 146 group = mkOption { 147 description = "The group to run the Phosh service."; 148 type = types.str; 149 example = "users"; 150 }; 151 152 phocConfig = mkOption { 153 description = '' 154 Configurations for the Phoc compositor. 155 ''; 156 type = types.oneOf [ types.lines types.path phocConfigType ]; 157 default = {}; 158 }; 159 }; 160 }; 161 162 config = mkIf cfg.enable { 163 systemd.defaultUnit = "graphical.target"; 164 # Inspired by https://gitlab.gnome.org/World/Phosh/phosh/-/blob/main/data/phosh.service 165 systemd.services.phosh = { 166 wantedBy = [ "graphical.target" ]; 167 serviceConfig = { 168 ExecStart = "${cfg.package}/bin/phosh-session"; 169 User = cfg.user; 170 Group = cfg.group; 171 PAMName = "login"; 172 WorkingDirectory = "~"; 173 Restart = "always"; 174 175 TTYPath = "/dev/tty7"; 176 TTYReset = "yes"; 177 TTYVHangup = "yes"; 178 TTYVTDisallocate = "yes"; 179 180 # Fail to start if not controlling the tty. 181 StandardInput = "tty-fail"; 182 StandardOutput = "journal"; 183 StandardError = "journal"; 184 185 # Log this user with utmp, letting it show up with commands 'w' and 'who'. 186 UtmpIdentifier = "tty7"; 187 UtmpMode = "user"; 188 }; 189 environment = { 190 # We are running without a display manager, so need to provide 191 # a value for XDG_CURRENT_DESKTOP. 192 # 193 # Among other things, this variable influences: 194 # - visibility of desktop entries with "OnlyShowIn=Phosh;" 195 # https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.5.html#key-onlyshowin 196 # - the chosen xdg-desktop-portal configuration. 197 # https://flatpak.github.io/xdg-desktop-portal/docs/portals.conf.html 198 XDG_CURRENT_DESKTOP = "Phosh:GNOME"; 199 # pam_systemd uses these to identify the session in logind. 200 # https://www.freedesktop.org/software/systemd/man/latest/pam_systemd.html#desktop= 201 XDG_SESSION_DESKTOP = "phosh"; 202 XDG_SESSION_TYPE = "wayland"; 203 }; 204 }; 205 206 environment.systemPackages = [ 207 pkgs.phoc 208 cfg.package 209 pkgs.squeekboard 210 oskItem 211 ]; 212 213 systemd.packages = [ cfg.package ]; 214 215 programs.feedbackd.enable = true; 216 217 security.pam.services.phosh = {}; 218 219 hardware.opengl.enable = mkDefault true; 220 221 services.gnome.core-shell.enable = true; 222 services.gnome.core-os-services.enable = true; 223 services.displayManager.sessionPackages = [ cfg.package ]; 224 225 environment.etc."phosh/phoc.ini".source = 226 if builtins.isPath cfg.phocConfig then cfg.phocConfig 227 else if builtins.isString cfg.phocConfig then pkgs.writeText "phoc.ini" cfg.phocConfig 228 else pkgs.writeText "phoc.ini" (renderPhocConfig cfg.phocConfig); 229 }; 230}