This commit is contained in:
2026-05-28 21:45:21 +02:00
parent 653439ac24
commit a356a3d369
610 changed files with 205679 additions and 0 deletions
@@ -0,0 +1,654 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.System
Item {
id: root
property var launcher: null
property string name: I18n.tr("launcher.providers.applications")
property bool handleSearch: true
property var entries: []
property string supportedLayouts: "both"
property bool isDefaultProvider: true // This provider handles empty search
property bool ignoreDensity: false // Apps should scale with launcher density
property bool trackUsage: true // Track usage frequency for "most used" sorting
// Category support
property string selectedCategory: "all"
property bool showsCategories: true // Default to showing categories
property var categories: ["all", "Pinned", "AudioVideo", "Chat", "Development", "Education", "Game", "Graphics", "Network", "Office", "System", "Misc", "WebBrowser"]
property var availableCategories: ["all"] // Reactive property for available categories
property var categoryIcons: ({
"all": "apps",
"Pinned": "pin",
"AudioVideo": "music",
"Chat": "message-circle",
"Development": "code",
"Education": "school" // Includes Science
,
"Game": "device-gamepad",
"Graphics": "brush",
"Network": "wifi",
"Office": "file-text",
"System": "device-desktop" // Includes Settings and Utility
,
"Misc": "dots",
"WebBrowser": "world"
})
function getCategoryName(category) {
const names = {
"all": I18n.tr("launcher.categories.all"),
"Pinned": I18n.tr("launcher.categories.pinned"),
"AudioVideo": I18n.tr("launcher.categories.audiovideo"),
"Chat": I18n.tr("launcher.categories.chat"),
"Development": I18n.tr("launcher.categories.development"),
"Education": I18n.tr("launcher.categories.education"),
"Game": I18n.tr("launcher.categories.game"),
"Graphics": I18n.tr("launcher.categories.graphics"),
"Network": I18n.tr("common.network"),
"Office": I18n.tr("launcher.categories.office"),
"System": I18n.tr("launcher.categories.system"),
"Misc": I18n.tr("launcher.categories.misc"),
"WebBrowser": I18n.tr("launcher.categories.webbrowser")
};
return names[category] || category;
}
function init() {
loadApplications();
migrateLegacyUsageKeys();
}
function onOpened() {
// Just update available categories in case pinned apps changed
updateAvailableCategories();
// Default to Pinned if there are pinned apps, otherwise all
if (availableCategories.includes("Pinned")) {
selectedCategory = "Pinned";
} else {
selectedCategory = "all";
}
// Set category mode initially (will be updated when getResults is called)
showsCategories = true;
}
// Reload applications when desktop entries change on disk
Connections {
target: typeof DesktopEntries !== 'undefined' ? DesktopEntries.applications : null
function onValuesChanged() {
Logger.d("ApplicationsProvider", "Desktop entries changed, reloading applications");
loadApplications();
}
}
function selectCategory(category) {
selectedCategory = category;
if (launcher) {
launcher.updateResults();
}
}
function getAppCategories(app) {
if (!app)
return [];
const result = [];
if (app.categories) {
if (Array.isArray(app.categories)) {
for (let cat of app.categories) {
if (cat && cat.trim && cat.trim() !== '') {
result.push(cat.trim());
} else if (cat && typeof cat === 'string' && cat.trim() !== '') {
result.push(cat.trim());
}
}
} else if (typeof app.categories === 'string') {
const cats = app.categories.split(';').filter(c => c && c.trim() !== '');
for (let cat of cats) {
const trimmed = cat.trim();
if (trimmed && !result.includes(trimmed)) {
result.push(trimmed);
}
}
} else if (app.categories.length !== undefined) {
try {
for (let i = 0; i < app.categories.length; i++) {
const cat = app.categories[i];
if (cat && cat.trim && typeof cat.trim === 'function' && cat.trim() !== '') {
result.push(cat.trim());
} else if (cat && typeof cat === 'string' && cat.trim() !== '') {
result.push(cat.trim());
}
}
} catch (e) {}
}
}
if (app.Categories) {
const cats = app.Categories.split(';').filter(c => c && c.trim() !== '');
for (let cat of cats) {
const trimmed = cat.trim();
if (trimmed && !result.includes(trimmed)) {
result.push(trimmed);
}
}
}
return result;
}
function getAppCategory(app) {
const appCategories = getAppCategories(app);
if (appCategories.length === 0)
return null;
const priorityCategories = ["AudioVideo", "Chat", "WebBrowser", "Game", "Development", "Graphics", "Office", "Education", "System", "Network", "Misc"];
for (let cat of appCategories) {
if (cat === "AudioVideo" || cat === "Audio" || cat === "Video") {
return "AudioVideo";
}
}
if (appCategories.includes("Chat") || appCategories.includes("InstantMessaging")) {
return "Chat";
}
if (appCategories.includes("WebBrowser")) {
return "WebBrowser";
}
// Map Science to Education
if (appCategories.includes("Science")) {
return "Education";
}
// Map Settings to System
if (appCategories.includes("Settings")) {
return "System";
}
// Map Utility to System
if (appCategories.includes("Utility")) {
return "System";
}
for (let priorityCat of priorityCategories) {
if (appCategories.includes(priorityCat) && root.categories.includes(priorityCat)) {
return priorityCat;
}
}
return "Misc";
}
// Helper function to normalize app IDs for case-insensitive matching
function normalizeAppId(appId) {
if (!appId || typeof appId !== 'string')
return "";
return appId.toLowerCase().trim();
}
// Helper function to check if an app is pinned
function isAppPinned(app) {
if (!app)
return false;
const pinnedApps = Settings.data.appLauncher.pinnedApps || [];
const appId = getAppKey(app);
const normalizedId = normalizeAppId(appId);
return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId);
}
function appMatchesCategory(app, category) {
// Check if app matches the selected category
if (category === "all")
return true;
// Handle Pinned category separately
if (category === "Pinned") {
return isAppPinned(app);
}
// Get the primary category for this app (first matching standard category)
const primaryCategory = getAppCategory(app);
// If app has no matching standard category, don't show it in any category (only in "all")
if (!primaryCategory)
return false;
// Map Audio/Video to AudioVideo
if (category === "AudioVideo") {
const appCategories = getAppCategories(app);
// Show if app has AudioVideo, Audio, or Video
return appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video");
}
// Map Science to Education
if (category === "Education") {
const appCategories = getAppCategories(app);
return appCategories.includes("Education") || appCategories.includes("Science");
}
// Map Settings and Utility to System
if (category === "System") {
const appCategories = getAppCategories(app);
return appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility");
}
// Only show app in its primary category to avoid overlap
// This ensures each app appears in exactly one category tab
return category === primaryCategory;
}
function getAvailableCategories() {
const categorySet = new Set();
let hasAudioVideo = false;
let hasEducation = false;
let hasSystem = false;
let hasPinned = false;
// Check if there are any pinned apps
const pinnedApps = Settings.data.appLauncher.pinnedApps || [];
if (pinnedApps.length > 0) {
// Verify that at least one pinned app exists in entries
for (let app of entries) {
if (isAppPinned(app)) {
hasPinned = true;
break;
}
}
}
for (let app of entries) {
const appCategories = getAppCategories(app);
const primaryCategory = getAppCategory(app);
if (appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video")) {
hasAudioVideo = true;
} else if (appCategories.includes("Education") || appCategories.includes("Science")) {
hasEducation = true;
} else if (appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility")) {
hasSystem = true;
} else if (primaryCategory && root.categories.includes(primaryCategory)) {
categorySet.add(primaryCategory);
}
}
const result = [];
// Add Pinned category first if there are pinned apps
if (hasPinned) {
result.push("Pinned");
}
result.push("all");
if (hasAudioVideo) {
categorySet.add("AudioVideo");
}
if (hasEducation) {
categorySet.add("Education");
}
if (hasSystem) {
categorySet.add("System");
}
for (let cat of root.categories) {
if (cat !== "all" && cat !== "Pinned" && cat !== "Misc" && categorySet.has(cat)) {
result.push(cat);
}
}
if (categorySet.has("Misc")) {
result.push("Misc");
}
if (result.length === 1) {
const fallback = root.categories.filter(c => c !== "Misc");
fallback.push("Misc");
return fallback;
}
return result;
}
function loadApplications() {
if (typeof DesktopEntries === 'undefined') {
Logger.w("ApplicationsProvider", "DesktopEntries service not available");
return;
}
const allApps = DesktopEntries.applications.values || [];
const seen = new Map(); // Map of appId -> exec command
entries = allApps.filter(app => {
if (!app || app.noDisplay || app.hidden)
return false;
const appId = app.id || app.name;
const execCmd = getExecutableName(app);
// Check if we've seen this app ID before
if (seen.has(appId)) {
const previousExec = seen.get(appId);
// If exec is different, it's a legitimate different entry - keep it
if (previousExec !== execCmd) {
Logger.d("ApplicationsProvider", `Keeping variant of ${appId}: ${execCmd} (differs from ${previousExec})`);
// Add with modified ID to make it unique
app.id = `${appId}_${execCmd}`;
seen.set(app.id, execCmd);
return true;
}
// Same appId AND same exec = true duplicate, skip it
Logger.d("ApplicationsProvider", `Skipping duplicate: ${appId}`);
return false;
}
seen.set(appId, execCmd);
return true;
}).map(app => {
app.executableName = getExecutableName(app);
return app;
});
Logger.d("ApplicationsProvider", `Loaded ${entries.length} applications`);
updateAvailableCategories();
}
function updateAvailableCategories() {
availableCategories = getAvailableCategories();
}
Connections {
target: Settings.data.appLauncher
function onPinnedAppsChanged() {
const wasViewingPinned = selectedCategory === "Pinned";
updateAvailableCategories();
// If we were viewing Pinned category and it's no longer available, switch to "all"
if (wasViewingPinned && !availableCategories.includes("Pinned")) {
selectedCategory = "all";
}
// Update results if we're currently viewing the Pinned category
if (selectedCategory === "Pinned" && launcher) {
launcher.updateResults();
} else if (wasViewingPinned && selectedCategory === "all" && launcher) {
// Also update results when switching to "all"
launcher.updateResults();
}
}
}
function getExecutableName(app) {
if (!app)
return "";
// Try to get executable name from command array
if (app.command && Array.isArray(app.command) && app.command.length > 0) {
const cmd = app.command[0];
// Extract just the executable name from the full path
const parts = cmd.split('/');
const executable = parts[parts.length - 1];
// Remove any arguments or parameters
return executable.split(' ')[0];
}
// Try to get from exec property if available
if (app.exec) {
const parts = app.exec.split('/');
const executable = parts[parts.length - 1];
return executable.split(' ')[0];
}
// Fallback to app id (desktop file name without .desktop)
if (app.id) {
return app.id.replace('.desktop', '');
}
return "";
}
function getResults(query) {
if (!entries || entries.length === 0)
return [];
// Set category mode based on whether there's a query
const isSearching = !!(query && query.trim() !== "");
showsCategories = !isSearching;
// Filter by category only when NOT searching
let filteredEntries = entries;
if (!isSearching && selectedCategory && selectedCategory !== "all") {
filteredEntries = entries.filter(app => appMatchesCategory(app, selectedCategory));
}
if (!query || query.trim() === "") {
// Return filtered apps, optionally sorted by usage
let sorted;
if (Settings.data.appLauncher.sortByMostUsed) {
sorted = filteredEntries.slice().sort((a, b) => {
// Pinned first
const aPinned = isAppPinned(a);
const bPinned = isAppPinned(b);
if (aPinned !== bPinned)
return aPinned ? -1 : 1;
const ua = getUsageCount(a);
const ub = getUsageCount(b);
if (ub !== ua)
return ub - ua;
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
});
} else {
sorted = filteredEntries.slice().sort((a, b) => {
const aPinned = isAppPinned(a);
const bPinned = isAppPinned(b);
if (aPinned !== bPinned)
return aPinned ? -1 : 1;
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
});
}
return sorted.map(app => createResultEntry(app));
}
// Use fuzzy search if available, fallback to simple search
if (typeof FuzzySort !== 'undefined') {
const fuzzyResults = FuzzySort.go(query, filteredEntries, {
"keys": ["name", "comment", "genericName", "executableName"],
"limit": 20
});
// Sort pinned first within fuzzy results while preserving fuzzysort order otherwise
const pinned = [];
const nonPinned = [];
for (const r of fuzzyResults) {
const app = r.obj;
if (isAppPinned(app))
pinned.push(r);
else
nonPinned.push(r);
}
return pinned.concat(nonPinned).map(result => createResultEntry(result.obj, result.score));
} else {
// Fallback to simple search
const searchTerm = query.toLowerCase();
return filteredEntries.filter(app => {
const name = (app.name || "").toLowerCase();
const comment = (app.comment || "").toLowerCase();
const generic = (app.genericName || "").toLowerCase();
const executable = getExecutableName(app).toLowerCase();
return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) || executable.includes(searchTerm);
}).sort((a, b) => {
// Prioritize name matches, then executable matches
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
const aExecutable = getExecutableName(a).toLowerCase();
const bExecutable = getExecutableName(b).toLowerCase();
const aStarts = aName.startsWith(searchTerm);
const bStarts = bName.startsWith(searchTerm);
const aExecStarts = aExecutable.startsWith(searchTerm);
const bExecStarts = bExecutable.startsWith(searchTerm);
// Prioritize name matches first
if (aStarts && !bStarts)
return -1;
if (!aStarts && bStarts)
return 1;
// Then prioritize executable matches
if (aExecStarts && !bExecStarts)
return -1;
if (!aExecStarts && bExecStarts)
return 1;
return aName.localeCompare(bName);
}).slice(0, 20).map(app => createResultEntry(app));
}
}
function createResultEntry(app, score) {
return {
"appId": getAppKey(app),
"usageKey": getAppKey(app),
"name": app.name || "Unknown",
"description": app.genericName || app.comment || "",
"icon": app.icon || "application-x-executable",
"isImage": false,
"_score": (score !== undefined ? score : 0),
"provider": root,
"onActivate": function () {
// Close the launcher/SmartPanel immediately without any animations.
// Ensures we are not preventing the future focusing of the app
launcher.closeImmediately();
// Defer execution to next event loop iteration to ensure panel is fully closed
Qt.callLater(() => {
Logger.d("ApplicationsProvider", `Launching: ${app.name} (App ID: ${app.id || "unknown"})`);
const execString = (app.exec !== undefined && app.exec !== null) ? String(app.exec) : "";
const commandArgs = Array.isArray(app.command) ? app.command : (app.command && app.command.length !== undefined) ? Array.from(app.command) : [];
let hasQuotedArgs = execString.includes("\"") || execString.includes("'");
let hasSpaceArgs = false;
if (!hasQuotedArgs) {
hasQuotedArgs = commandArgs.some(arg => {
const text = String(arg);
return text.includes("\"") || text.includes("'");
});
}
if (!hasSpaceArgs) {
hasSpaceArgs = commandArgs.some(arg => String(arg).includes(" "));
}
if (app.execute && (hasQuotedArgs || hasSpaceArgs)) {
Logger.d("ApplicationsProvider", `Detected quoted/space arguments in Exec for ${app.name}, using app.execute()`);
app.execute();
return;
}
if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix) {
// Use custom launch prefix
const prefix = Settings.data.appLauncher.customLaunchPrefix.split(" ");
Logger.d("ApplicationsProvider", `Using custom launch prefix: ${Settings.data.appLauncher.customLaunchPrefix}`);
if (app.runInTerminal) {
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
const command = prefix.concat(terminal.concat(app.command));
Logger.d("ApplicationsProvider", `Executing command (with prefix and terminal): ${command.join(" ")}`);
Quickshell.execDetached(command);
} else {
const command = prefix.concat(app.command);
Logger.d("ApplicationsProvider", `Executing command (with prefix): ${command.join(" ")}`);
Quickshell.execDetached(command);
}
} else {
if (app.runInTerminal) {
Logger.d("ApplicationsProvider", "Executing terminal app manually: " + app.name);
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
const command = terminal.concat(app.command);
Logger.d("ApplicationsProvider", "Executing command (manual terminal): " + command.join(" "));
CompositorService.spawn(command);
} else if (app.command && app.command.length > 0) {
Logger.d("ApplicationsProvider", "Executing command: " + app.command.join(" "));
CompositorService.spawn(app.command);
} else if (app.execute) {
Logger.d("ApplicationsProvider", "Calling app.execute() for: " + app.name);
app.execute();
} else {
Logger.w("ApplicationsProvider", `Could not launch: ${app.name}. No valid launch method.`);
}
}
});
}
};
}
// -------------------------
// Item actions for launcher delegate
function getItemActions(item) {
if (!item || !item.appId)
return [];
return [
{
"icon": isAppPinned({
"id": item.appId
}) ? "unpin" : "pin",
"tooltip": isAppPinned({
"id": item.appId
}) ? I18n.tr("common.unpin") : I18n.tr("common.pin"),
"action": function () {
togglePin(item.appId);
}
}
];
}
function togglePin(appId) {
if (!appId)
return;
const normalizedId = normalizeAppId(appId);
let arr = (Settings.data.appLauncher.pinnedApps || []).slice();
const idx = arr.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId);
if (idx >= 0)
arr.splice(idx, 1);
else
arr.push(appId);
Settings.data.appLauncher.pinnedApps = arr;
}
// -------------------------
// Usage tracking helpers
function getAppKey(app) {
if (app && app.id)
return String(app.id);
if (app && app.command && app.command.join)
return app.command.join(" ");
return String(app && app.name ? app.name : "unknown");
}
function getUsageCount(app) {
return ShellState.getLauncherUsageCount(getAppKey(app));
}
// Migrate legacy command-based usage keys to canonical app-id keys at startup
function migrateLegacyUsageKeys() {
for (let i = 0; i < entries.length; i++) {
const app = entries[i];
if (app && app.id && app.command && app.command.join) {
const key = getAppKey(app);
const legacyKey = app.command.join(" ");
if (legacyKey !== key && ShellState.getLauncherUsageCount(legacyKey) > 0) {
ShellState.migrateLauncherUsage(legacyKey, key);
Logger.d("ApplicationsProvider", `Migrated usage: "${legacyKey}" "${key}"`);
}
}
}
}
}
@@ -0,0 +1,77 @@
import QtQuick
import Quickshell
import "../../../../Helpers/AdvancedMath.js" as AdvancedMath
import qs.Commons
import qs.Services.Keyboard
import qs.Services.UI
Item {
id: root
// Provider metadata
property string name: I18n.tr("launcher.providers.calculator")
property var launcher: null
property string iconMode: Settings.data.appLauncher.iconMode
property bool handleSearch: true // Contribute to regular search
property string supportedLayouts: "list"
// Initialize provider
function init() {
Logger.d("CalculatorProvider", "Initialized");
}
// Get search results - evaluates math expressions inline
function getResults(query) {
if (!query)
return [];
const trimmed = query.trim();
if (!trimmed || !isMathExpression(trimmed))
return [];
try {
const result = AdvancedMath.evaluate(trimmed);
const formattedResult = AdvancedMath.formatResult(result);
return [
{
"name": formattedResult,
"description": I18n.tr("launcher.providers.calculator-press-enter-to-copy"),
"icon": iconMode === "tabler" ? "calculator" : "accessories-calculator",
"isTablerIcon": true,
"isImage": false,
"provider": root,
"onActivate": function () {
// Copy result to clipboard via xclip
Quickshell.execDetached(["sh", "-c", "echo -n '" + formattedResult.replace(/'/g, "'\\''") + "' | wl-copy"]);
ToastService.showNotice(I18n.tr("common.copied-to-clipboard"), formattedResult);
if (launcher)
launcher.close();
}
}
];
} catch (error) {
return [];
}
}
// Check if a string is a valid math expression
function isMathExpression(expr) {
// Allow: digits, operators, parentheses, decimal points, whitespace, letters (for functions), commas
if (!/^[\d\s\+\-\*\/\(\)\.\%\^a-zA-Z,]+$/.test(expr))
return false;
// Must contain at least one operator OR a function call (letter followed by parenthesis)
if (!/[+\-*/%\^]/.test(expr) && !/[a-zA-Z]\s*\(/.test(expr))
return false;
// Reject if ends with an operator (incomplete expression)
if (/[+\-*/%\^]\s*$/.test(expr))
return false;
// Reject if it's just letters (would match app names)
if (/^[a-zA-Z\s]+$/.test(expr))
return false;
return true;
}
}
@@ -0,0 +1,457 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services.Keyboard
import qs.Services.Noctalia
Item {
id: root
// Provider metadata
property string name: I18n.tr("launcher.providers.clipboard")
property var launcher: null
property string iconMode: Settings.data.appLauncher.iconMode
property string supportedLayouts: "list" // List view for clipboard content
property bool wrapNavigation: false // Don't wrap at end of list
// Provider capabilities
property bool handleSearch: false // Don't handle regular search
// Preview support
property bool hasPreview: Settings.data.appLauncher.enableClipPreview
property string previewComponentPath: "./ClipboardPreview.qml"
// Image handling - expose revision for reactive updates in delegates
readonly property int imageRevision: ClipboardService.revision
// Categories
property var availableCategories: Settings.data.appLauncher.enableClipboardChips ? ["All", "Images", "Links", "Files", "Code", "Colors"] : []
property string selectedCategory: "All"
function selectCategory(cat) {
if (selectedCategory !== cat) {
selectedCategory = cat;
if (launcher) {
launcher.updateResults();
}
}
}
property var categoryIcons: {
"All": iconMode === "tabler" ? "border-all" : "view-grid",
"Images": iconMode === "tabler" ? "photo" : "image",
"Links": iconMode === "tabler" ? "link" : "insert-link",
"Files": iconMode === "tabler" ? "file" : "text-x-generic",
"Code": iconMode === "tabler" ? "code" : "text-x-script",
"Colors": iconMode === "tabler" ? "palette" : "color-picker"
}
// Internal state
property bool isWaitingForData: false
property bool gotResults: false
property string lastSearchText: ""
// Listen for clipboard data updates
Connections {
target: ClipboardService
function onListCompleted() {
if (gotResults && (lastSearchText === searchText)) {
// Do not update results after the first fetch.
// This will avoid the list resetting every 2seconds when the service updates.
return;
}
// Refresh results if we're waiting for data or if clipboard plugin is active
if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) {
isWaitingForData = false;
gotResults = true;
if (launcher) {
launcher.updateResults();
}
}
}
function onActiveChanged() {
// When active state changes (e.g. dependency check completes), refresh results
if (ClipboardService.active && launcher && launcher.searchText.startsWith(">clip")) {
isWaitingForData = true;
gotResults = false;
ClipboardService.list(100);
}
}
}
// Initialize provider
function init() {
Logger.d("ClipboardProvider", "Initialized");
// Pre-load clipboard data if service is active
if (ClipboardService.active) {
ClipboardService.list(100);
}
}
// Called when launcher opens
function onOpened() {
isWaitingForData = true;
gotResults = false;
lastSearchText = "";
// Refresh clipboard history when launcher opens
if (ClipboardService.active) {
ClipboardService.list(100);
}
}
// Check if this provider handles the command
function handleCommand(searchText) {
return searchText.startsWith(">clip");
}
// Return available commands when user types ">"
function commands() {
return [
{
"name": ">clip",
"description": I18n.tr("launcher.providers.clipboard-search-description"),
"icon": iconMode === "tabler" ? "clipboard" : "diodon",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {
launcher.setSearchText(">clip ");
}
},
{
"name": ">clip clear",
"description": I18n.tr("launcher.providers.clipboard-clear-description"),
"icon": iconMode === "tabler" ? "trash" : "user-trash",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {
ClipboardService.wipeAll();
launcher.close();
}
}
];
}
// Get search results
function getResults(searchText) {
if (!searchText.startsWith(">clip")) {
return [];
}
lastSearchText = searchText;
const results = [];
const query = searchText.slice(5).trim();
// Check if clipboard service is not active
if (!ClipboardService.active) {
// If dependency check hasn't completed yet, show loading instead of disabled
if (!ClipboardService.dependencyChecked) {
return [
{
"name": I18n.tr("launcher.providers.clipboard-loading"),
"description": I18n.tr("launcher.providers.emoji-loading-description"),
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {}
}
];
}
return [
{
"name": I18n.tr("launcher.providers.clipboard-history-disabled"),
"description": I18n.tr("launcher.providers.clipboard-history-disabled-description"),
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {}
}
];
}
// Special command: clear
if (query === "clear") {
return [
{
"name": I18n.tr("launcher.providers.clipboard-clear-history"),
"description": I18n.tr("launcher.providers.clipboard-clear-description-full"),
"icon": iconMode === "tabler" ? "trash" : "user-trash",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {
ClipboardService.wipeAll();
launcher.close();
}
}
];
}
// Show loading state if data is being loaded
if (ClipboardService.loading || isWaitingForData) {
return [
{
"name": I18n.tr("launcher.providers.clipboard-loading"),
"description": I18n.tr("launcher.providers.emoji-loading-description"),
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {}
}
];
}
// Get clipboard items
const items = ClipboardService.items || [];
// If no items and we haven't tried loading yet, trigger a load
if (items.count === 0 && !ClipboardService.loading) {
isWaitingForData = true;
ClipboardService.list(100);
return [
{
"name": I18n.tr("launcher.providers.clipboard-loading"),
"description": I18n.tr("launcher.providers.emoji-loading-description"),
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {}
}
];
}
// Search clipboard items
const searchTerm = query.toLowerCase();
const now = Date.now() / 1000;
const catMap = {
"Images": "image",
"Links": "link",
"Files": "file",
"Code": "code",
"Colors": "color"
};
// Filter and format results
items.forEach(function (item) {
// Category filter
if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") {
if (item.contentType !== catMap[root.selectedCategory]) {
return;
}
}
const preview = (item.preview || "").toLowerCase();
// Skip if search term doesn't match
if (searchTerm && preview.indexOf(searchTerm) === -1) {
return;
}
const firstSeen = ClipboardService.firstSeenById[item.id] || now;
// Format the result based on type
let entry;
if (item.isImage) {
entry = formatImageEntry(item, firstSeen);
} else {
entry = formatTextEntry(item, firstSeen);
}
// Add activation handler
entry.onActivate = function () {
if (Settings.data.appLauncher.autoPasteClipboard) {
launcher.closeImmediately();
Qt.callLater(() => {
ClipboardService.pasteFromClipboard(item.id, item.mime);
});
} else {
ClipboardService.copyToClipboard(item.id);
launcher.close();
}
};
results.push(entry);
});
// Show empty state if no results
if (results.length === 0) {
results.push({
"name": searchTerm ? "No matching clipboard items" : "Clipboard is empty",
"description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here",
"icon": iconMode === "tabler" ? "clipboard" : "text-x-generic",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {// Do nothing
}
});
}
//Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`)
return results;
}
function formatImageEntry(item, firstSeen) {
const meta = ClipboardService.parseImageMeta(item.preview);
const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000));
let desc = meta ? `${meta.fmt} ${meta.size}` : item.mime || "Image data";
if (timeStr)
desc += ` ${timeStr}`;
return {
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
"description": desc,
"icon": iconMode === "tabler" ? "photo" : "image",
"isTablerIcon": true,
"isImage": true,
"imageWidth": meta ? meta.w : 0,
"imageHeight": meta ? meta.h : 0,
"clipboardId": item.id,
"mime": item.mime,
"preview": item.preview,
"provider": root
};
}
function formatTextEntry(item, firstSeen) {
const preview = (item.preview || "").trim();
const lines = preview.split('\n').filter(l => l.trim());
let title = lines[0] || "Empty text";
if (title.length > 60) {
title = title.substring(0, 57) + "...";
}
let description = "";
if (lines.length > 1) {
description = lines[1];
if (description.length > 80) {
description = description.substring(0, 77) + "...";
}
} else {
// Preview is truncated at ~100 chars, so we can't show exact count
if (preview.length >= 100) {
description = I18n.tr("toast.clipboard.long-text");
} else {
const chars = preview.length;
const words = preview.split(/\s+/).length;
description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`;
}
}
const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000));
if (timeStr)
description += ` ${timeStr}`;
let defaultIcon = iconMode === "tabler" ? "clipboard" : "text-x-generic";
let colorHex = "";
if (Settings.data.appLauncher.enableClipboardSmartIcons) {
if (item.contentType === "link")
defaultIcon = iconMode === "tabler" ? "link" : "insert-link";
else if (item.contentType === "file")
defaultIcon = iconMode === "tabler" ? "file" : "text-x-generic";
else if (item.contentType === "code")
defaultIcon = iconMode === "tabler" ? "code" : "text-x-script";
else if (item.contentType === "color") {
defaultIcon = iconMode === "tabler" ? "palette" : "color-picker";
colorHex = preview;
}
}
return {
"name": title,
"description": description,
"icon": defaultIcon,
"isTablerIcon": true,
"isImage": false,
"clipboardId": item.id,
"preview": preview,
"contentType": item.contentType,
"colorHex": colorHex,
"provider": root
};
}
function getImageForItem(clipboardId) {
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null;
}
// -------------------------
// Item actions for launcher delegate
function getItemActions(item) {
if (!item || !item.clipboardId)
return [];
var actions = [];
// Annotation tool for images
if (item.isImage && Settings.data.appLauncher.screenshotAnnotationTool !== "") {
actions.push({
"icon": "pencil",
"tooltip": I18n.tr("tooltips.open-annotation-tool"),
"action": function () {
var tool = Settings.data.appLauncher.screenshotAnnotationTool;
Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipboardId + " | " + tool]);
if (launcher)
launcher.close();
}
});
}
// Delete action
actions.push({
"icon": "trash",
"tooltip": I18n.tr("launcher.providers.clipboard-delete"),
"action": function () {
deleteItem(item);
}
});
return actions;
}
function canDeleteItem(item) {
return item && !!item.clipboardId;
}
function deleteItem(item) {
if (!item || !item.clipboardId)
return;
// Set provider state before deletion so refresh works
gotResults = false;
isWaitingForData = true;
lastSearchText = launcher ? launcher.searchText : "";
// Delete the item
ClipboardService.deleteById(String(item.clipboardId));
}
// Prepare item for display (handles image decoding)
function prepareItem(item) {
if (item && item.isImage && item.clipboardId) {
if (!ClipboardService.getImageData(item.clipboardId)) {
ClipboardService.decodeToDataUrl(item.clipboardId, item.mime, null);
}
}
}
// Get image URL for item (used by delegates)
function getImageUrl(item) {
if (!item || !item.clipboardId)
return "";
return ClipboardService.getImageData(item.clipboardId) || "";
}
// Get preview data for the preview panel
function getPreviewData(item) {
if (!item)
return null;
return {
"clipboardId": item.clipboardId,
"isImage": item.isImage,
"mime": item.mime,
"preview": item.preview
};
}
}
@@ -0,0 +1,51 @@
import QtQuick
import Quickshell
import qs.Commons
Item {
property var launcher: null
property string name: I18n.tr("common.command")
property string iconMode: Settings.data.appLauncher.iconMode
function handleCommand(query) {
return query.startsWith(">cmd");
}
function commands() {
return [
{
"name": ">cmd",
"description": I18n.tr("launcher.providers.command-description"),
"icon": iconMode === "tabler" ? "terminal" : "utilities-terminal",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {
launcher.setSearchText(">cmd ");
}
}
];
}
function getResults(query) {
if (!query.startsWith(">cmd"))
return [];
let expression = query.substring(4).trim();
return [
{
"name": I18n.tr("common.command"),
"description": I18n.tr("launcher.providers.command-description"),
"icon": iconMode === "tabler" ? "terminal" : "utilities-terminal",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {
launcher.closeImmediately();
Qt.callLater(() => {
Logger.d("CommandProvider", "Executing shell command: " + expression);
Quickshell.execDetached(["sh", "-c", expression]);
});
}
}
];
}
}
@@ -0,0 +1,165 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services.Keyboard
Item {
id: root
// Provider metadata
property string name: I18n.tr("launcher.providers.emoji")
property var launcher: null
property string iconMode: Settings.data.appLauncher.iconMode
property bool handleSearch: false
property string supportedLayouts: "grid" // Only grid layout for emoji
property int preferredGridColumns: 7 // More columns for compact emoji display
property real preferredGridCellRatio: 1.15 // Slightly taller than wide to accommodate label
property bool supportsAutoPaste: true // Emoji can be auto-pasted
property bool ignoreDensity: false // Emoji should scale with launcher density
property string selectedCategory: "recent"
property bool showsCategories: true // Default to showing categories
// Empty state message for category view
readonly property string emptyBrowsingMessage: selectedCategory === "recent" ? I18n.tr("launcher.providers.emoji-no-recent") : ""
property var categoryIcons: ({
"all": "apps",
"recent": "clock",
"people": "user",
"animals": "paw",
"nature": "leaf",
"food": "apple",
"activity": "run",
"travel": "plane",
"objects": "home",
"symbols": "star",
"flags": "flag"
})
property var categories: ["all", "recent", "people", "animals", "nature", "food", "activity", "travel", "objects", "symbols", "flags"]
function getCategoryName(category) {
const names = {
"all": I18n.tr("launcher.categories.all"),
"recent": I18n.tr("launcher.categories.emoji-recent"),
"people": I18n.tr("launcher.categories.emoji-people"),
"animals": I18n.tr("launcher.categories.emoji-animals"),
"nature": I18n.tr("launcher.categories.emoji-nature"),
"food": I18n.tr("launcher.categories.emoji-food"),
"activity": I18n.tr("launcher.categories.emoji-activity"),
"travel": I18n.tr("launcher.categories.emoji-travel"),
"objects": I18n.tr("launcher.categories.emoji-objects"),
"symbols": I18n.tr("launcher.categories.emoji-symbols"),
"flags": I18n.tr("launcher.categories.emoji-flags")
};
return names[category] || category;
}
// Force update results when emoji service loads
Connections {
target: EmojiService
function onLoadedChanged() {
if (EmojiService.loaded && root.launcher) {
root.launcher.updateResults();
}
}
}
// Initialize provider
function init() {
Logger.d("EmojiProvider", "Initialized");
}
function selectCategory(category) {
selectedCategory = category;
if (launcher) {
launcher.updateResults();
}
}
function onOpened() {
// Always reset to "recent" category when opening
selectedCategory = "recent";
}
// Check if this provider handles the command
function handleCommand(searchText) {
return searchText.startsWith(">emoji");
}
// Return available commands when user types ">"
function commands() {
return [
{
"name": ">emoji",
"description": I18n.tr("launcher.providers.emoji-search-description"),
"icon": iconMode === "tabler" ? "mood-smile" : "face-smile",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {
launcher.setSearchText(">emoji ");
}
}
];
}
// Get search results
function getResults(searchText) {
if (!searchText.startsWith(">emoji")) {
return [];
}
if (!EmojiService.loaded) {
return [
{
"name": I18n.tr("launcher.providers.emoji-loading"),
"description": I18n.tr("launcher.providers.emoji-loading-description"),
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {}
}
];
}
var query = searchText.slice(6).trim();
var emojis = [];
if (query !== "" || selectedCategory === "all") {
emojis = EmojiService.search(query);
} else {
emojis = EmojiService.getEmojisByCategory(selectedCategory);
}
return emojis.map(formatEmojiEntry);
}
// Format an emoji entry for the results list
function formatEmojiEntry(emoji) {
let title = emoji.name;
let description = emoji.keywords.join(", ");
if (emoji.category) {
description += " • Category: " + emoji.category;
}
const emojiChar = emoji.emoji;
return {
"name": title,
"description": description,
"icon": null,
"isImage": false,
"displayString": emojiChar,
"autoPasteText": emojiChar,
"provider": root,
"onAutoPaste": function () {
EmojiService.recordUsage(emojiChar);
},
"onActivate": function () {
EmojiService.copy(emojiChar);
launcher.close();
}
};
}
}
@@ -0,0 +1,194 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services.Compositor
import qs.Services.UI
Item {
id: root
// Provider metadata
property string name: I18n.tr("tooltips.session-menu")
property var launcher: null
property bool handleSearch: Settings.data.appLauncher.enableSessionSearch
property string supportedLayouts: "list"
property string iconMode: Settings.data.appLauncher.iconMode
// Session actions with search keywords
readonly property var sessionActions: [
{
"action": "lock",
"labelKey": "common.lock",
"icon": iconMode === "tabler" ? "lock" : "system-lock-screen",
"keywords": ["lock", "screen", "secure"]
},
{
"action": "suspend",
"labelKey": "common.suspend",
"icon": iconMode === "tabler" ? "suspend" : "system-suspend",
"keywords": ["suspend", "sleep", "standby"]
},
{
"action": "hibernate",
"labelKey": "common.hibernate",
"icon": iconMode === "tabler" ? "hibernate" : "system-suspend-hibernate",
"keywords": ["hibernate", "disk"]
},
{
"action": "reboot",
"labelKey": "common.reboot",
"icon": iconMode === "tabler" ? "reboot" : "system-reboot",
"keywords": ["reboot", "restart", "reload"]
},
{
"action": "rebootToUefi",
"labelKey": "common.reboot-to-uefi",
"icon": iconMode === "tabler" ? "reboot" : "system-reboot",
"keywords": ["reboot", "uefi", "firmware", "bios"]
},
{
"action": "userspaceReboot",
"labelKey": "common.userspace-reboot",
"icon": iconMode === "tabler" ? "rotate" : "system-reboot",
"keywords": ["reboot", "restart", "soft", "userspace"]
},
{
"action": "logout",
"labelKey": "common.logout",
"icon": iconMode === "tabler" ? "logout" : "system-log-out",
"keywords": ["logout", "sign out", "exit", "leave"]
},
{
"action": "shutdown",
"labelKey": "common.shutdown",
"icon": iconMode === "tabler" ? "shutdown" : "system-shutdown",
"keywords": ["shutdown", "power off", "turn off", "poweroff"]
}
]
function init() {
Logger.d("SessionProvider", "Initialized");
}
function getEnabledActions() {
var powerOptions = Settings.data.sessionMenu.powerOptions || [];
var enabledSet = {};
for (var i = 0; i < powerOptions.length; i++) {
if (powerOptions[i].enabled) {
enabledSet[powerOptions[i].action] = powerOptions[i];
}
}
var enabled = [];
for (var j = 0; j < sessionActions.length; j++) {
var action = sessionActions[j];
if (enabledSet[action.action]) {
enabled.push({
"action": action.action,
"labelKey": action.labelKey,
"icon": action.icon,
"keywords": action.keywords,
"command": enabledSet[action.action].command || ""
});
}
}
return enabled;
}
function getResults(query) {
if (!query)
return [];
var trimmed = query.trim();
if (!trimmed || trimmed.length < 2)
return [];
var enabledActions = getEnabledActions();
if (enabledActions.length === 0)
return [];
// Build searchable items with resolved translations
var items = [];
for (var i = 0; i < enabledActions.length; i++) {
var action = enabledActions[i];
var label = I18n.tr(action.labelKey);
items.push({
"action": action.action,
"icon": action.icon,
"label": label,
"command": action.command,
"searchText": [label.toLowerCase()].concat(action.keywords).join(" ")
});
}
var results = FuzzySort.go(trimmed, items, {
"keys": ["label", "searchText"],
"limit": 6
});
var launcherItems = [];
for (var j = 0; j < results.length; j++) {
var entry = results[j].obj;
var score = results[j].score;
launcherItems.push({
"name": entry.label,
"description": I18n.tr("tooltips.session-menu"),
"icon": entry.icon,
"isTablerIcon": true,
"isImage": false,
"_score": score - 1,
"provider": root,
"onActivate": createActivateHandler(entry.action, entry.command)
});
}
return launcherItems;
}
function createActivateHandler(action, command) {
return function () {
if (launcher)
launcher.close();
// Execute via Qt.callLater, but reference only singletons
// (root may be destroyed after launcher.close() unloads the panel)
Qt.callLater(() => {
switch (action) {
case "lock":
if (PanelService.lockScreen && !PanelService.lockScreen.active) {
PanelService.lockScreen.active = true;
}
break;
case "suspend":
if (Settings.data.general.lockOnSuspend) {
CompositorService.lockAndSuspend();
} else {
CompositorService.suspend();
}
break;
case "hibernate":
CompositorService.hibernate();
break;
case "reboot":
CompositorService.reboot();
break;
case "rebootToUefi":
CompositorService.rebootToUefi();
break;
case "userspaceReboot":
CompositorService.userspaceReboot();
break;
case "logout":
CompositorService.logout();
break;
case "shutdown":
CompositorService.shutdown();
break;
}
});
};
}
}
@@ -0,0 +1,161 @@
import QtQuick
import qs.Commons
import qs.Services.UI
Item {
id: root
// Provider metadata
property string name: I18n.tr("common.settings")
property var launcher: null
property bool handleSearch: Settings.data.appLauncher.enableSettingsSearch
property string supportedLayouts: "list"
property string iconMode: Settings.data.appLauncher.iconMode
function init() {
Logger.d("SettingsProvider", "Initialized");
}
// Check if this provider handles the command
function handleCommand(searchText) {
return searchText.startsWith(">settings");
}
// Return available commands when user types ">"
function commands() {
return [
{
"name": ">settings",
"description": I18n.tr("launcher.providers.settings-search-description"),
"icon": iconMode === "tabler" ? "settings" : "preferences-system",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {
launcher.setSearchText(">settings ");
}
}
];
}
function getResults(query) {
if (!query || SettingsSearchService.searchIndex.length === 0)
return [];
var trimmed = query.trim();
// Handle command mode: ">settings" or ">settings <search>"
var isCommandMode = trimmed.startsWith(">settings");
if (isCommandMode) {
// Extract search term after ">settings "
var searchTerm = trimmed.substring(9).trim();
// In command mode, show all settings if no search term
if (searchTerm.length === 0) {
return getAllSettings();
}
trimmed = searchTerm;
} else {
// Regular search mode - require at least 2 chars
if (!trimmed || trimmed.length < 2)
return [];
}
// Build searchable items with resolved translations, filtering out invisible entries
let items = [];
for (let j = 0; j < SettingsSearchService.searchIndex.length; j++) {
const entry = SettingsSearchService.searchIndex[j];
if (!SettingsSearchService.isEntryVisible(entry))
continue;
items.push({
"labelKey": entry.labelKey,
"descriptionKey": entry.descriptionKey,
"widget": entry.widget,
"tab": entry.tab,
"tabLabel": entry.tabLabel,
"subTab": entry.subTab,
"subTabLabel": entry.subTabLabel || null,
"label": I18n.tr(entry.labelKey),
"description": entry.descriptionKey ? I18n.tr(entry.descriptionKey) : "",
"subTabName": entry.subTabLabel ? I18n.tr(entry.subTabLabel) : ""
});
}
const results = FuzzySort.go(trimmed, items, {
"keys": ["label", "subTabName", "description"],
"limit": 10,
"scoreFn": function (r) {
const labelScore = r[0].score;
const subTabScore = r[1].score * 1.5;
const descScore = r[2].score;
return Math.max(labelScore, subTabScore, descScore);
}
});
let launcherItems = [];
for (let i = 0; i < results.length; i++) {
const entry = results[i].obj;
const score = results[i].score;
const tabName = I18n.tr(entry.tabLabel);
const subTabName = entry.subTabName || "";
const breadcrumb = subTabName ? (tabName + " " + subTabName) : tabName;
launcherItems.push({
"name": entry.label,
"description": breadcrumb,
"icon": iconMode === "tabler" ? "settings" : "preferences-system",
"isTablerIcon": true,
"isImage": false,
"_score": score - 2,
"provider": root,
"onActivate": createActivateHandler(entry)
});
}
return launcherItems;
}
function getAllSettings() {
var launcherItems = [];
for (var j = 0; j < SettingsSearchService.searchIndex.length; j++) {
var entry = SettingsSearchService.searchIndex[j];
if (!SettingsSearchService.isEntryVisible(entry))
continue;
var label = I18n.tr(entry.labelKey);
var tabName = I18n.tr(entry.tabLabel);
var subTabName = entry.subTabLabel ? I18n.tr(entry.subTabLabel) : "";
var breadcrumb = subTabName ? (tabName + " " + subTabName) : tabName;
launcherItems.push({
"name": label,
"description": breadcrumb,
"icon": iconMode === "tabler" ? "settings" : "preferences-system",
"isTablerIcon": true,
"isImage": false,
"_score": 0,
"provider": root,
"onActivate": createActivateHandler({
"labelKey": entry.labelKey,
"descriptionKey": entry.descriptionKey,
"widget": entry.widget,
"tab": entry.tab,
"tabLabel": entry.tabLabel,
"subTab": entry.subTab,
"subTabLabel": entry.subTabLabel || null
})
});
}
return launcherItems;
}
function createActivateHandler(entry) {
return function () {
if (launcher)
launcher.close();
Qt.callLater(() => {
SettingsPanelService.openToEntry(entry, launcher.screen);
});
};
}
}
@@ -0,0 +1,159 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services.Compositor
Item {
id: root
property string name: I18n.tr("common.windows")
property var launcher: null
property bool handleSearch: Settings.data.appLauncher.enableWindowsSearch
property string supportedLayouts: "list"
property string iconMode: Settings.data.appLauncher.iconMode
function init() {
Logger.d("WindowsProvider", "Initialized");
}
// Check if this provider handles the command
function handleCommand(searchText) {
return searchText.startsWith(">win");
}
// Return available commands when user types ">"
function commands() {
return [
{
"name": ">win",
"description": I18n.tr("launcher.providers.windows-search-description"),
"icon": iconMode === "tabler" ? "app-window" : "preferences-system-windows",
"isTablerIcon": true,
"isImage": false,
"onActivate": function () {
launcher.setSearchText(">win ");
}
}
];
}
function getResults(query) {
if (!query)
return [];
var trimmed = query.trim();
// Handle command mode: ">win" or ">win <search>"
var isCommandMode = trimmed.startsWith(">win");
if (isCommandMode) {
// Extract search term after ">win "
var searchTerm = trimmed.substring(4).trim();
// In command mode, show all windows if no search term
if (searchTerm.length === 0) {
return getAllWindows();
}
trimmed = searchTerm;
} else {
// Regular search mode - require at least 2 chars
if (trimmed.length < 2)
return [];
}
var items = [];
// Collect all windows from CompositorService
for (var i = 0; i < CompositorService.windows.count; i++) {
var win = CompositorService.windows.get(i);
items.push({
"id": win.id,
"title": win.title || "",
"appId": win.appId || "",
"workspaceId": win.workspaceId,
"isFocused": win.isFocused,
"searchText": (win.title + " " + win.appId).toLowerCase()
});
}
// Fuzzy search on title and appId
var results = FuzzySort.go(trimmed, items, {
"keys": ["title", "appId"],
"limit": 10
});
// Map to launcher items
var launcherItems = [];
for (var j = 0; j < results.length; j++) {
var entry = results[j].obj;
var score = results[j].score;
// Get icon name from DesktopEntry if available, otherwise use appId
var iconName = entry.appId;
var appEntry = ThemeIcons.findAppEntry(entry.appId);
if (appEntry && appEntry.icon) {
iconName = appEntry.icon;
}
launcherItems.push({
"name": entry.title || entry.appId,
"description": entry.appId,
"icon": iconName || "application-x-executable",
"isTablerIcon": false,
"badgeIcon": "app-window",
"_score": score,
"provider": root,
"windowId": entry.id,
"onActivate": createActivateHandler(entry)
});
}
return launcherItems;
}
function getAllWindows() {
var launcherItems = [];
for (var i = 0; i < CompositorService.windows.count; i++) {
var win = CompositorService.windows.get(i);
var iconName = win.appId;
var appEntry = ThemeIcons.findAppEntry(win.appId);
if (appEntry && appEntry.icon) {
iconName = appEntry.icon;
}
launcherItems.push({
"name": win.title || win.appId,
"description": win.appId,
"icon": iconName || "application-x-executable",
"isTablerIcon": false,
"badgeIcon": "app-window",
"_score": 0,
"provider": root,
"windowId": win.id,
"onActivate": createActivateHandler({
"id": win.id
})
});
}
return launcherItems;
}
function createActivateHandler(windowEntry) {
return function () {
if (launcher)
launcher.close();
Qt.callLater(() => {
// Find the actual window object to pass to focusWindow
for (var i = 0; i < CompositorService.windows.count; i++) {
var win = CompositorService.windows.get(i);
if (win.id === windowEntry.id) {
CompositorService.focusWindow(win);
break;
}
}
});
};
}
}