Skip to content

Commit 62d3265

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 62d3265

File tree

3 files changed

+196
-4
lines changed

3 files changed

+196
-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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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()}`
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+
// Call RequestBackground and AWAIT the request path
56+
const requestPath = await background.RequestBackground('', options)
57+
log.info(`Stretchly: RequestBackground called for autostart=${enabled}, request path: ${requestPath}`)
58+
59+
// When DISABLING, the portal doesn't create a persistent request object
60+
// It's a fire-and-forget operation - no Response signal to wait for
61+
if (!enabled) {
62+
log.info('Stretchly: Autostart successfully disabled via XDG Portal (no response expected).')
63+
return true
64+
}
65+
66+
// When ENABLING, we must wait for the Response signal
67+
return new Promise((resolve, reject) => {
68+
const timeoutMs = 30000 // 30 second timeout
69+
let requestInterface = null
70+
71+
const cleanup = () => {
72+
if (timeoutId) clearTimeout(timeoutId)
73+
// Safely remove the specific listener
74+
if (requestInterface) {
75+
try {
76+
requestInterface.off('Response', signalHandler)
77+
} catch (err) {
78+
log.warn('Stretchly: Error cleaning up D-Bus signal listener:', err)
79+
}
80+
}
81+
}
82+
83+
const signalHandler = (response, results) => {
84+
cleanup()
85+
log.info(`Stretchly: Portal Response signal received - response: ${response}, results:`, results)
86+
87+
if (response === 0) {
88+
// Success
89+
const autostartGranted = results && results.autostart && results.autostart.value === enabled
90+
if (autostartGranted) {
91+
log.info(`Stretchly: Autostart successfully ${enabled ? 'enabled' : 'disabled'} via XDG Portal.`)
92+
} else {
93+
log.warn('Stretchly: Autostart status did not match request. Results:', results)
94+
}
95+
resolve(autostartGranted)
96+
} else if (response === 1) {
97+
// User cancelled - autostart remains in its previous state
98+
reject(new Error('User cancelled the portal request'))
99+
} else {
100+
// Other error
101+
reject(new Error(`Portal request failed with response code: ${response}`))
102+
}
103+
}
104+
105+
const timeoutId = setTimeout(() => {
106+
cleanup()
107+
reject(new Error('Portal request timeout after 30 seconds'))
108+
}, timeoutMs)
109+
110+
// Get the proxy object and interface for the received request path
111+
this.bus.getProxyObject('org.freedesktop.portal.Desktop', requestPath)
112+
.then(requestObj => {
113+
requestInterface = requestObj.getInterface('org.freedesktop.portal.Request')
114+
// Attach the signal listener
115+
requestInterface.on('Response', signalHandler)
116+
})
117+
.catch(err => {
118+
cleanup()
119+
log.error('Stretchly: Failed to get request proxy object or interface:', err)
120+
reject(new Error(`Failed to subscribe to Response signal: ${err.message}`))
121+
})
122+
})
123+
} catch (error) {
124+
log.error(`Stretchly: Failed to set autostart=${enabled} via XDG Portal:`, error)
125+
throw error
126+
}
127+
}
128+
129+
/**
130+
* Queries the current autostart status from the system.
131+
* We check if the autostart desktop file exists.
132+
* @returns {Promise<boolean>} - True if autostart is currently enabled.
133+
*/
134+
async getBackgroundStatus () {
135+
await this.initialize()
136+
137+
try {
138+
// The most reliable way to check autostart status is to check the desktop file
139+
// The portal creates a file in ~/.config/autostart/ when autostart is enabled
140+
const autostartPath = join(homedir(), '.config', 'autostart', 'net.hovancik.Stretchly.desktop')
141+
142+
try {
143+
await access(autostartPath, constants.F_OK)
144+
log.info('Stretchly: Autostart desktop file found, autostart is enabled')
145+
return true
146+
} catch {
147+
log.info('Stretchly: Autostart desktop file not found, autostart is disabled')
148+
return false
149+
}
150+
} catch (error) {
151+
log.error('Stretchly: Failed to check autostart status:', error)
152+
return false
153+
}
154+
}
155+
156+
disconnect () {
157+
if (this.bus) {
158+
this.bus.disconnect()
159+
this.bus = null
160+
this.portal = null
161+
this.initialized = false
162+
log.info('Stretchly: XDG Background Portal disconnected')
163+
}
164+
}
165+
}
166+
167+
export default FlatpakPortalManager

0 commit comments

Comments
 (0)