diff options
author | Kristóf Marussy <kristof@marussy.com> | 2023-03-15 17:26:13 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-15 17:26:13 +0100 |
commit | f1152d3dbb4c6deefea168d66f15f77b7155a5fe (patch) | |
tree | 22c27234b6e3a2dfe47ade037ece47b2533f7039 /src | |
parent | 6.2.6-nightly.5 [skip ci] (diff) | |
download | ferdium-app-f1152d3dbb4c6deefea168d66f15f77b7155a5fe.tar.gz ferdium-app-f1152d3dbb4c6deefea168d66f15f77b7155a5fe.tar.zst ferdium-app-f1152d3dbb4c6deefea168d66f15f77b7155a5fe.zip |
Basic D-Bus API (#866)
* feat: basic D-Bus API
Expose muted state and the number of unread message over D-Bus when
running on Linux. This is useful for, e.g., displaying notifications on
a window manager status bar.
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
* docs: create docs directory
Move the documentation to a separate directory so that new documentation
can be added into one place.
We keep the following files still in the repository root by convention:
* CHANGELOG.md
* CODE_OF_CONDUCT.md
* CONTRIBUTING.md
* LICENSE.md
* README.md
* SECURITY.md
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
* docs: D-Bus usage example
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
* fix: remove unneeded D-Bus signals
Only notify clients that the message counts or the mute status has
changed if there actually was a change.
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
* docs: rewrite sample bar client
* docs: better unread --services help
* docs: update dbus docs
* docs: use ferdium-dbus in dbus bar example
* docs: make command argument required in bar example
---------
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Co-authored-by: Victor Bonnelle <victor.bonnelle@protonmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/DBus.ts | 69 | ||||
-rw-r--r-- | src/lib/dbus/Ferdium.ts | 88 | ||||
-rw-r--r-- | src/stores/ServicesStore.ts | 52 |
3 files changed, 186 insertions, 23 deletions
diff --git a/src/lib/DBus.ts b/src/lib/DBus.ts index bbff405c4..530e30c85 100644 --- a/src/lib/DBus.ts +++ b/src/lib/DBus.ts | |||
@@ -1,28 +1,92 @@ | |||
1 | import { ipcMain } from 'electron'; | ||
2 | import { comparer } from 'mobx'; | ||
3 | |||
1 | import { MessageBus, sessionBus } from 'dbus-next'; | 4 | import { MessageBus, sessionBus } from 'dbus-next'; |
2 | import { isLinux } from '../environment'; | 5 | import { isLinux } from '../environment'; |
3 | import TrayIcon from './Tray'; | 6 | import TrayIcon from './Tray'; |
7 | import Ferdium, { type UnreadServices } from './dbus/Ferdium'; | ||
4 | 8 | ||
5 | export default class DBus { | 9 | export default class DBus { |
6 | bus: MessageBus | null = null; | 10 | private bus: MessageBus | null = null; |
7 | 11 | ||
8 | trayIcon: TrayIcon; | 12 | trayIcon: TrayIcon; |
9 | 13 | ||
14 | private ferdium: Ferdium | null = null; | ||
15 | |||
16 | muted = false; | ||
17 | |||
18 | unreadDirectMessageCount = 0; | ||
19 | |||
20 | unreadIndirectMessageCount = 0; | ||
21 | |||
22 | unreadServices: UnreadServices = []; | ||
23 | |||
10 | constructor(trayIcon: TrayIcon) { | 24 | constructor(trayIcon: TrayIcon) { |
11 | this.trayIcon = trayIcon; | 25 | this.trayIcon = trayIcon; |
26 | ipcMain.on('initialAppSettings', (_, appSettings) => { | ||
27 | this.updateSettings(appSettings); | ||
28 | }); | ||
29 | ipcMain.on('updateAppSettings', (_, appSettings) => { | ||
30 | this.updateSettings(appSettings); | ||
31 | }); | ||
32 | ipcMain.on( | ||
33 | 'updateDBusUnread', | ||
34 | ( | ||
35 | _, | ||
36 | unreadDirectMessageCount, | ||
37 | unreadIndirectMessageCount, | ||
38 | unreadServices, | ||
39 | ) => { | ||
40 | this.setUnread( | ||
41 | unreadDirectMessageCount, | ||
42 | unreadIndirectMessageCount, | ||
43 | unreadServices, | ||
44 | ); | ||
45 | }, | ||
46 | ); | ||
47 | } | ||
48 | |||
49 | private updateSettings(appSettings): void { | ||
50 | const muted = !!appSettings.data.isAppMuted; | ||
51 | if (this.muted !== muted) { | ||
52 | this.muted = muted; | ||
53 | this.ferdium?.emitMutedChanged(); | ||
54 | } | ||
12 | } | 55 | } |
13 | 56 | ||
14 | start() { | 57 | private setUnread( |
58 | unreadDirectMessageCount: number, | ||
59 | unreadIndirectMessageCount: number, | ||
60 | unreadServices: UnreadServices, | ||
61 | ): void { | ||
62 | if ( | ||
63 | this.unreadDirectMessageCount !== unreadDirectMessageCount || | ||
64 | this.unreadIndirectMessageCount !== unreadIndirectMessageCount || | ||
65 | !comparer.structural(this.unreadServices, unreadServices) | ||
66 | ) { | ||
67 | this.unreadDirectMessageCount = unreadDirectMessageCount; | ||
68 | this.unreadIndirectMessageCount = unreadIndirectMessageCount; | ||
69 | this.unreadServices = unreadServices; | ||
70 | this.ferdium?.emitUnreadChanged(); | ||
71 | } | ||
72 | } | ||
73 | |||
74 | async start() { | ||
15 | if (!isLinux || this.bus) { | 75 | if (!isLinux || this.bus) { |
16 | return; | 76 | return; |
17 | } | 77 | } |
18 | 78 | ||
19 | try { | 79 | try { |
20 | this.bus = sessionBus(); | 80 | this.bus = sessionBus(); |
81 | await this.bus.requestName('org.ferdium.Ferdium', 0); | ||
21 | } catch { | 82 | } catch { |
22 | // Error connecting to the bus. | 83 | // Error connecting to the bus. |
23 | return; | 84 | return; |
24 | } | 85 | } |
25 | 86 | ||
87 | this.ferdium = new Ferdium(this); | ||
88 | this.bus.export('/org/ferdium', this.ferdium); | ||
89 | |||
26 | // HACK Hook onto the MessageBus to track StatusNotifierWatchers | 90 | // HACK Hook onto the MessageBus to track StatusNotifierWatchers |
27 | // @ts-expect-error Property '_addMatch' does not exist on type 'MessageBus'. | 91 | // @ts-expect-error Property '_addMatch' does not exist on type 'MessageBus'. |
28 | this.bus._addMatch( | 92 | this.bus._addMatch( |
@@ -56,5 +120,6 @@ export default class DBus { | |||
56 | 120 | ||
57 | this.bus.disconnect(); | 121 | this.bus.disconnect(); |
58 | this.bus = null; | 122 | this.bus = null; |
123 | this.ferdium = null; | ||
59 | } | 124 | } |
60 | } | 125 | } |
diff --git a/src/lib/dbus/Ferdium.ts b/src/lib/dbus/Ferdium.ts new file mode 100644 index 000000000..b2a9105f4 --- /dev/null +++ b/src/lib/dbus/Ferdium.ts | |||
@@ -0,0 +1,88 @@ | |||
1 | import * as dbus from 'dbus-next'; | ||
2 | |||
3 | import type DBus from '../DBus'; | ||
4 | |||
5 | export type UnreadServices = [string, number, number][]; | ||
6 | |||
7 | export default class Ferdium extends dbus.interface.Interface { | ||
8 | constructor(private readonly dbus: DBus) { | ||
9 | super('org.ferdium.Ferdium'); | ||
10 | } | ||
11 | |||
12 | emitMutedChanged(): void { | ||
13 | Ferdium.emitPropertiesChanged(this, { Muted: this.dbus.muted }, []); | ||
14 | } | ||
15 | |||
16 | get Muted(): boolean { | ||
17 | return this.dbus.muted; | ||
18 | } | ||
19 | |||
20 | set Muted(muted: boolean) { | ||
21 | if (this.dbus.muted !== muted) { | ||
22 | this.ToggleMute(); | ||
23 | } | ||
24 | } | ||
25 | |||
26 | ToggleMute(): void { | ||
27 | this.dbus.trayIcon.mainWindow?.webContents.send('muteApp'); | ||
28 | } | ||
29 | |||
30 | ToggleWindow(): void { | ||
31 | this.dbus.trayIcon._toggleWindow(); | ||
32 | } | ||
33 | |||
34 | emitUnreadChanged(): void { | ||
35 | Ferdium.emitPropertiesChanged( | ||
36 | this, | ||
37 | { | ||
38 | UnreadDirectMessageCount: this.dbus.unreadDirectMessageCount, | ||
39 | UnreadIndirectMessageCount: this.dbus.unreadIndirectMessageCount, | ||
40 | UnreadServices: this.dbus.unreadServices, | ||
41 | }, | ||
42 | [], | ||
43 | ); | ||
44 | } | ||
45 | |||
46 | get UnreadDirectMessageCount(): number { | ||
47 | return this.dbus.unreadDirectMessageCount; | ||
48 | } | ||
49 | |||
50 | get UnreadIndirectMessageCount(): number { | ||
51 | return this.dbus.unreadIndirectMessageCount; | ||
52 | } | ||
53 | |||
54 | get UnreadServices(): UnreadServices { | ||
55 | return this.dbus.unreadServices; | ||
56 | } | ||
57 | } | ||
58 | |||
59 | Ferdium.configureMembers({ | ||
60 | methods: { | ||
61 | ToggleMute: { | ||
62 | inSignature: '', | ||
63 | outSignature: '', | ||
64 | }, | ||
65 | ToggleWindow: { | ||
66 | inSignature: '', | ||
67 | outSignature: '', | ||
68 | }, | ||
69 | }, | ||
70 | properties: { | ||
71 | Muted: { | ||
72 | signature: 'b', | ||
73 | access: dbus.interface.ACCESS_READWRITE, | ||
74 | }, | ||
75 | UnreadDirectMessageCount: { | ||
76 | signature: 'u', | ||
77 | access: dbus.interface.ACCESS_READ, | ||
78 | }, | ||
79 | UnreadIndirectMessageCount: { | ||
80 | signature: 'u', | ||
81 | access: dbus.interface.ACCESS_READ, | ||
82 | }, | ||
83 | UnreadServices: { | ||
84 | signature: 'a(suu)', | ||
85 | access: dbus.interface.ACCESS_READ, | ||
86 | }, | ||
87 | }, | ||
88 | }); | ||
diff --git a/src/stores/ServicesStore.ts b/src/stores/ServicesStore.ts index 0ab4dbc5b..829c64d76 100644 --- a/src/stores/ServicesStore.ts +++ b/src/stores/ServicesStore.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { shell } from 'electron'; | 1 | import { ipcRenderer, shell } from 'electron'; |
2 | import { action, reaction, computed, observable, makeObservable } from 'mobx'; | 2 | import { action, reaction, computed, observable, makeObservable } from 'mobx'; |
3 | import { debounce, remove } from 'lodash'; | 3 | import { debounce, remove } from 'lodash'; |
4 | import ms from 'ms'; | 4 | import ms from 'ms'; |
@@ -23,6 +23,7 @@ import { cleanseJSObject } from '../jsUtils'; | |||
23 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; | 23 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; |
24 | import { ferdiumVersion } from '../environment-remote'; | 24 | import { ferdiumVersion } from '../environment-remote'; |
25 | import TypedStore from './lib/TypedStore'; | 25 | import TypedStore from './lib/TypedStore'; |
26 | import type { UnreadServices } from '../lib/dbus/Ferdium'; | ||
26 | 27 | ||
27 | const debug = require('../preload-safe-debug')('Ferdium:ServiceStore'); | 28 | const debug = require('../preload-safe-debug')('Ferdium:ServiceStore'); |
28 | 29 | ||
@@ -1230,26 +1231,29 @@ export default class ServicesStore extends TypedStore { | |||
1230 | const { showMessageBadgeWhenMuted } = this.stores.settings.all.app; | 1231 | const { showMessageBadgeWhenMuted } = this.stores.settings.all.app; |
1231 | const { showMessageBadgesEvenWhenMuted } = this.stores.ui; | 1232 | const { showMessageBadgesEvenWhenMuted } = this.stores.ui; |
1232 | 1233 | ||
1233 | const unreadDirectMessageCount = this.allDisplayed | 1234 | const unreadServices: UnreadServices = []; |
1234 | .filter( | 1235 | let unreadDirectMessageCount = 0; |
1235 | s => | 1236 | let unreadIndirectMessageCount = 0; |
1236 | (showMessageBadgeWhenMuted || s.isNotificationEnabled) && | 1237 | |
1237 | showMessageBadgesEvenWhenMuted && | 1238 | if (showMessageBadgesEvenWhenMuted) { |
1238 | s.isBadgeEnabled, | 1239 | for (const s of this.allDisplayed) { |
1239 | ) | 1240 | if (s.isBadgeEnabled) { |
1240 | .map(s => s.unreadDirectMessageCount) | 1241 | const direct = |
1241 | .reduce((a, b) => a + b, 0); | 1242 | showMessageBadgeWhenMuted || s.isNotificationEnabled |
1242 | 1243 | ? s.unreadDirectMessageCount | |
1243 | const unreadIndirectMessageCount = this.allDisplayed | 1244 | : 0; |
1244 | .filter( | 1245 | const indirect = |
1245 | s => | 1246 | showMessageBadgeWhenMuted && s.isIndirectMessageBadgeEnabled |
1246 | showMessageBadgeWhenMuted && | 1247 | ? s.unreadIndirectMessageCount |
1247 | showMessageBadgesEvenWhenMuted && | 1248 | : 0; |
1248 | s.isBadgeEnabled && | 1249 | unreadDirectMessageCount += direct; |
1249 | s.isIndirectMessageBadgeEnabled, | 1250 | unreadIndirectMessageCount += indirect; |
1250 | ) | 1251 | if (direct > 0 || indirect > 0) { |
1251 | .map(s => s.unreadIndirectMessageCount) | 1252 | unreadServices.push([s.name, direct, indirect]); |
1252 | .reduce((a, b) => a + b, 0); | 1253 | } |
1254 | } | ||
1255 | } | ||
1256 | } | ||
1253 | 1257 | ||
1254 | // We can't just block this earlier, otherwise the mobx reaction won't be aware of the vars to watch in some cases | 1258 | // We can't just block this earlier, otherwise the mobx reaction won't be aware of the vars to watch in some cases |
1255 | if (showMessageBadgesEvenWhenMuted) { | 1259 | if (showMessageBadgesEvenWhenMuted) { |
@@ -1257,6 +1261,12 @@ export default class ServicesStore extends TypedStore { | |||
1257 | unreadDirectMessageCount, | 1261 | unreadDirectMessageCount, |
1258 | unreadIndirectMessageCount, | 1262 | unreadIndirectMessageCount, |
1259 | }); | 1263 | }); |
1264 | ipcRenderer.send( | ||
1265 | 'updateDBusUnread', | ||
1266 | unreadDirectMessageCount, | ||
1267 | unreadIndirectMessageCount, | ||
1268 | unreadServices, | ||
1269 | ); | ||
1260 | } | 1270 | } |
1261 | } | 1271 | } |
1262 | 1272 | ||