Skip to content

Commit 3694482

Browse files
committed
Add flatpak portal manager, autostart on flatpaks
Assisted-by: Claude Code, Sonnet 4.5 Signed-off-by: Andrew Bernal <[email protected]>
1 parent 7df8a38 commit 3694482

File tree

4 files changed

+209
-6
lines changed

4 files changed

+209
-6
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

77
## [Unreleased]
8+
### Added
9+
- Autostart functionality in Flatpaks
10+
811
### Fixed
912
- hide autostart option for Windows store
1013

app/main.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ app.on('before-quit', (event) => {
193193
event.preventDefault()
194194
} else {
195195
globalShortcut.unregisterAll()
196+
// Clean up D-Bus connections
197+
if (autostartManager) {
198+
autostartManager.disconnect()
199+
}
196200
app.quit()
197201
}
198202
})
@@ -290,9 +294,17 @@ async function initialize (isAppStart = true) {
290294
autostartManager = new AutostartManager({
291295
platform: process.platform,
292296
windowsStore: insideWindowsStore(),
293-
app
297+
app,
298+
settings
294299
})
295300

301+
// Initialize portal early for Flatpak so it's ready when user opens preferences
302+
if (process.platform === 'linux' && insideFlatpak()) {
303+
autostartManager.flatpakPortalManager.initialize().catch(err => {
304+
log.error('Stretchly: Failed to initialize portal manager during startup:', err)
305+
})
306+
}
307+
296308
displayManager = new DisplayManager(settings)
297309

298310
startI18next()

app/utils/autostartManager.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,54 @@
11
import log from 'electron-log/main.js'
22
import AutoLaunch from 'auto-launch'
3+
import FlatpakPortalManager from './flatpakPortalManager.js'
4+
import { insideFlatpak } from './utils.js'
35

46
class AutostartManager {
57
constructor ({
68
platform,
79
windowsStore,
8-
app
10+
app,
11+
settings
912
}) {
1013
this.platform = platform
1114
this.windowsStore = windowsStore
1215
this.app = app
16+
this.settings = settings
17+
this.flatpakPortalManager = new FlatpakPortalManager()
1318
}
1419

15-
setAutostartEnabled (value) {
16-
if (this.platform === 'linux') {
20+
async setAutostartEnabled (value) {
21+
log.info(`Stretchly: setting autostart to ${value} on ${this.platform}${this.platform === 'win32' && this.windowsStore ? ' (Windows Store)' : ''}${insideFlatpak() && this.platform === 'linux' ? ' (Flatpak)' : ''}`)
22+
if (this.platform === 'linux' && insideFlatpak()) {
23+
try {
24+
// Initialize portal manager first
25+
await this.flatpakPortalManager.initialize()
26+
27+
const result = await this.flatpakPortalManager.setAutostart(value)
28+
29+
// Only save to settings if portal call succeeded
30+
if (result) {
31+
this.settings.set('flatpakAutostart', value)
32+
log.info(`Stretchly: Saved flatpakAutostart=${value} to settings after successful portal call`)
33+
} else {
34+
log.warn('Stretchly: Portal call returned false, not saving flatpakAutostart setting')
35+
}
36+
} catch (error) {
37+
log.error('Stretchly: Failed to set autostart via XDG Portal. No fallback available for Flatpak.', error)
38+
}
39+
} else if (this.platform === 'linux') {
1740
value ? this._linuxAutoLaunch.enable() : this._linuxAutoLaunch.disable()
1841
} else if (this.platform === 'win32' && this.windowsStore) {
1942
value ? this._windowsStoreAutoLaunch.enable() : this._windowsStoreAutoLaunch.disable()
2043
} else {
2144
this.app.setLoginItemSettings({ openAtLogin: value })
2245
}
23-
log.info(`Stretchly: setting autostart to ${value} on ${this.platform}${this.platform === 'win32' && this.windowsStore ? ' (Windows Store)' : ''}`)
2446
}
2547

2648
async autoLaunchStatus () {
27-
if (this.platform === 'linux') {
49+
if (this.platform === 'linux' && insideFlatpak()) {
50+
return this.settings.get('flatpakAutostart', false)
51+
} else if (this.platform === 'linux') {
2852
return await this._linuxAutoLaunch.isEnabled()
2953
} else if (this.platform === 'win32' && this.windowsStore) {
3054
return await this._windowsStoreAutoLaunch.isEnabled()
@@ -48,6 +72,12 @@ class AutostartManager {
4872
})
4973
return stretchlyAutoLaunch
5074
}
75+
76+
disconnect () {
77+
if (this.flatpakPortalManager) {
78+
this.flatpakPortalManager.disconnect()
79+
}
80+
}
5181
}
5282

5383
export default AutostartManager

app/utils/flatpakPortalManager.js

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import dbus from '@particle/dbus-next'
2+
import log from 'electron-log/main.js'
3+
4+
const { Variant } = dbus
5+
6+
class FlatpakPortalManager {
7+
constructor () {
8+
this.bus = null
9+
this.portal = null
10+
this.initialized = false
11+
}
12+
13+
async initialize () {
14+
if (this.initialized) return
15+
16+
// Clean up any stale connection first
17+
if (this.bus) {
18+
try {
19+
this.bus.disconnect()
20+
} catch (err) {
21+
// Ignore errors disconnecting stale connection
22+
}
23+
this.bus = null
24+
this.portal = null
25+
}
26+
27+
try {
28+
this.bus = dbus.sessionBus()
29+
this.portal = await this.bus.getProxyObject(
30+
'org.freedesktop.portal.Desktop',
31+
'/org/freedesktop/portal/desktop'
32+
)
33+
this.initialized = true
34+
log.info('Stretchly: XDG Background Portal initialized successfully')
35+
} catch (error) {
36+
log.error('Stretchly: Failed to initialize XDG Background Portal:', error)
37+
this.initialized = false
38+
throw error
39+
}
40+
}
41+
42+
/**
43+
* Sets the autostart status for the application using the XDG Background Portal.
44+
* @param {boolean} enabled - True to enable autostart, false to disable.
45+
* @returns {Promise<boolean>} - True if the autostart status was successfully set.
46+
*/
47+
async setAutostart (enabled) {
48+
// Ensure initialized (safe to call multiple times)
49+
await this.initialize()
50+
51+
try {
52+
const background = this.portal.getInterface('org.freedesktop.portal.Background')
53+
54+
// Create a unique handle token for this request
55+
const handleToken = `stretchly_autostart_${Date.now()}_${Math.random().toString(36).substring(7)}`
56+
57+
const options = {
58+
handle_token: new Variant('s', handleToken),
59+
reason: new Variant('s', 'Stretchly needs to run in the background to remind you to take breaks'),
60+
autostart: new Variant('b', enabled),
61+
'dbus-activatable': new Variant('b', false)
62+
}
63+
64+
// When DISABLING, the portal doesn't create a persistent request object
65+
// It's a fire-and-forget operation - no Response signal to wait for
66+
if (!enabled) {
67+
await background.RequestBackground('', options)
68+
log.info('Stretchly: Autostart disabled via XDG Portal (no response expected).')
69+
return true
70+
}
71+
72+
// When ENABLING, we must wait for the Response signal
73+
return new Promise((resolve) => {
74+
const timeoutMs = 30000 // 30 second timeout
75+
let timeoutId = null
76+
let messageListener = null // Store the listener function here
77+
78+
const cleanup = () => {
79+
if (timeoutId) {
80+
clearTimeout(timeoutId)
81+
timeoutId = null
82+
}
83+
if (messageListener && this.bus) {
84+
// The actual listener removal
85+
this.bus.off('message', messageListener)
86+
messageListener = null
87+
}
88+
}
89+
90+
// Define the listener function
91+
messageListener = (msg) => {
92+
// Check if this message is the one we're waiting for
93+
if (msg.interface === 'org.freedesktop.portal.Request' &&
94+
msg.member === 'Response' &&
95+
msg.path.includes(handleToken)) {
96+
const [response, results] = msg.body
97+
98+
cleanup() // Clean up immediately (removes listener + timeout)
99+
100+
log.info(`Stretchly: Portal Response signal received - response: ${response}, results:`, results)
101+
102+
if (response === 0) { // Success
103+
const autostartGranted = results && results.autostart && results.autostart.value === enabled
104+
if (autostartGranted) {
105+
log.info('Stretchly: Autostart successfully enabled via XDG Portal.')
106+
} else {
107+
log.warn('Stretchly: Autostart status did not match request. Results:', results)
108+
}
109+
resolve(autostartGranted)
110+
} else if (response === 1) { // User cancelled
111+
log.warn('Stretchly: User cancelled the portal request.')
112+
resolve(false)
113+
} else { // Other error
114+
log.error(`Stretchly: Portal request failed with response code: ${response}`)
115+
resolve(false)
116+
}
117+
}
118+
}
119+
120+
// Set the timeout
121+
timeoutId = setTimeout(() => {
122+
cleanup() // This will now remove the listener
123+
log.error('Stretchly: Portal request timeout after 30 seconds')
124+
resolve(false)
125+
}, timeoutMs)
126+
127+
// Listen for Response signals
128+
this.bus.on('message', messageListener) // Register the named listener
129+
130+
// NOW make the request
131+
background.RequestBackground('', options)
132+
.then(requestPath => {
133+
log.info(`Stretchly: RequestBackground called for autostart=true, request path: ${requestPath}`)
134+
})
135+
.catch(err => {
136+
cleanup() // This will now remove the listener
137+
log.error('Stretchly: Failed to call RequestBackground:', err)
138+
resolve(false)
139+
})
140+
})
141+
} catch (error) {
142+
log.error(`Stretchly: Failed to set autostart=${enabled} via XDG Portal:`, error)
143+
throw error
144+
}
145+
}
146+
147+
disconnect () {
148+
if (this.bus) {
149+
this.bus.disconnect()
150+
this.bus = null
151+
this.portal = null
152+
this.initialized = false
153+
log.info('Stretchly: XDG Background Portal disconnected')
154+
}
155+
}
156+
}
157+
158+
export default FlatpakPortalManager

0 commit comments

Comments
 (0)