Combine the Omarchy menu and the launcher
Now that we can deep search, they don't need to be different
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
const search = requireFromRoot('shell/services/AppSearch.js')
|
||||
const menuQml = fs.readFileSync(path.join(root, 'shell/plugins/menu/Menu.qml'), 'utf8')
|
||||
const appLibraryQml = fs.readFileSync(path.join(root, 'shell/services/AppLibrary.qml'), 'utf8')
|
||||
|
||||
const entries = [
|
||||
{
|
||||
name: 'Google Contacts',
|
||||
genericName: 'Address Book',
|
||||
comment: 'Manage contacts',
|
||||
keywords: ['contacts', 'address book', 'people'],
|
||||
id: 'google-contacts.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Calculator',
|
||||
genericName: 'Calculator',
|
||||
comment: 'Perform arithmetic, scientific or financial calculations',
|
||||
keywords: ['calculation', 'arithmetic', 'scientific', 'financial'],
|
||||
id: 'org.gnome.Calculator.desktop'
|
||||
},
|
||||
{
|
||||
name: 'OBS Studio',
|
||||
genericName: 'Streaming/Recording Software',
|
||||
comment: 'Free and Open Source Streaming/Recording Software',
|
||||
keywords: ['streaming', 'recording', 'capture'],
|
||||
id: 'com.obsproject.Studio.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Aether',
|
||||
genericName: '',
|
||||
comment: 'Minimal internet radio player',
|
||||
keywords: ['audio', 'music', 'radio'],
|
||||
id: 'io.github.taqi.aether.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Xournal++',
|
||||
genericName: 'Notetaking',
|
||||
comment: 'Take handwritten notes',
|
||||
keywords: ['notes', 'pdf', 'annotation'],
|
||||
id: 'com.github.xournalpp.xournalpp.desktop'
|
||||
},
|
||||
{
|
||||
name: 'RustDesk',
|
||||
genericName: 'Remote Desktop',
|
||||
comment: 'Remote desktop control',
|
||||
keywords: ['remote', 'desktop', 'control'],
|
||||
id: 'com.rustdesk.RustDesk.desktop'
|
||||
}
|
||||
]
|
||||
|
||||
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
|
||||
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
|
||||
|
||||
assert(
|
||||
search.fuzzyScore(entries[1], 'contact') < 0,
|
||||
'calculator does not match contact as a loose subsequence'
|
||||
)
|
||||
|
||||
const acronymMatches = search.sortedEntries(entries, 'gc').map(row => search.entryName(row.entry))
|
||||
assertEqual(acronymMatches[0], 'Google Contacts', 'short acronym matching still works')
|
||||
|
||||
const directMatches = search.sortedEntries(entries, 'obs').map(row => search.entryName(row.entry))
|
||||
assertEqual(directMatches[0], 'OBS Studio', 'direct app-name matching still works')
|
||||
|
||||
// The menu's Apps submenu is the launcher now: app rows launch and uninstall
|
||||
// through the shared app library instead of running commands themselves.
|
||||
const activateMatch = menuQml.match(/function activateIndex\(index, fromPointer\) \{([\s\S]*?)\n \}/)
|
||||
assert(activateMatch, 'menu activateIndex function exists')
|
||||
assert(
|
||||
activateMatch[1].includes('root.appLibrary.launch('),
|
||||
'menu routes app launch through the shared app library'
|
||||
)
|
||||
assert(
|
||||
!activateMatch[1].includes('entry.execute()'),
|
||||
'menu does not execute desktop entries directly'
|
||||
)
|
||||
|
||||
const confirmDeleteMatch = menuQml.match(/function confirmDelete\(\) \{([\s\S]*?)\n \}/)
|
||||
assert(confirmDeleteMatch, 'menu confirmDelete function exists')
|
||||
assert(
|
||||
confirmDeleteMatch[1].includes('root.appLibrary.remove('),
|
||||
'menu delete routes through the shared app library'
|
||||
)
|
||||
assert(
|
||||
confirmDeleteMatch[1].includes('root.cancel()'),
|
||||
'menu delete closes the menu after confirmation'
|
||||
)
|
||||
|
||||
assert(
|
||||
/function remove\(desktopId, name\) \{[\s\S]*?omarchy-remove-launcher-entry[\s\S]*?\n \}/.test(appLibraryQml),
|
||||
'app library remove runs the remover through the shell'
|
||||
)
|
||||
|
||||
assert(
|
||||
/function launch\(desktopId, name\) \{[\s\S]*?gtk-launch[\s\S]*?\n \}/.test(appLibraryQml) &&
|
||||
appLibraryQml.includes('Util.execDetached("gtk-launch "'),
|
||||
'app library runs desktop entry launch through the shell'
|
||||
)
|
||||
|
||||
assert(
|
||||
/function iconIndexScanCommand\(\)[\s\S]*-path "\*\/apps\/\*" -o -path "\*\/devices\/\*"/.test(appLibraryQml),
|
||||
'app library fallback icon index includes device icons'
|
||||
)
|
||||
|
||||
const iconSourceMatch = appLibraryQml.match(/function iconSource\(icon\) \{([\s\S]*?)\n \}/)
|
||||
assert(iconSourceMatch, 'app library iconSource function exists')
|
||||
assert(
|
||||
iconSourceMatch[1].indexOf('root.iconIndex[value]') < iconSourceMatch[1].indexOf('Quickshell.iconPath(value, true)'),
|
||||
'app library prefers indexed app icons over ambiguous themed icons'
|
||||
)
|
||||
|
||||
const openMatch = menuQml.match(/function openExistingMenu\(initialMenu\) \{([\s\S]*?)\n \}/)
|
||||
assert(openMatch, 'menu openExistingMenu function exists')
|
||||
assert(
|
||||
openMatch[1].includes('root.appLibrary.refreshIcons()'),
|
||||
'menu refreshes the shared icon index when opened'
|
||||
)
|
||||
JS
|
||||
@@ -1,145 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
const search = requireFromRoot('shell/plugins/launcher/LauncherSearch.js')
|
||||
const launcherQml = fs.readFileSync(path.join(root, 'shell/plugins/launcher/Launcher.qml'), 'utf8')
|
||||
|
||||
const entries = [
|
||||
{
|
||||
name: 'Google Contacts',
|
||||
genericName: 'Address Book',
|
||||
comment: 'Manage contacts',
|
||||
keywords: ['contacts', 'address book', 'people'],
|
||||
id: 'google-contacts.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Calculator',
|
||||
genericName: 'Calculator',
|
||||
comment: 'Perform arithmetic, scientific or financial calculations',
|
||||
keywords: ['calculation', 'arithmetic', 'scientific', 'financial'],
|
||||
id: 'org.gnome.Calculator.desktop'
|
||||
},
|
||||
{
|
||||
name: 'OBS Studio',
|
||||
genericName: 'Streaming/Recording Software',
|
||||
comment: 'Free and Open Source Streaming/Recording Software',
|
||||
keywords: ['streaming', 'recording', 'capture'],
|
||||
id: 'com.obsproject.Studio.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Aether',
|
||||
genericName: '',
|
||||
comment: 'Minimal internet radio player',
|
||||
keywords: ['audio', 'music', 'radio'],
|
||||
id: 'io.github.taqi.aether.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Xournal++',
|
||||
genericName: 'Notetaking',
|
||||
comment: 'Take handwritten notes',
|
||||
keywords: ['notes', 'pdf', 'annotation'],
|
||||
id: 'com.github.xournalpp.xournalpp.desktop'
|
||||
},
|
||||
{
|
||||
name: 'RustDesk',
|
||||
genericName: 'Remote Desktop',
|
||||
comment: 'Remote desktop control',
|
||||
keywords: ['remote', 'desktop', 'control'],
|
||||
id: 'com.rustdesk.RustDesk.desktop'
|
||||
}
|
||||
]
|
||||
|
||||
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
|
||||
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
|
||||
|
||||
assert(
|
||||
search.fuzzyScore(entries[1], 'contact') < 0,
|
||||
'calculator does not match contact as a loose subsequence'
|
||||
)
|
||||
|
||||
const acronymMatches = search.sortedEntries(entries, 'gc').map(row => search.entryName(row.entry))
|
||||
assertEqual(acronymMatches[0], 'Google Contacts', 'short acronym matching still works')
|
||||
|
||||
const directMatches = search.sortedEntries(entries, 'obs').map(row => search.entryName(row.entry))
|
||||
assertEqual(directMatches[0], 'OBS Studio', 'direct app-name matching still works')
|
||||
|
||||
assert(
|
||||
/function select\(delta\)[\s\S]*root\.disarmHover\(\)[\s\S]*root\.selectedIndex =/.test(launcherQml),
|
||||
'launcher keyboard navigation disarms stale hover before moving selection'
|
||||
)
|
||||
assert(
|
||||
/PointerMoveGate\s*\{[\s\S]*id: pointerGate[\s\S]*referenceItem: card[\s\S]*\}/.test(launcherQml),
|
||||
'launcher uses shared pointer movement gate in card coordinates'
|
||||
)
|
||||
assert(
|
||||
/function disarmHover\(\)[\s\S]*pointerGate\.reset\(\)/.test(launcherQml),
|
||||
'launcher resets pointer movement gate when hover is disarmed'
|
||||
)
|
||||
const openMatch = launcherQml.match(/function open\(payloadJson\) \{([\s\S]*?)\n \}/)
|
||||
assert(openMatch, 'launcher open function exists')
|
||||
assert(
|
||||
openMatch[1].indexOf('root.disarmHover()') < openMatch[1].indexOf('root.opened = true')
|
||||
&& !openMatch[1].includes('pointerGate.allowInitialSample()'),
|
||||
'launcher ignores a stale hidden-pointer position when becoming visible'
|
||||
)
|
||||
assert(
|
||||
/function selectFromPointer\(index, item, mouse\)[\s\S]*pointerGate\.moved\(item, mouse\)[\s\S]*root\.selectedIndex = index/.test(launcherQml),
|
||||
'launcher only selects from pointer after real movement'
|
||||
)
|
||||
assert(
|
||||
/onPositionChanged: function\(mouse\) \{\s*root\.selectFromPointer\(row\.index, row, mouse\)\s*\}/.test(launcherQml),
|
||||
'launcher row hover routes through pointer movement gate'
|
||||
)
|
||||
assert(
|
||||
/onEntered: root\.selectFromPointer\(row\.index, row, \{\s*x: mouseArea\.mouseX,\s*y: mouseArea\.mouseY\s*\}\)/.test(launcherQml),
|
||||
'launcher samples pointer movement immediately when entering a row'
|
||||
)
|
||||
assert(
|
||||
!/onContainsMouseChanged:[\s\S]*root\.selectedIndex/.test(launcherQml),
|
||||
'launcher does not select rows from containsMouse'
|
||||
)
|
||||
|
||||
const confirmDeleteMatch = launcherQml.match(/function confirmDelete\(\) \{([\s\S]*?)\n \}/)
|
||||
assert(confirmDeleteMatch, 'launcher confirmDelete function exists')
|
||||
assert(
|
||||
confirmDeleteMatch[1].includes('root.dismiss()'),
|
||||
'launcher delete closes launcher after confirmation'
|
||||
)
|
||||
assert(
|
||||
confirmDeleteMatch[1].includes('Util.execDetached(command)'),
|
||||
'launcher delete runs remover through the shell'
|
||||
)
|
||||
|
||||
const activateMatch = launcherQml.match(/function activateIndex\(index\) \{([\s\S]*?)\n \}/)
|
||||
assert(activateMatch, 'launcher activateIndex function exists')
|
||||
assert(
|
||||
!activateMatch[1].includes('entry.execute()'),
|
||||
'launcher does not execute desktop entries directly'
|
||||
)
|
||||
assert(
|
||||
activateMatch[1].includes('gtk-launch') && activateMatch[1].includes('Util.execDetached'),
|
||||
'launcher runs desktop entry launch through the shell'
|
||||
)
|
||||
|
||||
assert(
|
||||
/function iconIndexScanCommand\(\)[\s\S]*-path "\*\/apps\/\*" -o -path "\*\/devices\/\*"/.test(launcherQml),
|
||||
'launcher fallback icon index includes device icons'
|
||||
)
|
||||
|
||||
const iconSourceMatch = launcherQml.match(/function iconSource\(icon\) \{([\s\S]*?)\n \}/)
|
||||
assert(iconSourceMatch, 'launcher iconSource function exists')
|
||||
assert(
|
||||
iconSourceMatch[1].indexOf('root.iconIndex[value]') < iconSourceMatch[1].indexOf('Quickshell.iconPath(value, true)'),
|
||||
'launcher prefers indexed app icons over ambiguous themed icons'
|
||||
)
|
||||
|
||||
assert(
|
||||
openMatch[1].includes('if (!iconIndexScan.running) iconIndexScan.running = true'),
|
||||
'launcher refreshes its icon index when opened'
|
||||
)
|
||||
JS
|
||||
@@ -92,6 +92,8 @@ assertDeepEqual(
|
||||
kind: 'action',
|
||||
icon: '',
|
||||
iconFont: '',
|
||||
appIcon: '',
|
||||
appId: '',
|
||||
label: 'Theme picker',
|
||||
target: 'style.theme',
|
||||
detail: 'Style',
|
||||
|
||||
@@ -5,7 +5,6 @@ run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
|
||||
const menuQml = fs.readFileSync(path.join(root, 'shell/plugins/menu/Menu.qml'), 'utf8')
|
||||
const launcherQml = fs.readFileSync(path.join(root, 'shell/plugins/launcher/Launcher.qml'), 'utf8')
|
||||
|
||||
assert(
|
||||
/rowReservedBorderLeft:\s*Border\.left\(selectedBorderSpec\)/.test(menuQml)
|
||||
@@ -18,14 +17,4 @@ assert(
|
||||
'Menu row content does not depend on current selected border state'
|
||||
)
|
||||
|
||||
assert(
|
||||
/rowReservedBorderLeft:\s*Border\.left\(selectedBorderSpec\)/.test(launcherQml)
|
||||
&& /rowReservedBorderRight:\s*Border\.right\(selectedBorderSpec\)/.test(launcherQml),
|
||||
'Launcher rows reserve selected border insets'
|
||||
)
|
||||
|
||||
assert(
|
||||
!/anchors\.(left|right)Margin:[^\n]*\brow\.border(Left|Right)\b/.test(launcherQml),
|
||||
'Launcher row content does not depend on current selected border state'
|
||||
)
|
||||
JS
|
||||
|
||||
@@ -127,8 +127,8 @@ jq -e '
|
||||
}
|
||||
pass "shell IPC returns effective shell config"
|
||||
|
||||
[[ $(shell_ipc shell summon omarchy.launcher '{"query":"term"}') == "ok" ]] || fail_with_log "shell IPC summons launcher overlay"
|
||||
shell_ipc_quiet shell hide omarchy.launcher >/dev/null
|
||||
[[ $(shell_ipc shell summon omarchy.menu '{"menu":"apps"}') == "ok" ]] || fail_with_log "shell IPC summons menu apps overlay"
|
||||
shell_ipc_quiet shell hide omarchy.menu >/dev/null
|
||||
[[ $(shell_ipc shell summon missing.plugin "{}") == "unknown" ]] || fail_with_log "shell IPC rejects unknown plugin"
|
||||
pass "shell IPC summon and hide contract works"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user