Skip to content

Commit 03efa71

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 03efa71

File tree

3 files changed

+212
-4
lines changed

3 files changed

+212
-4
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/utils/autostartManager.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
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 ({
@@ -10,21 +12,35 @@ class AutostartManager {
1012
this.platform = platform
1113
this.windowsStore = windowsStore
1214
this.app = app
15+
this.flatpakPortalManager = new FlatpakPortalManager()
1316
}
1417

15-
setAutostartEnabled (value) {
16-
if (this.platform === 'linux') {
18+
async setAutostartEnabled (value) {
19+
log.info(`Stretchly: setting autostart to ${value} on ${this.platform}${this.platform === 'win32' && this.windowsStore ? ' (Windows Store)' : ''}${insideFlatpak() && this.platform === 'linux' ? ' (Flatpak)' : ''}`)
20+
if (this.platform === 'linux' && insideFlatpak()) {
21+
try {
22+
await this.flatpakPortalManager.setAutostart(value)
23+
} catch (error) {
24+
log.error('Stretchly: Failed to set autostart via XDG Portal. No fallback available for Flatpak.', error)
25+
}
26+
} else if (this.platform === 'linux') {
1727
value ? this._linuxAutoLaunch.enable() : this._linuxAutoLaunch.disable()
1828
} else if (this.platform === 'win32' && this.windowsStore) {
1929
value ? this._windowsStoreAutoLaunch.enable() : this._windowsStoreAutoLaunch.disable()
2030
} else {
2131
this.app.setLoginItemSettings({ openAtLogin: value })
2232
}
23-
log.info(`Stretchly: setting autostart to ${value} on ${this.platform}${this.platform === 'win32' && this.windowsStore ? ' (Windows Store)' : ''}`)
2433
}
2534

2635
async autoLaunchStatus () {
27-
if (this.platform === 'linux') {
36+
if (this.platform === 'linux' && insideFlatpak()) {
37+
try {
38+
return await this.flatpakPortalManager.getBackgroundStatus()
39+
} catch (error) {
40+
log.error('Stretchly: Failed to get autostart status via XDG Portal:', error)
41+
return false
42+
}
43+
} else if (this.platform === 'linux') {
2844
return await this._linuxAutoLaunch.isEnabled()
2945
} else if (this.platform === 'win32' && this.windowsStore) {
3046
return await this._windowsStoreAutoLaunch.isEnabled()
@@ -48,6 +64,12 @@ class AutostartManager {
4864
})
4965
return stretchlyAutoLaunch
5066
}
67+
68+
disconnect () {
69+
if (this.flatpakPortalManager) {
70+
this.flatpakPortalManager.disconnect()
71+
}
72+
}
5173
}
5274

5375
export default AutostartManager

app/utils/flatpakPortalManager.js

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import dbus from '@particle/dbus-next'
2+
import log from 'electron-log/main.js'
3+
import { homedir } from 'node:os'
4+
import { access, constants } from 'node:fs/promises'
5+
import { join } from 'node:path'
6+
7+
const { Variant } = dbus
8+
9+
class FlatpakPortalManager {
10+
constructor () {
11+
this.bus = null
12+
this.portal = null
13+
this.initialized = false
14+
}
15+
16+
async initialize () {
17+
if (this.initialized) return
18+
19+
try {
20+
this.bus = dbus.sessionBus()
21+
this.portal = await this.bus.getProxyObject(
22+
'org.freedesktop.portal.Desktop',
23+
'/org/freedesktop/portal/desktop'
24+
)
25+
this.initialized = true
26+
log.info('Stretchly: XDG Background Portal initialized successfully')
27+
} catch (error) {
28+
log.error('Stretchly: Failed to initialize XDG Background Portal:', error)
29+
this.initialized = false
30+
throw error
31+
}
32+
}
33+
34+
/**
35+
* Sets the autostart status for the application using the XDG Background Portal.
36+
* @param {boolean} enabled - True to enable autostart, false to disable.
37+
* @returns {Promise<boolean>} - True if the autostart status was successfully set.
38+
*/
39+
async setAutostart (enabled) {
40+
await this.initialize()
41+
42+
try {
43+
const background = this.portal.getInterface('org.freedesktop.portal.Background')
44+
45+
// Create a unique handle token for this request
46+
const handleToken = `stretchly_autostart_${Date.now()}_${Math.random().toString(36).substring(7)}`
47+
48+
const options = {
49+
handle_token: new Variant('s', handleToken),
50+
reason: new Variant('s', 'Stretchly needs to run in the background to remind you to take breaks'),
51+
autostart: new Variant('b', enabled),
52+
'dbus-activatable': new Variant('b', false)
53+
}
54+
55+
// When DISABLING, the portal doesn't create a persistent request object
56+
// It's a fire-and-forget operation - no Response signal to wait for
57+
if (!enabled) {
58+
await background.RequestBackground('', options)
59+
log.info('Stretchly: Autostart disabled via XDG Portal (no response expected).')
60+
return true
61+
}
62+
63+
// When ENABLING, we must wait for the Response signal
64+
// Set up the listener on the portal object BEFORE making the request to avoid race condition
65+
return new Promise((resolve, reject) => {
66+
const timeoutMs = 30000 // 30 second timeout
67+
let timeoutId = null
68+
69+
const cleanup = () => {
70+
if (timeoutId) {
71+
clearTimeout(timeoutId)
72+
timeoutId = null
73+
}
74+
}
75+
76+
const signalHandler = (requestPath, response, results) => {
77+
// Check if this response is for our specific request by matching the path
78+
// The requestPath should contain our handle token
79+
if (!requestPath.includes(handleToken)) {
80+
return // Not our request, ignore it
81+
}
82+
83+
cleanup()
84+
85+
// Remove this specific listener
86+
try {
87+
this.portal.off(`/org/freedesktop/portal/desktop/request/${this.bus.name.replace(/\./g, '_').replace(/:/g, '_')}/${handleToken}`, 'Response', signalHandler)
88+
} catch (err) {
89+
// Listener might already be removed, that's fine
90+
}
91+
92+
log.info(`Stretchly: Portal Response signal received - response: ${response}, results:`, results)
93+
94+
if (response === 0) {
95+
// Success
96+
const autostartGranted = results && results.autostart && results.autostart.value === enabled
97+
if (autostartGranted) {
98+
log.info('Stretchly: Autostart successfully enabled via XDG Portal.')
99+
} else {
100+
log.warn('Stretchly: Autostart status did not match request. Results:', results)
101+
}
102+
resolve(autostartGranted)
103+
} else if (response === 1) {
104+
// User cancelled - autostart remains in its previous state
105+
reject(new Error('User cancelled the portal request'))
106+
} else {
107+
// Other error
108+
reject(new Error(`Portal request failed with response code: ${response}`))
109+
}
110+
}
111+
112+
timeoutId = setTimeout(() => {
113+
cleanup()
114+
reject(new Error('Portal request timeout after 30 seconds'))
115+
}, timeoutMs)
116+
117+
// Listen for Response signals on ANY request path
118+
// We'll filter by our handle token in the signal handler
119+
this.bus.on('message', (msg) => {
120+
if (msg.interface === 'org.freedesktop.portal.Request' &&
121+
msg.member === 'Response' &&
122+
msg.path.includes(handleToken)) {
123+
const [response, results] = msg.body
124+
signalHandler(msg.path, response, results)
125+
}
126+
})
127+
128+
// NOW make the request
129+
background.RequestBackground('', options)
130+
.then(requestPath => {
131+
log.info(`Stretchly: RequestBackground called for autostart=true, request path: ${requestPath}`)
132+
})
133+
.catch(err => {
134+
cleanup()
135+
log.error('Stretchly: Failed to call RequestBackground:', err)
136+
reject(new Error(`Failed to call RequestBackground: ${err.message}`))
137+
})
138+
})
139+
} catch (error) {
140+
log.error(`Stretchly: Failed to set autostart=${enabled} via XDG Portal:`, error)
141+
throw error
142+
}
143+
}
144+
145+
/**
146+
* Queries the current autostart status from the system.
147+
* We check if the autostart desktop file exists.
148+
* @returns {Promise<boolean>} - True if autostart is currently enabled.
149+
*/
150+
async getBackgroundStatus () {
151+
await this.initialize()
152+
153+
try {
154+
// The most reliable way to check autostart status is to check the desktop file
155+
// The portal creates a file in ~/.config/autostart/ when autostart is enabled
156+
const autostartPath = join(homedir(), '.config', 'autostart', 'net.hovancik.Stretchly.desktop')
157+
158+
try {
159+
await access(autostartPath, constants.F_OK)
160+
log.info('Stretchly: Autostart desktop file found, autostart is enabled')
161+
return true
162+
} catch {
163+
log.info('Stretchly: Autostart desktop file not found, autostart is disabled')
164+
return false
165+
}
166+
} catch (error) {
167+
log.error('Stretchly: Failed to check autostart status:', error)
168+
return false
169+
}
170+
}
171+
172+
disconnect () {
173+
if (this.bus) {
174+
this.bus.disconnect()
175+
this.bus = null
176+
this.portal = null
177+
this.initialized = false
178+
log.info('Stretchly: XDG Background Portal disconnected')
179+
}
180+
}
181+
}
182+
183+
export default FlatpakPortalManager

0 commit comments

Comments
 (0)