add support for directly launching windows exes from file exporer using .desktop shortcut

This commit is contained in:
olcxja 2026-09-30 17:32:30 +02:00
commit 5e217472aa
15 changed files with 440 additions and 28 deletions

View file

@ -597,6 +597,114 @@ async function doImport(srcDir, mode) {
}
}
// --- "Open with PLWL" (file manager -> Open With -> PLWL) -----------------------
const openwithTexts = {}
function ot(key, ...args) {
let text = openwithTexts[key] ?? t(key)
args.forEach((v, i) => {
text = String(text).split(`{${i}}`).join(String(v))
})
return text
}
async function loadOpenwithTexts() {
try {
Object.assign(openwithTexts, await window.plwl.openwithTexts(state.lang))
} catch {}
}
function isWindowsExecutable(p) {
return /\.(exe|msi|bat|cmd|lnk)$/i.test(p || '')
}
// The popup: wine version + prefix selects defaulting to the LAUNCHER's
// defaults (settings.defaultWine / prefixes.default), like the per-app
// config's "Default (...)" options - just without importing anything.
async function showOpenWithModal(openPath) {
// always fresh: wines/prefixes/default can change while the launcher sits
// in the tray
try {
const [wines, prefixes, settings] = await Promise.all([
window.plwl.winesList(),
window.plwl.prefixesList(),
window.plwl.settingsGet()
])
state.wines = wines
state.prefixes = prefixes
state.settings = settings
} catch {}
const modal = showModal(`
<h2>${ot('openwith.title')}</h2>
<p class="subtitle openwith-file-row">${ot('openwith.file')}: <code id="ow-file"></code></p>
<p class="subtitle" id="ow-missing" style="display:none">${ot('openwith.fileMissing')}</p>
<label class="field-label">${ot('openwith.wine')}</label>
<span id="ow-wine"></span>
<label class="field-label">${ot('openwith.prefix')}</label>
<span id="ow-prefix"></span>
<div class="modal-actions">
<button class="button" id="ow-cancel">${t('dialog.cancel')}</button>
<button class="button primary" id="ow-run">${ot('openwith.run')}</button>
</div>
`)
modal.querySelector('#ow-file').textContent = openPath
modal.querySelector('#ow-file').title = openPath
const missing = !window.plwl.fileExists(openPath) || !isWindowsExecutable(openPath)
modal.querySelector('#ow-missing').style.display = missing ? '' : 'none'
const defWine = (state.settings && state.settings.defaultWine) || 'system'
const defPrefix = state.prefixes.default || ''
const wineOpts = [
{ value: 'default', label: ot('openwith.wineDefault', wineLabel(defWine)) },
...state.wines.map(w => ({ value: w.id, label: w.label }))
]
const prefixOpts = [
{ value: '', label: ot('openwith.prefixDefault', defPrefix || 'Main') },
...state.prefixes.prefixes.filter(p => p.name !== defPrefix).map(p => ({ value: p.name, label: p.name }))
]
new CustomSelect(modal.querySelector('#ow-wine'), wineOpts, 'default')
new CustomSelect(modal.querySelector('#ow-prefix'), prefixOpts, '')
modal.querySelector('#ow-cancel').onclick = closeModal
modal.querySelector('#ow-run').onclick = async () => {
if (missing) {
closeModal()
return
}
const runBtn = modal.querySelector('#ow-run')
runBtn.disabled = true
const res = await window.plwl.openwithRun({
openPath,
wine: modal.querySelector('#ow-wine').dataset.value || 'default',
prefix: modal.querySelector('#ow-prefix').dataset.value || ''
})
closeModal()
if (res.ok) {
toast(t('toast.started', window.plwl.basename(openPath)), 'success')
} else {
const raw = res.error === 'file-missing' ? ot('openwith.fileMissing') : (res.error === 'not-windows' ? ot('openwith.notWindows') : (res.error || ''))
toast(ot('openwith.startFailed', window.plwl.basename(openPath), raw), 'error')
}
}
}
// Entry point: the main process hands over a file opened through PLWL
// (boot argv or a second-instance handoff).
async function handleOpenWith(openPath) {
if (!openPath) return
await loadOpenwithTexts()
showOpenWithModal(openPath)
}
function wireOpenWith() {
// Both delivery paths (live handoff and the boot queue) converge on
// openwithTake(): the main-process slot is drained exactly when a popup
// shows, so a reloaded window can never resurrect an old popup and two
// quick handoffs collapse into one (the newest file wins).
window.plwl.onOpenWith(() => window.plwl.openwithTake().then(handleOpenWith))
// file queued before the window existed (boot handoff): pick it up once
window.plwl.openwithTake().then(handleOpenWith)
}
// Import from a prefix, step 2: register the app in place - nothing is
// copied, paths stay prefix-relative.
async function doPrefixImport(prefix, srcDir) {
@ -1075,6 +1183,7 @@ async function boot() {
applyI18n()
wireEvents()
wireRealtime()
wireOpenWith()
qs('#loading').remove()
qs('#main').style.display = 'flex'
showView('library')