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}