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