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}