pinokiod 3.270.0 → 3.272.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kernel/ansi_stream_tracker.js +115 -0
- package/kernel/api/app/index.js +422 -0
- package/kernel/api/htmlmodal/index.js +94 -0
- package/kernel/app_launcher/index.js +115 -0
- package/kernel/app_launcher/platform/base.js +276 -0
- package/kernel/app_launcher/platform/linux.js +229 -0
- package/kernel/app_launcher/platform/macos.js +163 -0
- package/kernel/app_launcher/platform/unsupported.js +34 -0
- package/kernel/app_launcher/platform/windows.js +247 -0
- package/kernel/bin/conda-meta.js +93 -0
- package/kernel/bin/conda.js +2 -4
- package/kernel/bin/index.js +2 -4
- package/kernel/index.js +9 -2
- package/kernel/peer.js +186 -19
- package/kernel/shell.js +212 -1
- package/package.json +1 -1
- package/server/index.js +491 -6
- package/server/public/common.js +224 -741
- package/server/public/create-launcher.js +754 -0
- package/server/public/htmlmodal.js +292 -0
- package/server/public/logs.js +715 -0
- package/server/public/resizeSync.js +117 -0
- package/server/public/style.css +651 -6
- package/server/public/tab-idle-notifier.js +34 -59
- package/server/public/tab-link-popover.js +7 -10
- package/server/public/terminal-settings.js +723 -9
- package/server/public/terminal_input_utils.js +72 -0
- package/server/public/terminal_key_caption.js +187 -0
- package/server/public/urldropdown.css +120 -3
- package/server/public/xterm-inline-bridge.js +116 -0
- package/server/socket.js +29 -0
- package/server/views/agents.ejs +1 -2
- package/server/views/app.ejs +55 -28
- package/server/views/bookmarklet.ejs +1 -1
- package/server/views/bootstrap.ejs +1 -0
- package/server/views/connect.ejs +1 -2
- package/server/views/create.ejs +63 -0
- package/server/views/editor.ejs +36 -4
- package/server/views/index.ejs +1 -2
- package/server/views/index2.ejs +1 -2
- package/server/views/init/index.ejs +36 -28
- package/server/views/install.ejs +20 -22
- package/server/views/layout.ejs +2 -8
- package/server/views/logs.ejs +155 -0
- package/server/views/mini.ejs +0 -18
- package/server/views/net.ejs +2 -2
- package/server/views/network.ejs +1 -2
- package/server/views/network2.ejs +1 -2
- package/server/views/old_network.ejs +1 -2
- package/server/views/pro.ejs +26 -23
- package/server/views/prototype/index.ejs +30 -34
- package/server/views/screenshots.ejs +1 -2
- package/server/views/settings.ejs +1 -20
- package/server/views/shell.ejs +59 -66
- package/server/views/terminal.ejs +118 -73
- package/server/views/tools.ejs +1 -2
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const os = require('os')
|
|
2
|
+
|
|
3
|
+
const PLATFORM_MODULE = {
|
|
4
|
+
darwin: './platform/macos',
|
|
5
|
+
win32: './platform/windows',
|
|
6
|
+
linux: './platform/linux'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class AppLauncher {
|
|
10
|
+
constructor(kernel) {
|
|
11
|
+
this.kernel = kernel
|
|
12
|
+
this.platform = os.platform()
|
|
13
|
+
this.adapter = null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async init() {
|
|
17
|
+
try {
|
|
18
|
+
const adapter = await this.ensureAdapter()
|
|
19
|
+
if (adapter && adapter.ensureIndex) {
|
|
20
|
+
await adapter.ensureIndex({ force: false })
|
|
21
|
+
}
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.warn('[AppLauncher] init failed:', error.message)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
resolveAdapterModule() {
|
|
28
|
+
return PLATFORM_MODULE[this.platform] || './platform/unsupported'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async ensureAdapter() {
|
|
32
|
+
if (this.adapter) {
|
|
33
|
+
return this.adapter
|
|
34
|
+
}
|
|
35
|
+
const modulePath = this.resolveAdapterModule()
|
|
36
|
+
const Adapter = require(modulePath)
|
|
37
|
+
this.adapter = new Adapter(this.kernel)
|
|
38
|
+
return this.adapter
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async search(params = {}) {
|
|
42
|
+
const adapter = await this.ensureAdapter()
|
|
43
|
+
return adapter.search(params)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async info(params = {}) {
|
|
47
|
+
if (!params || !params.id) {
|
|
48
|
+
throw new Error('app.info requires params.id')
|
|
49
|
+
}
|
|
50
|
+
const adapter = await this.ensureAdapter()
|
|
51
|
+
return adapter.info(params.id, { force: params.refresh })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async findMatch(query, options = {}) {
|
|
55
|
+
if (!query) {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
const adapter = await this.ensureAdapter()
|
|
59
|
+
return adapter.findMatch(query, options)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async refresh(params = {}) {
|
|
63
|
+
const adapter = await this.ensureAdapter()
|
|
64
|
+
return adapter.refresh(params)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async launch(params = {}) {
|
|
68
|
+
if (!params || (!params.id && !params.app)) {
|
|
69
|
+
throw new Error('app.launch requires params.app or params.id')
|
|
70
|
+
}
|
|
71
|
+
const adapter = await this.ensureAdapter()
|
|
72
|
+
if (params.refresh) {
|
|
73
|
+
await adapter.ensureIndex({ force: true })
|
|
74
|
+
} else {
|
|
75
|
+
await adapter.ensureIndex({ force: false })
|
|
76
|
+
}
|
|
77
|
+
let entry = null
|
|
78
|
+
let confidence = null
|
|
79
|
+
if (params.id) {
|
|
80
|
+
entry = adapter.getEntry(params.id)
|
|
81
|
+
}
|
|
82
|
+
if (!entry && params.app) {
|
|
83
|
+
const match = await adapter.findMatch(params.app, { force: false })
|
|
84
|
+
if (match) {
|
|
85
|
+
entry = match.entry
|
|
86
|
+
confidence = match.score
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!entry) {
|
|
90
|
+
if (!params.install && adapter && typeof adapter.launchUnknown === 'function') {
|
|
91
|
+
return adapter.launchUnknown(params)
|
|
92
|
+
}
|
|
93
|
+
const error = new Error(params.app ? `Application "${params.app}" was not found` : 'Application not found')
|
|
94
|
+
error.code = 'APP_NOT_FOUND'
|
|
95
|
+
error.app = params.app || params.id
|
|
96
|
+
throw error
|
|
97
|
+
}
|
|
98
|
+
const result = await adapter.launch(entry, params)
|
|
99
|
+
if (result && typeof result === 'object') {
|
|
100
|
+
const response = Object.assign({}, result)
|
|
101
|
+
response.id = response.id || entry.id
|
|
102
|
+
response.name = response.name || entry.name
|
|
103
|
+
response.platform = response.platform || entry.platform || this.platform
|
|
104
|
+
response.kind = response.kind || entry.kind
|
|
105
|
+
response.detail = response.detail || entry.detail
|
|
106
|
+
if (confidence !== null && typeof confidence !== 'undefined') {
|
|
107
|
+
response.confidence = confidence
|
|
108
|
+
}
|
|
109
|
+
return response
|
|
110
|
+
}
|
|
111
|
+
return result
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = AppLauncher
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
const { spawn } = require('child_process')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
|
|
4
|
+
class BasePlatformAdapter {
|
|
5
|
+
constructor(kernel) {
|
|
6
|
+
this.kernel = kernel
|
|
7
|
+
this.platform = os.platform()
|
|
8
|
+
this.entries = new Map()
|
|
9
|
+
this.indexTimestamp = 0
|
|
10
|
+
this.indexTTL = 1000 * 60 * 5
|
|
11
|
+
this.indexPromise = null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async ensureIndex(options = {}) {
|
|
15
|
+
const force = Boolean(options.force)
|
|
16
|
+
const now = Date.now()
|
|
17
|
+
const expired = !this.indexTimestamp || (now - this.indexTimestamp > this.indexTTL)
|
|
18
|
+
if (!force && this.entries.size > 0 && !expired) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
if (this.indexPromise) {
|
|
22
|
+
return this.indexPromise
|
|
23
|
+
}
|
|
24
|
+
this.indexPromise = (async () => {
|
|
25
|
+
try {
|
|
26
|
+
await this.buildIndex()
|
|
27
|
+
this.indexTimestamp = Date.now()
|
|
28
|
+
} finally {
|
|
29
|
+
this.indexPromise = null
|
|
30
|
+
}
|
|
31
|
+
})()
|
|
32
|
+
return this.indexPromise
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// subclasses must override
|
|
36
|
+
async buildIndex() {
|
|
37
|
+
throw new Error('buildIndex() not implemented')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
listEntries() {
|
|
41
|
+
return Array.from(this.entries.values())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getEntry(id) {
|
|
45
|
+
if (!id) {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
return this.entries.get(id)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addEntry(entry) {
|
|
52
|
+
if (!entry || !entry.id) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
const aliases = Array.isArray(entry.aliases) ? entry.aliases.filter(Boolean) : []
|
|
56
|
+
const uniqueAliases = Array.from(new Set([entry.name, ...aliases].filter(Boolean)))
|
|
57
|
+
entry.aliases = uniqueAliases
|
|
58
|
+
entry.platform = entry.platform || this.platform
|
|
59
|
+
this.entries.set(entry.id, entry)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
normalize(text) {
|
|
63
|
+
return (text || '').trim().toLowerCase()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
scoreEntry(query, entry) {
|
|
67
|
+
if (!query) {
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
const candidates = new Set()
|
|
71
|
+
if (entry.name) {
|
|
72
|
+
candidates.add(entry.name)
|
|
73
|
+
}
|
|
74
|
+
if (entry.aliases) {
|
|
75
|
+
for (const alias of entry.aliases) {
|
|
76
|
+
candidates.add(alias)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (entry.id) {
|
|
80
|
+
candidates.add(entry.id)
|
|
81
|
+
}
|
|
82
|
+
if (entry.detail) {
|
|
83
|
+
candidates.add(entry.detail)
|
|
84
|
+
}
|
|
85
|
+
if (entry.bundleId) {
|
|
86
|
+
candidates.add(entry.bundleId)
|
|
87
|
+
}
|
|
88
|
+
if (entry.desktopId) {
|
|
89
|
+
candidates.add(entry.desktopId)
|
|
90
|
+
}
|
|
91
|
+
let best = Number.POSITIVE_INFINITY
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
const normalized = this.normalize(candidate)
|
|
94
|
+
if (!normalized) {
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
if (normalized === query) {
|
|
98
|
+
return 0
|
|
99
|
+
}
|
|
100
|
+
if (normalized.startsWith(query)) {
|
|
101
|
+
best = Math.min(best, 1)
|
|
102
|
+
} else if (normalized.includes(query)) {
|
|
103
|
+
best = Math.min(best, 2)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (best < Number.POSITIVE_INFINITY) {
|
|
107
|
+
return best
|
|
108
|
+
}
|
|
109
|
+
return best
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
formatEntry(entry) {
|
|
113
|
+
if (!entry) {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
const payload = {
|
|
117
|
+
id: entry.id,
|
|
118
|
+
name: entry.name,
|
|
119
|
+
platform: entry.platform || this.platform,
|
|
120
|
+
kind: entry.kind || 'app'
|
|
121
|
+
}
|
|
122
|
+
if (entry.aliases && entry.aliases.length > 0) {
|
|
123
|
+
payload.aliases = entry.aliases
|
|
124
|
+
}
|
|
125
|
+
if (entry.detail) {
|
|
126
|
+
payload.detail = entry.detail
|
|
127
|
+
}
|
|
128
|
+
const meta = {}
|
|
129
|
+
if (entry.bundleId) {
|
|
130
|
+
meta.bundleId = entry.bundleId
|
|
131
|
+
}
|
|
132
|
+
if (entry.desktopId) {
|
|
133
|
+
meta.desktopId = entry.desktopId
|
|
134
|
+
}
|
|
135
|
+
if (entry.appId) {
|
|
136
|
+
meta.appId = entry.appId
|
|
137
|
+
}
|
|
138
|
+
if (entry.path) {
|
|
139
|
+
meta.path = entry.path
|
|
140
|
+
}
|
|
141
|
+
if (entry.execPath) {
|
|
142
|
+
meta.execPath = entry.execPath
|
|
143
|
+
}
|
|
144
|
+
if (Object.keys(meta).length > 0) {
|
|
145
|
+
payload.meta = meta
|
|
146
|
+
}
|
|
147
|
+
return payload
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
sanitizeArgs(args) {
|
|
151
|
+
if (!args) {
|
|
152
|
+
return []
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(args)) {
|
|
155
|
+
return [String(args)]
|
|
156
|
+
}
|
|
157
|
+
return args
|
|
158
|
+
.filter((arg) => typeof arg !== 'undefined' && arg !== null)
|
|
159
|
+
.map((arg) => String(arg))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async search(options = {}) {
|
|
163
|
+
const { query = '', limit = 20, refresh = false } = options
|
|
164
|
+
await this.ensureIndex({ force: refresh })
|
|
165
|
+
const normalized = this.normalize(query)
|
|
166
|
+
let entries = this.listEntries()
|
|
167
|
+
if (!normalized) {
|
|
168
|
+
entries = entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
169
|
+
return entries.slice(0, limit).map((entry) => this.formatEntry(entry))
|
|
170
|
+
}
|
|
171
|
+
const scored = []
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const score = this.scoreEntry(normalized, entry)
|
|
174
|
+
if (!Number.isFinite(score)) {
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
if (score === Number.POSITIVE_INFINITY) {
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
scored.push({ entry, score })
|
|
181
|
+
}
|
|
182
|
+
scored.sort((a, b) => {
|
|
183
|
+
if (a.score !== b.score) {
|
|
184
|
+
return a.score - b.score
|
|
185
|
+
}
|
|
186
|
+
return a.entry.name.localeCompare(b.entry.name)
|
|
187
|
+
})
|
|
188
|
+
return scored.slice(0, limit).map((item) => this.formatEntry(item.entry))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async findMatch(name, options = {}) {
|
|
192
|
+
const normalized = this.normalize(name)
|
|
193
|
+
if (!normalized) {
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
await this.ensureIndex({ force: options.force })
|
|
197
|
+
let best = null
|
|
198
|
+
let bestScore = Number.POSITIVE_INFINITY
|
|
199
|
+
for (const entry of this.entries.values()) {
|
|
200
|
+
const score = this.scoreEntry(normalized, entry)
|
|
201
|
+
if (score < bestScore) {
|
|
202
|
+
best = entry
|
|
203
|
+
bestScore = score
|
|
204
|
+
if (score === 0) {
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!best) {
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
return { entry: best, score: bestScore }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async info(id, options = {}) {
|
|
216
|
+
await this.ensureIndex({ force: options.force })
|
|
217
|
+
const entry = this.getEntry(id)
|
|
218
|
+
return this.formatEntry(entry)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async refresh() {
|
|
222
|
+
await this.ensureIndex({ force: true })
|
|
223
|
+
return {
|
|
224
|
+
entries: this.entries.size,
|
|
225
|
+
platform: this.platform,
|
|
226
|
+
updatedAt: this.indexTimestamp
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async launch() {
|
|
231
|
+
throw new Error('launch() not implemented')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async launchUnknown(params = {}) {
|
|
235
|
+
if (!params.app) {
|
|
236
|
+
throw new Error('Application not found')
|
|
237
|
+
}
|
|
238
|
+
throw new Error(`Application "${params.app}" was not found in the app index`)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
spawnDetached(command, args, options = {}) {
|
|
242
|
+
const spawnOptions = Object.assign({
|
|
243
|
+
detached: true,
|
|
244
|
+
stdio: 'ignore',
|
|
245
|
+
windowsHide: true
|
|
246
|
+
}, options || {})
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
let child
|
|
249
|
+
try {
|
|
250
|
+
child = spawn(command, args, spawnOptions)
|
|
251
|
+
} catch (error) {
|
|
252
|
+
error.command = command
|
|
253
|
+
error.args = args
|
|
254
|
+
return reject(error)
|
|
255
|
+
}
|
|
256
|
+
let resolved = false
|
|
257
|
+
child.once('error', (err) => {
|
|
258
|
+
if (!resolved) {
|
|
259
|
+
err.command = command
|
|
260
|
+
err.args = args
|
|
261
|
+
reject(err)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
child.once('spawn', () => {
|
|
265
|
+
resolved = true
|
|
266
|
+
try {
|
|
267
|
+
child.unref()
|
|
268
|
+
} catch (_) {
|
|
269
|
+
}
|
|
270
|
+
resolve({ pid: child.pid, command, args })
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = BasePlatformAdapter
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const BaseAdapter = require('./base')
|
|
5
|
+
|
|
6
|
+
const DESKTOP_DIRS = [
|
|
7
|
+
'/usr/share/applications',
|
|
8
|
+
'/usr/local/share/applications',
|
|
9
|
+
'/var/lib/snapd/desktop/applications',
|
|
10
|
+
path.join(os.homedir(), '.local/share/applications')
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
class LinuxAdapter extends BaseAdapter {
|
|
14
|
+
constructor(kernel) {
|
|
15
|
+
super(kernel)
|
|
16
|
+
this.desktopDirs = DESKTOP_DIRS
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async buildIndex() {
|
|
20
|
+
this.entries.clear()
|
|
21
|
+
for (const dir of this.desktopDirs) {
|
|
22
|
+
await this.collectDesktopFiles(dir)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async collectDesktopFiles(root) {
|
|
27
|
+
if (!root) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
let entries
|
|
31
|
+
try {
|
|
32
|
+
entries = await fs.promises.readdir(root, { withFileTypes: true })
|
|
33
|
+
} catch (_) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const fullPath = path.join(root, entry.name)
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
await this.collectDesktopFiles(fullPath)
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (!entry.isFile() || !entry.name.endsWith('.desktop')) {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
await this.addDesktopEntry(fullPath)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async addDesktopEntry(filePath) {
|
|
50
|
+
let content
|
|
51
|
+
try {
|
|
52
|
+
content = await fs.promises.readFile(filePath, 'utf8')
|
|
53
|
+
} catch (_) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
const data = this.parseDesktopFile(content)
|
|
57
|
+
const name = this.resolveLocalizedValue(data, 'Name')
|
|
58
|
+
const execLine = data.Exec
|
|
59
|
+
if (!name || !execLine) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if (String(data.NoDisplay || '').toLowerCase() === 'true') {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const desktopId = path.basename(filePath, '.desktop')
|
|
66
|
+
let id = `desktop:${desktopId}`
|
|
67
|
+
if (this.entries.has(id)) {
|
|
68
|
+
let suffix = 1
|
|
69
|
+
while (this.entries.has(`${id}#${suffix}`)) {
|
|
70
|
+
suffix += 1
|
|
71
|
+
}
|
|
72
|
+
id = `${id}#${suffix}`
|
|
73
|
+
}
|
|
74
|
+
const aliases = [desktopId]
|
|
75
|
+
const generic = this.resolveLocalizedValue(data, 'GenericName')
|
|
76
|
+
if (generic) {
|
|
77
|
+
aliases.push(generic)
|
|
78
|
+
}
|
|
79
|
+
this.addEntry({
|
|
80
|
+
id,
|
|
81
|
+
name,
|
|
82
|
+
aliases,
|
|
83
|
+
kind: 'linux-desktop',
|
|
84
|
+
desktopId,
|
|
85
|
+
execLine,
|
|
86
|
+
path: filePath,
|
|
87
|
+
detail: filePath,
|
|
88
|
+
terminal: String(data.Terminal || '').toLowerCase() === 'true',
|
|
89
|
+
workingDirectory: data.Path || null
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
parseDesktopFile(content) {
|
|
94
|
+
const data = {}
|
|
95
|
+
const lines = content.split(/\r?\n/)
|
|
96
|
+
let inEntry = false
|
|
97
|
+
for (const rawLine of lines) {
|
|
98
|
+
const line = rawLine.trim()
|
|
99
|
+
if (!line || line.startsWith('#')) {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
if (line.startsWith('[')) {
|
|
103
|
+
inEntry = line === '[Desktop Entry]'
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if (!inEntry) {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const idx = line.indexOf('=')
|
|
110
|
+
if (idx === -1) {
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
const key = line.slice(0, idx).trim()
|
|
114
|
+
const value = line.slice(idx + 1).trim()
|
|
115
|
+
if (!(key in data)) {
|
|
116
|
+
data[key] = value
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return data
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
resolveLocalizedValue(data, key) {
|
|
123
|
+
if (!data) {
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
if (data[key]) {
|
|
127
|
+
return data[key]
|
|
128
|
+
}
|
|
129
|
+
const lang = process.env.LANG ? process.env.LANG.split('.')[0] : ''
|
|
130
|
+
if (lang) {
|
|
131
|
+
if (data[`${key}[${lang}]`]) {
|
|
132
|
+
return data[`${key}[${lang}]`]
|
|
133
|
+
}
|
|
134
|
+
const short = lang.split('_')[0]
|
|
135
|
+
if (short && data[`${key}[${short}]`]) {
|
|
136
|
+
return data[`${key}[${short}]`]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const localizedKey = Object.keys(data).find((k) => k.startsWith(`${key}[`))
|
|
140
|
+
if (localizedKey) {
|
|
141
|
+
return data[localizedKey]
|
|
142
|
+
}
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
tokenizeExec(execLine) {
|
|
147
|
+
const tokens = []
|
|
148
|
+
let current = ''
|
|
149
|
+
let quote = null
|
|
150
|
+
let escape = false
|
|
151
|
+
for (const ch of execLine) {
|
|
152
|
+
if (escape) {
|
|
153
|
+
current += ch
|
|
154
|
+
escape = false
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (ch === '\\') {
|
|
158
|
+
escape = true
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
if (ch === '"' || ch === '\'') {
|
|
162
|
+
if (quote === ch) {
|
|
163
|
+
quote = null
|
|
164
|
+
} else if (!quote) {
|
|
165
|
+
quote = ch
|
|
166
|
+
} else {
|
|
167
|
+
current += ch
|
|
168
|
+
}
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (!quote && /\s/.test(ch)) {
|
|
172
|
+
if (current) {
|
|
173
|
+
tokens.push(current)
|
|
174
|
+
current = ''
|
|
175
|
+
}
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
current += ch
|
|
179
|
+
}
|
|
180
|
+
if (current) {
|
|
181
|
+
tokens.push(current)
|
|
182
|
+
}
|
|
183
|
+
return tokens
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
buildExecCommand(execLine, extraArgs) {
|
|
187
|
+
const tokens = this.tokenizeExec(execLine)
|
|
188
|
+
const cleaned = []
|
|
189
|
+
for (const token of tokens) {
|
|
190
|
+
if (!token) {
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
let replaced = token.replace(/%%/g, '%')
|
|
194
|
+
replaced = replaced.replace(/%[fFuUdDnNickvm]/g, '')
|
|
195
|
+
if (!replaced) {
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
cleaned.push(replaced)
|
|
199
|
+
}
|
|
200
|
+
const args = Array.isArray(extraArgs) ? extraArgs : []
|
|
201
|
+
return cleaned.concat(args)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async launch(entry, params = {}) {
|
|
205
|
+
const args = this.sanitizeArgs(params.args)
|
|
206
|
+
const commandParts = this.buildExecCommand(entry.execLine, args)
|
|
207
|
+
if (!commandParts.length) {
|
|
208
|
+
throw new Error(`Unable to determine launch command for ${entry.name}`)
|
|
209
|
+
}
|
|
210
|
+
let command = commandParts[0]
|
|
211
|
+
let finalArgs = commandParts.slice(1)
|
|
212
|
+
const options = {}
|
|
213
|
+
if (entry.workingDirectory) {
|
|
214
|
+
options.cwd = entry.workingDirectory
|
|
215
|
+
}
|
|
216
|
+
const result = await this.spawnDetached(command, finalArgs, options)
|
|
217
|
+
return {
|
|
218
|
+
success: true,
|
|
219
|
+
id: entry.id,
|
|
220
|
+
name: entry.name,
|
|
221
|
+
kind: entry.kind,
|
|
222
|
+
launcher: 'exec',
|
|
223
|
+
pid: result.pid || null,
|
|
224
|
+
detail: entry.execLine
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = LinuxAdapter
|