summaryrefslogtreecommitdiff
path: root/accounts/gkleen@skadhi/shell/quickshell/Services
diff options
context:
space:
mode:
Diffstat (limited to 'accounts/gkleen@skadhi/shell/quickshell/Services')
-rw-r--r--accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml107
-rw-r--r--accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml18
-rw-r--r--accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml23
-rw-r--r--accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml8
-rw-r--r--accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml217
-rw-r--r--accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml162
-rw-r--r--accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml63
-rw-r--r--accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml63
-rw-r--r--accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml8
-rw-r--r--accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml84
10 files changed, 753 insertions, 0 deletions
diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml
new file mode 100644
index 00000000..f3ae6804
--- /dev/null
+++ b/accounts/gkleen@skadhi/shell/quickshell/Services/Brightness.qml
@@ -0,0 +1,107 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Io
6import Custom as Custom
7import qs.Services
8
9Singleton {
10 id: root
11
12 property string subsystem: "backlight"
13 property string device: "intel_backlight"
14
15 property real currBrightness
16 property real exponent: 4
17
18 function calcCurrBrightness() {
19 if (!currFile.loaded || !maxFile.loaded)
20 return undefined;
21 const curr = Number(currFile.text());
22 const max = Number(maxFile.text());
23 const val = Math.pow(curr / max, 1 / root.exponent);
24 return val;
25 }
26
27 Connections {
28 target: currFile
29 function onLoaded() {
30 const b = root.calcCurrBrightness();
31 if (typeof b !== 'undefined')
32 root.currBrightness = b;
33 }
34 }
35 Connections {
36 target: maxFile
37 function onLoaded() {
38 const b = root.calcCurrBrightness();
39 if (typeof b !== 'undefined')
40 root.currBrightness = b;
41 }
42 }
43
44 onCurrBrightnessChanged: {
45 root.currBrightness = Math.max(0, Math.min(1, root.currBrightness));
46
47 const prev = root.calcCurrBrightness();
48 if (typeof prev === 'undefined' || Math.abs(root.currBrightness - prev) < 0.01)
49 return;
50
51 const max = Number(maxFile.text());
52 const actual = Number(currFile.text());
53 let curr = Math.max(0, Math.min(max, Math.pow(root.currBrightness, root.exponent) * max));
54 if (Math.round(curr) == actual && curr < actual)
55 curr = Math.max(0, actual - 1);
56 else if (Math.round(curr) == actual && curr > actual)
57 curr = Math.min(max, actual + 1);
58 // root.currBrightness = Math.pow(curr / max, 1 / root.exponent);
59 Custom.Systemd.setBrightness(root.subsystem, root.device, Math.round(curr));
60 }
61
62 FileView {
63 id: currFile
64 path: `/sys/class/${root.subsystem}/${root.device}/brightness`
65 blockAllReads: true
66 watchChanges: true
67 onFileChanged: reload()
68 }
69 FileView {
70 id: maxFile
71 path: `/sys/class/${root.subsystem}/${root.device}/max_brightness`
72 blockAllReads: true
73 watchChanges: true
74 onFileChanged: reload()
75 }
76
77
78 Timer {
79 id: startupDelay
80 interval: 500
81 running: true
82 repeat: false
83 }
84 property bool onlyInternal: true
85 Connections {
86 target: NiriService
87 function onOutputsChanged() {
88 root.onlyInternal = Object.keys(NiriService.outputs).every(oname => oname == "eDP-1");
89 }
90 }
91 onOnlyInternalChanged: {
92 if (startupDelay.running)
93 return;
94
95 if (!root.onlyInternal)
96 root.currBrightness = 1
97 }
98
99
100 IpcHandler {
101 target: "Brightness"
102
103 function set(brightness: real): void {
104 root.currBrightness = brightness;
105 }
106 }
107}
diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml
new file mode 100644
index 00000000..3de69535
--- /dev/null
+++ b/accounts/gkleen@skadhi/shell/quickshell/Services/GpgAgent.qml
@@ -0,0 +1,18 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Io
5
6Singleton {
7 id: root
8
9 Socket {
10 id: agentSocket
11 connected: true
12 path: `${Quickshell.env("XDG_RUNTIME_DIR")}/gnupg/S.gpg-agent`
13 }
14
15 function reloadAgent() {
16 agentSocket.write("RELOADAGENT\n")
17 }
18}
diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml
new file mode 100644
index 00000000..60202a29
--- /dev/null
+++ b/accounts/gkleen@skadhi/shell/quickshell/Services/InhibitorState.qml
@@ -0,0 +1,23 @@
1pragma Singleton
2
3import Quickshell
4import Custom as Custom
5
6Singleton {
7 id: inhibitorState
8
9 property bool waylandIdleInhibited: false
10 property alias lidSwitchInhibited: lidSwitchInhibitor.enabled
11 property bool lockscreenInhibited: false
12
13 Custom.SystemdInhibitor {
14 id: lidSwitchInhibitor
15
16 enabled: false
17
18 what: Custom.SystemdInhibitorParams.HandleLidSwitch
19 who: "quickshell"
20 why: "User request"
21 mode: Custom.SystemdInhibitorParams.BlockWeak
22 }
23}
diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml
new file mode 100644
index 00000000..e3ab9755
--- /dev/null
+++ b/accounts/gkleen@skadhi/shell/quickshell/Services/MprisProxy.qml
@@ -0,0 +1,8 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Services.Mpris
5
6Scope {
7 property list<var> players: Mpris.players.values
8}
diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml
new file mode 100644
index 00000000..e9e86f95
--- /dev/null
+++ b/accounts/gkleen@skadhi/shell/quickshell/Services/NiriService.qml
@@ -0,0 +1,217 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Io
5import QtQuick
6
7Singleton {
8 id: root
9
10 property var workspaces: []
11 property var outputs: {}
12 property var keyboardLayouts: {}
13 property var windows: []
14 property var casts: []
15 readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
16
17 function refreshOutputs() {
18 commandSocket.sendCommand("Outputs", data => {
19 outputs = data.Ok.Outputs;
20 });
21 }
22
23 function sendCommand(command, callback) {
24 commandSocket.sendCommand(command, callback);
25 }
26
27 Socket {
28 id: eventStreamSocket
29 path: root.socketPath
30 connected: true
31
32 property bool acked: false
33
34 onConnectionStateChanged: {
35 if (connected) {
36 acked = false;
37 write('"EventStream"\n');
38 }
39 }
40
41 parser: SplitParser {
42 onRead: line => {
43 try {
44 const event = JSON.parse(line)
45
46 // console.log(JSON.stringify(event))
47
48 if (event.WorkspacesChanged) {
49 root.workspaces = event.WorkspacesChanged.workspaces
50 root.refreshOutputs();
51 } else if (event.WorkspaceActivated)
52 eventWorkspaceActivated(event.WorkspaceActivated);
53 else if (event.WorkspaceUrgencyChanged)
54 eventWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged);
55 else if (event.WorkspaceActiveWindowChanged)
56 eventWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged);
57 else if (event.KeyboardLayoutsChanged)
58 root.keyboardLayouts = event.KeyboardLayoutsChanged.keyboard_layouts;
59 else if (event.KeyboardLayoutSwitched)
60 root.keyboardLayouts = Object.assign({}, root.keyboardLayouts, {"current_idx": event.KeyboardLayoutSwitched.idx });
61 else if (event.WindowsChanged)
62 root.windows = event.WindowsChanged.windows
63 else if (event.WindowOpenedOrChanged)
64 eventWindowOpenedOrChanged(event.WindowOpenedOrChanged);
65 else if (event.WindowClosed)
66 eventWindowClosed(event.WindowClosed);
67 else if (event.WindowFocusChanged)
68 eventWindowFocusChanged(event.WindowFocusChanged);
69 else if (event.WindowUrgencyChanged)
70 eventWindowUrgencyChanged(event.WindowUrgencyChanged);
71 else if (event.WindowLayoutsChanged)
72 eventWindowLayoutsChanged(event.WindowLayoutsChanged);
73 else if (event.WindowFocusTimestampChanged)
74 eventWindowFocusTimestampChanged(event.WindowFocusTimestampChanged);
75 else if (event.CastsChanged)
76 root.casts = event.CastsChanged.casts
77 else if (event.CastStartedOrChanged)
78 eventCastStartedOrChanged(event.CastStartedOrChanged);
79 else if (event.CastStopped)
80 eventCastStopped(event.CastStopped);
81 else if (event.Ok && !eventStreamSocket.acked) { eventStreamSocket.acked = true; }
82 else if (event.OverviewOpenedOrClosed) {}
83 else if (event.ScreenshotCaptured) {}
84 else if (event.ConfigLoaded) {}
85 else
86 console.log(JSON.stringify(event));
87 } catch (e) {
88 console.warn("NiriService: Failed to parse event:", line, e)
89 }
90 }
91 }
92 }
93
94 Socket {
95 id: commandSocket
96 path: root.socketPath
97 connected: true
98
99 property var awaitingAnswer: null
100 property var cmdQueue: []
101
102 parser: SplitParser {
103 onRead: line => {
104 if (commandSocket.awaitingAnswer === null)
105 return;
106
107 try {
108 const response = JSON.parse(line);
109 commandSocket.awaitingAnswer.callback(response);
110 commandSocket.awaitingAnswer = null;
111 } catch (e) {
112 console.warn("NiriService: Failed to parse response:", line, e)
113 }
114 commandSocket._handleQueue();
115 }
116 }
117
118 onCmdQueueChanged: {
119 _handleQueue();
120 }
121 onAwaitingAnswerChanged: {
122 _handleQueue();
123 }
124
125 function _handleQueue() {
126 if (cmdQueue.length <= 0 || awaitingAnswer !== null)
127 return;
128
129 let localQueue = Array.from(cmdQueue);
130 awaitingAnswer = localQueue.shift();
131 cmdQueue = localQueue;
132 write(JSON.stringify(awaitingAnswer.command) + '\n');
133 }
134
135 function sendCommand(command, callback) {
136 cmdQueue = Array.from(cmdQueue).concat([{ "command": command, "callback": callback }])
137 }
138 }
139
140 function eventWorkspaceActivated(data) {
141 let relevant_output = null;
142 Array.from(root.workspaces).forEach(ws => {
143 if (data.id === ws.id)
144 relevant_output = ws.output;
145 });
146 root.workspaces = Array.from(root.workspaces).map(ws => {
147 if (data.focused)
148 ws.is_focused = false;
149 if (ws.output === relevant_output)
150 ws.is_active = false;
151 if (data.id === ws.id) {
152 ws.is_active = true;
153 ws.is_focused = data.focused;
154 }
155 return ws;
156 });
157 }
158 function eventWorkspaceUrgencyChanged(data) {
159 root.workspaces = Array.from(root.workspaces).map(ws => {
160 if (data.id == ws.id)
161 ws.is_urgent = data.urgent;
162 return ws;
163 });
164 }
165 function eventWorkspaceActiveWindowChanged(data) {
166 root.workspaces = Array.from(root.workspaces).map(ws => {
167 if (data.workspace_id === ws.id)
168 ws.active_window_id = data.active_window_id;
169 return ws;
170 });
171 }
172 function eventWindowOpenedOrChanged(data) {
173 root.windows = Array.from(root.windows).map(win => {
174 if (data.window.is_focused)
175 win.is_focused = false;
176 return win;
177 }).filter(win => win.id !== data.window.id).concat([data.window]);
178 }
179 function eventWindowClosed(data) {
180 root.windows = Array.from(root.windows).filter(win => win.id !== data.id);
181 }
182 function eventWindowFocusChanged(data) {
183 root.windows = Array.from(root.windows).map(win => {
184 win.is_focused = win.id === data.id;
185 return win;
186 });
187 }
188 function eventWindowUrgencyChanged(data) {
189 root.windows = Array.from(root.windows).map(win => {
190 if (win.id === data.id)
191 win.is_urgent = data.urgent;
192 return win;
193 });
194 }
195 function eventWindowLayoutsChanged(data) {
196 root.windows = Array.from(root.windows).map(win => {
197 Array.from(data.changes).forEach(change => {
198 if (win.id === change[0])
199 win.layout = change[1];
200 });
201 return win;
202 });
203 }
204 function eventWindowFocusTimestampChanged(data) {
205 root.windows = Array.from(root.windows).map(win => {
206 if (win.id === data.id)
207 win.focus_timestamp = data.focus_timestamp;
208 return win;
209 });
210 }
211 function eventCastStartedOrChanged(data) {
212 root.casts = [...Array.from(root.casts).filter(cast => cast.stream_id !== data.cast.stream_id), data.cast];
213 }
214 function eventCastStopped(data) {
215 root.casts = Array.from(root.casts).filter(cast => cast.stream_id !== data.stream_id);
216 }
217}
diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml
new file mode 100644
index 00000000..5f8ff419
--- /dev/null
+++ b/accounts/gkleen@skadhi/shell/quickshell/Services/NotificationManager.qml
@@ -0,0 +1,162 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Services.Notifications
6
7Singleton {
8 id: root
9
10 readonly property bool active: !root.lockscreenActive && !root.displayInhibited
11 property bool lockscreenActive: false
12 property bool displayInhibited: false
13 property alias trackedNotifications: server.trackedNotifications
14 readonly property var groups: {
15 function matchesGroupKey(notif, groupKey) {
16 var matches = true;
17 for (const prop in groupKey.test) {
18 if (notif[prop] !== groupKey.test[prop]) {
19 matches = false;
20 break;
21 }
22 }
23 return matches;
24 }
25
26 var groups = new Map();
27 var notifs = new Array();
28 for (const [ix, notif] of server.trackedNotifications.values.entries()) {
29 var didGroup = false;
30 for (const groupKey of root.groupKeys) {
31 if (!matchesGroupKey(notif, groupKey))
32 continue;
33
34 const key = JSON.stringify({
35 "key": groupKey,
36 "values": Object.assign({}, ...(Array.from(groupKey["group-by"]).map(prop => {
37 var res = {};
38 res[prop] = notif[prop];
39 return res;
40 })))
41 });
42 if (!groups.has(key))
43 groups.set(key, new Array());
44 groups.get(key).push({ "ix": ix, "notif": notif });
45 didGroup = true;
46 break;
47 }
48
49 if (!didGroup)
50 notifs.push([{ "ix": ix, "notif": notif }]);
51 }
52 notifs.push(...groups.values());
53 notifs.sort((as, bs) => Math.min(...(as.map(o => o.ix))) - Math.min(...(bs.map(o => o.ix))));
54 return notifs.map(ns => ns.map(n => n.notif));
55 }
56
57 property var groupKeys: [
58 { "test": { "appName": "Element" }, "group-by": [ "summary" ] }
59 ];
60
61 property int historyLimit: 100
62 property var history: []
63
64 Component {
65 id: expirationTimer
66
67 QtObject {
68 id: timer
69
70 required property QtObject parent
71 required property int expirationTime
72
73 property list<QtObject> data: [
74 Timer {
75 running: root.active && !timer.expired
76 interval: timer.expirationTime
77 onTriggered: {
78 timer.parent.expirationTimer.destroy();
79 timer.parent.expirationTimer = null;
80 timer.parent.expire();
81 }
82 }
83 ]
84 }
85 }
86
87 Component {
88 id: notificationLock
89
90 RetainableLock {}
91 }
92
93 readonly property SystemClock clock: SystemClock {
94 precision: SystemClock.Minutes
95 }
96
97 function formatTime(time) {
98 const now = root.clock.date;
99 const diff = now - time;
100 const minutes = Math.ceil(diff / 60000);
101 const hours = Math.floor(minutes / 60);
102
103 if (hours < 1) {
104 if (minutes < 1)
105 return "now";
106 if (minutes == 1)
107 return "1 minute";
108 return `${minutes} minutes`;
109 }
110
111 const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
112 const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate())
113 const days = Math.floor((nowDate - timeDate) / (1000 * 86400))
114
115 const timeStr = time.toLocaleTimeString(Qt.locale(), "HH:mm");
116
117 if (days === 0)
118 return timeStr;
119 if (days === 1)
120 return `yesterday ${timeStr}`;
121
122 const dateStr = time.toLocaleDateString(Qt.locale(), "YYYY-MM-DD");
123 return `${dateStr} ${timeStr}`;
124 }
125
126 NotificationServer {
127 id: server
128
129 bodySupported: true
130 actionsSupported: true
131 actionIconsSupported: true
132 imageSupported: true
133 bodyMarkupSupported: true
134 bodyImagesSupported: true
135
136 onNotification: notification => {
137 var timeout = notification.expireTimeout * 1000;
138 if (notification.appName == "poweralertd")
139 timeout = 2000;
140 if (timeout > 0) {
141 Object.defineProperty(notification, "expirationTimer", { configurable: true, enumerable: true, writable: true });
142 notification.expirationTimer = expirationTimer.createObject(notification, { parent: notification, expirationTime: timeout });
143 }
144 Object.defineProperty(notification, "receivedTime", { configurable: true, enumerable: true, writable: true });
145 notification.receivedTime = root.clock.date;
146 notification.closed.connect((reason) => server.onNotificationClosed(notification, reason));
147 notification.tracked = true;
148 }
149
150 function onNotificationClosed(notification, reason) {
151 while (root.history.length >= root.historyLimit) {
152 root.history[0].lock.locked = false;
153 root.history.shift();
154 }
155
156 root.history.push({
157 lock: notificationLock.createObject(root, { locked: true, object: notification }),
158 notification: notification
159 });
160 }
161 }
162}
diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml
new file mode 100644
index 00000000..9c813e49
--- /dev/null
+++ b/accounts/gkleen@skadhi/shell/quickshell/Services/Privacy.qml
@@ -0,0 +1,63 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Services.Pipewire
6
7Singleton {
8 id: root
9
10 PwObjectTracker {
11 objects: Pipewire.nodes.values
12 }
13
14 enum Item {
15 Microphone,
16 Screensharing
17 }
18
19 readonly property list<var> activeItems: {
20 var items = [];
21 if (microphoneActive)
22 items.push(Privacy.Item.Microphone);
23 if (screensharingActive)
24 items.push(Privacy.Item.Screensharing);
25 return items;
26 }
27
28 readonly property bool microphoneActive: {
29 if (!Pipewire.ready || !Pipewire.nodes?.values) {
30 return false
31 }
32
33 for (const node of Pipewire.nodes.values) {
34 if (!node || (node.type & PwNodeType.AudioInStream) != PwNodeType.AudioInStream)
35 continue;
36
37 if (node.properties?.["stream.monitor"] === "true")
38 continue;
39
40 if (node.audio?.muted)
41 continue;
42
43 return true;
44 }
45
46 return false;
47 }
48
49 readonly property bool screensharingActive: {
50 if (!Pipewire.ready || !Pipewire.nodes?.values) {
51 return false
52 }
53
54 for (const node of Pipewire.nodes.values) {
55 if (!node || (node.type & PwNodeType.VideoInStream) != PwNodeType.VideoInStream)
56 continue;
57
58 return true;
59 }
60
61 return false;
62 }
63}
diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml
new file mode 100644
index 00000000..eb415452
--- /dev/null
+++ b/accounts/gkleen@skadhi/shell/quickshell/Services/ScreenRecord.qml
@@ -0,0 +1,63 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Io
5
6Singleton {
7 id: root
8 property bool active: false
9 property bool slurpSuccess: false
10
11 onActiveChanged: {
12 if (!active) {
13 slurp.running = false;
14 screenRecorder.running = false;
15 }
16 if (active)
17 slurp.running = true;
18 }
19
20 Process {
21 id: screenRecorder
22 running: false
23 onRunningChanged: {
24 console.log("wf-recorder running: ", screenRecorder.running);
25
26 if (!screenRecorder.running && !slurp.running)
27 root.active = false;
28 }
29 stderr: SplitParser {
30 onRead: line => console.log("wf-recorder stderr: ", line)
31 }
32 stdout: SplitParser {
33 onRead: line => console.log("wf-recorder stdout: ", line)
34 }
35 }
36
37 Process {
38 id: slurp
39 running: false
40 command: [ @slurp@, "-o", "-d" ]
41 stdout: StdioCollector {}
42 stderr: SplitParser {
43 onRead: line => console.log("slurp stderr: ", line)
44 }
45 onExited: exitCode => {
46 if (exitCode !== 0) {
47 console.log("slurp failed: ", exitCode);
48 root.active = false;
49 return;
50 }
51 console.log("slurp succeeded: ", slurp.stdout.text);
52
53 const nowDate = new Date();
54
55 screenRecorder.command = [
56 @wf-recorder@,
57 "-g", slurp.stdout.text,
58 "-f", `${Quickshell.env("HOME")}/screenshots/${nowDate.toLocaleString(Qt.locale(), "yyyy-MM-ddThh:mm:ss")}.mkv`,
59 ];
60 screenRecorder.running = true;
61 }
62 }
63}
diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml
new file mode 100644
index 00000000..c71a9cca
--- /dev/null
+++ b/accounts/gkleen@skadhi/shell/quickshell/Services/WallpaperSelector.qml
@@ -0,0 +1,8 @@
1import Custom as Custom
2
3Custom.FileSelector {
4 id: root
5
6 directory: @wallpapers@
7 epoch: 79200000
8}
diff --git a/accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml b/accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml
new file mode 100644
index 00000000..d98378f1
--- /dev/null
+++ b/accounts/gkleen@skadhi/shell/quickshell/Services/Worktime.qml
@@ -0,0 +1,84 @@
1pragma Singleton
2
3import QtQuick
4import Quickshell
5import Quickshell.Io
6import QtQml
7
8Singleton {
9 id: root
10
11 property alias time: timeState
12 property alias today: todayState
13
14 CommandState {
15 id: timeState
16 command: "time"
17 }
18 CommandState {
19 id: todayState
20 command: "today"
21 }
22
23 IpcHandler {
24 target: "Worktime"
25
26 function refresh(): void {
27 time.running = true;
28 today.running = true;
29 }
30 }
31
32 component CommandState : Scope {
33 id: commandState
34
35 required property string command
36 property var state: null
37
38 property bool strikeout: !strikeoutTimer.running
39 property alias running: process.running
40 property alias updating: updateTimer.running
41
42 Process {
43 id: process
44 running: true
45 command: [ @worktime@, commandState.command, "--waybar" ]
46 stdout: StdioCollector {
47 id: processCollector
48 onStreamFinished: {
49 try {
50 commandState.state = JSON.parse(processCollector.text);
51 strikeoutTimer.restart();
52 } catch (e) {
53 console.warn("Worktime: Failed to parse output:", processCollector.text, e);
54 }
55 }
56 }
57 }
58
59 Timer {
60 id: updateTimer
61 running: commandState.state?.class == "running" || commandState.state?.class == "over"
62 interval: 60000
63 repeat: true
64 onTriggered: process.running = true
65 }
66
67 Timer {
68 id: strikeoutTimer
69 running: false
70 interval: 5 * updateTimer.interval
71 repeat: false
72 }
73 }
74
75 Timer {
76 running: Boolean(timeState.state) && Boolean(todayState.state) && timeState.strikeout && todayState.strikeout
77 interval: 1000
78 repeat: false
79 onTriggered: {
80 timeState.state = null;
81 todayState.state = null;
82 }
83 }
84}