Skip to content

Commit 7d60e8c

Browse files
committed
Support bundle and mutex app relationships globally
- dynamically detect bundle children from catalog descriptions - hide children but keep parents visible and redirect detail view after install - handle appimage mutex pairs and redirect to surviving app - preserve install feedback during operations - generalized parser improvements
1 parent 38f4cb4 commit 7d60e8c

File tree

3 files changed

+114
-28
lines changed

3 files changed

+114
-28
lines changed

main.js

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ function runSandboxTask(sender, { pm, action, args, stdinScript, appName }) {
261261
}
262262
let settled = false;
263263
let output = '';
264-
const passwordRegex = /mot de passe.*:|password.*:/i;
264+
const passwordRegex = /\[sudo\]|mot de passe.*:|password.*:/i;
265265
const send = (payload) => {
266266
if (!sender) return;
267267
try {
@@ -528,7 +528,7 @@ ipcMain.handle('am-action', async (event, action, software) => {
528528
});
529529
child.onData((txt) => {
530530
output += txt;
531-
if (/mot de passe.*:|password.*:/i.test(txt)) {
531+
if (/\[sudo\]|mot de passe.*:|password.*:/i.test(txt)) {
532532
// Demander le mot de passe au renderer via IPC
533533
if (event.sender) event.sender.send('password-prompt', { id });
534534
}
@@ -613,7 +613,7 @@ ipcMain.handle('install-start', async (event, name) => {
613613
const txt = chunk.toString();
614614
output += txt;
615615
// Détection du prompt mot de passe sudo
616-
if (/mot de passe.*:|password.*:/i.test(txt)) {
616+
if (/\[sudo\]|mot de passe.*:|password.*:/i.test(txt)) {
617617
// Demander le mot de passe au renderer via IPC
618618
wc.send('password-prompt', { id });
619619
// Attendre la réponse avant d'envoyer le mot de passe au process
@@ -770,7 +770,7 @@ ipcMain.handle('updates-start', async (event) => {
770770
child.onData((txt) => {
771771
output += txt;
772772
send({ kind: 'data', chunk: txt });
773-
if (/mot de passe.*:|password.*:/i.test(txt)) {
773+
if (/\[sudo\]|mot de passe.*:|password.*:/i.test(txt)) {
774774
try { wc.send('password-prompt', { id }); }
775775
catch(_) {}
776776
}
@@ -885,39 +885,61 @@ ipcMain.handle('list-apps-detailed', async () => {
885885
/^TOTAL/i,
886886
/^\*has/i
887887
];
888+
// curEntry tracks the current ◆ entry so continuation lines can be
889+
// appended to its description (descriptions can span multiple lines).
890+
let curName = null;
891+
let curDesc = null;
892+
let curInCatalog = false;
893+
const flushEntry = () => {
894+
if (!curName) return;
895+
if (!curInCatalog) {
896+
installedFromCatalog.add(curName);
897+
if (curDesc) installedDesc.set(curName, curDesc);
898+
} else {
899+
catalogSet.add(curName);
900+
if (curDesc) catalogDesc.set(curName, curDesc);
901+
}
902+
diamondSet.add(curName);
903+
curName = null;
904+
curDesc = null;
905+
};
906+
888907
for (const raw of lines) {
889908
const line = raw.trim();
890909
if (line === '') {
891910
// blank line: if we've already scanned at least one ◆ entry and
892911
// we're not yet in the catalog, switch to catalog mode. Subsequent
893912
// ◆ entries will then be catalog items.
894913
if (seenAppEntry && !inCatalog) {
914+
flushEntry();
895915
inCatalog = true;
916+
} else {
917+
flushEntry();
896918
}
897919
continue;
898920
}
899-
if (!line.startsWith('\u25c6')) continue; // ignore non-◆ lines
900-
const rest = line.slice(1).trim();
901-
const colonIdx = rest.indexOf(':');
902-
let desc = null;
903-
let left = rest;
904-
if (colonIdx !== -1) {
905-
left = rest.slice(0, colonIdx).trim();
906-
desc = rest.slice(colonIdx + 1).trim();
907-
if (desc === '') desc = null;
908-
}
909-
const name = left.split(/\s+/)[0].trim();
910-
if (ignoreNamePatterns.some(re => re.test(name))) continue;
911-
if (!inCatalog) {
912-
installedFromCatalog.add(name);
913-
if (desc) installedDesc.set(name, desc);
914-
} else {
915-
catalogSet.add(name);
916-
if (desc) catalogDesc.set(name, desc);
921+
if (line.startsWith('\u25c6')) {
922+
flushEntry();
923+
const rest = line.slice(1).trim();
924+
const colonIdx = rest.indexOf(':');
925+
let left = rest;
926+
let desc = null;
927+
if (colonIdx !== -1) {
928+
left = rest.slice(0, colonIdx).trim();
929+
desc = rest.slice(colonIdx + 1).trim() || null;
930+
}
931+
const name = left.split(/\s+/)[0].trim();
932+
if (ignoreNamePatterns.some(re => re.test(name))) continue;
933+
curName = name;
934+
curDesc = desc;
935+
curInCatalog = inCatalog;
936+
seenAppEntry = true;
937+
} else if (curName && curInCatalog) {
938+
// continuation line: append to current catalog entry description
939+
curDesc = curDesc ? curDesc + ' ' + line : line;
917940
}
918-
diamondSet.add(name);
919-
seenAppEntry = true;
920941
}
942+
flushEntry(); // flush the last entry
921943
} catch (e) {
922944
// ignore parse errors from catalog
923945
}
@@ -982,6 +1004,16 @@ ipcMain.handle('list-apps-detailed', async () => {
9821004
}
9831005
}
9841006

1007+
// Build bundle-child map: apps whose description says
1008+
// "This script installs the full 'X' suite" are children of X.
1009+
// When X is installed the child should be hidden from the catalog.
1010+
const bundleChildOf = {};
1011+
const suitePattern = /installs the full "([^"]+)" suite/i;
1012+
for (const [name, desc] of catalogDesc) {
1013+
const m = suitePattern.exec(desc);
1014+
if (m) bundleChildOf[name] = m[1].toLowerCase();
1015+
}
1016+
9851017
const allSet = new Set([...catalogSet, ...installedSet]);
9861018
const all = Array.from(allSet).map(name => ({
9871019
name,
@@ -997,7 +1029,7 @@ ipcMain.handle('list-apps-detailed', async () => {
9971029
version: installedDesc.get(name) || null,
9981030
desc: catalogDesc.get(name) || null
9991031
}));
1000-
return resolve({ installed, all, pmFound: true });
1032+
return resolve({ installed, all, pmFound: true, bundleChildOf });
10011033
} catch (e) {
10021034
return resolve({ installed: [], all: [], pmFound: true, error: 'Erreur interne lors du parsing.' });
10031035
}

src/renderer/features/search/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@
174174
base = Array.isArray(override.apps) ? override.apps : [];
175175
} else if (activeCategory === 'installed') {
176176
resetSearchModeIfNeeded();
177-
base = filterInstalled(allApps);
177+
base = filterInstalled(allApps); // uses full allApps to include implied apps
178178
} else if (activeCategory !== 'all') {
179179
resetSearchModeIfNeeded();
180180
base = filterByCategory(allApps, activeCategory);

src/renderer/renderer.js

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,8 @@ const state = {
374374
currentDetailsApp: null,
375375
renderVersion: 0,
376376
lastScrollY: 0,
377-
installed: new Set() // ensemble des noms installés (lowercase)
377+
installed: new Set(), // ensemble des noms installés (lowercase)
378+
bundleChildOf: {} // { childName: parentName } – populated after loadApps
378379
};
379380

380381
let virtualListApi = null;
@@ -2220,6 +2221,53 @@ async function loadApps() {
22202221
}
22212222
state.installed = installedNames;
22222223
} catch(_) { state.installed = new Set(); }
2224+
// Dynamic app group filtering:
2225+
// 1. Bundle children: mark as installed when parent suite is installed
2226+
// (e.g. user installs "adb" → "platform-tools" is installed → adb stays
2227+
// visible and is shown as installed, so the user gets clear feedback)
2228+
// 2. Appimage mutex pairs: X-appimage and X share the same binary/config;
2229+
// hide only the uninstalled partner so the installed one stays visible.
2230+
function applyAppGroupFiltering(bundleChildOf) {
2231+
const toRemove = new Set();
2232+
// 1. Bundle children: remove from all views (parent stays visible, installed).
2233+
// After installing a child (adb…) the details panel redirects to the parent.
2234+
for (const app of state.allApps) {
2235+
const name = String(app.name).toLowerCase();
2236+
const parent = (bundleChildOf || {})[name];
2237+
if (parent && state.installed.has(parent.toLowerCase())) {
2238+
state.installed.add(name);
2239+
toRemove.add(name);
2240+
}
2241+
}
2242+
// 2. Appimage mutex pairs (e.g. firefox ↔ firefox-appimage)
2243+
// Also build mutexRedirect: { 'firefox-appimage': 'firefox' } so that
2244+
// post-install can redirect to the surviving app.
2245+
const allNames = new Set(state.allApps.map(a => String(a.name).toLowerCase()));
2246+
const mutexRedirect = {};
2247+
for (const app of state.allApps) {
2248+
const name = String(app.name).toLowerCase();
2249+
if (name.endsWith('-appimage')) {
2250+
const base = name.slice(0, -'-appimage'.length);
2251+
if (allNames.has(base)) {
2252+
if (state.installed.has(base)) {
2253+
toRemove.add(name);
2254+
mutexRedirect[name] = base; // installing firefox-appimage → show firefox
2255+
} else if (state.installed.has(name)) {
2256+
toRemove.add(base);
2257+
mutexRedirect[base] = name; // installing firefox → show firefox-appimage
2258+
}
2259+
}
2260+
}
2261+
}
2262+
state.mutexRedirect = mutexRedirect;
2263+
if (toRemove.size > 0) {
2264+
state.allApps = state.allApps.filter(a => !toRemove.has(String(a.name).toLowerCase()));
2265+
state.filtered = state.filtered.filter(a => !toRemove.has(String(a.name).toLowerCase()));
2266+
}
2267+
}
2268+
state.bundleChildOf = detailed.bundleChildOf || {};
2269+
state.mutexRedirect = {};
2270+
applyAppGroupFiltering(detailed.bundleChildOf);
22232271
if (installedCountEl) installedCountEl.textContent = String(state.allApps.filter(a => a.installed && a.hasDiamond).length);
22242272
cleanupSandboxCache();
22252273
rerenderActiveCategory();
@@ -3453,7 +3501,13 @@ if (window.electronAPI.onInstallProgress){
34533501
// Plus de gestion du log ou du bouton log ici
34543502
loadApps().then(()=> {
34553503
if (msg.success) {
3456-
if (msg.name) showDetails(msg.name); else if (detailsInstallBtn?.getAttribute('data-name')) showDetails(detailsInstallBtn.getAttribute('data-name'));
3504+
// Redirect to the surviving app after install:
3505+
// - bundle child (adb) → parent (platform-tools)
3506+
// - mutex partner (firefox-appimage) → canonical (firefox), or vice-versa
3507+
const installedName = msg.name || detailsInstallBtn?.getAttribute('data-name');
3508+
const key = installedName && installedName.toLowerCase();
3509+
const targetName = (key && (state.bundleChildOf[key] || state.mutexRedirect[key])) || installedName;
3510+
if (targetName) showDetails(targetName);
34573511
}
34583512
if (msg.name) {
34593513
const tile = document.querySelector(`.app-tile[data-app="${CSS.escape(msg.name)}"]`);

0 commit comments

Comments
 (0)