[FEATURE] Native Quickshell-based Application Launcher as a lightweight alternative · Issue #1805 · mylinuxforwork/dotfiles · GitHub
Skip to content

[FEATURE] Native Quickshell-based Application Launcher as a lightweight alternative #1805

Open

Description

@tetam01

Is your feature request related to a problem? Please describe.

With ML4W's transition to Quickshell for the statusbar and dock, the desktop environment has become incredibly fluid, modern, and unified.

Following the deprecation of Walker, users currently rely on Rofi as the primary launcher. While Rofi (via rofi-wayland) is a solid tool, its styling is detached from the QML ecosystem.

Furthermore, Walker and its backend dependencies (like the elephant libraries) are only available via the AUR. This makes installation and updates complex and introduces package maintenance overhead for users.

Describe the solution you'd like

An optional, native application launcher built entirely on Quickshell (QML) to complete the QML desktop experience.
Key benefits of this solution:

  1. No AUR/Dependency Overhead: It runs directly on the already installed Quickshell runtime. No need to install Walker or elephant packages from the AUR.
  2. Native QML API: It accesses applications instantly using Quickshell's native DesktopEntries.applications.values API.
  3. Zero Wayland Mapping Lags: By running the window permanently mapped (visible: true) and dynamically routing mouse/keyboard inputs via Wayland input masks (mask: Region), it launches instantly without typical Wayland window-mapping flicker.
  4. Persistent Recents: Stores and displays recently opened apps at the top using the QtCore Settings API.
  5. Matugen Integration: Inherits colors and updates in real-time from the active ML4W colors.

Describe alternatives you've considered

  1. Rofi (rofi-wayland): A solid and reliable fallback, but its configuration and styling (.rasi) are detached from the QML styling paradigm.
  2. Walker: Great feature set, but introduces maintenance overhead due to AUR-only dependencies (elephant).

Additional context

I have successfully implemented this custom Quickshell launcher on my system. The entire setup is very modular, living under ~/.config/quickshell/launcher/.

Here is the wrapper script (qslauncher) used to toggle the instance instantly via IPC:

Click to view the full qslauncher
#!/usr/bin/env bash
CONFIG_DIR="$HOME/.config/quickshell/launcher"
if pgrep -f "quickshell -p .*quickshell/launcher" > /dev/null; then
    qs -p "$CONFIG_DIR" ipc call launcher toggle
else
    nohup quickshell -p "$CONFIG_DIR" > /dev/null 2>&1 &
    for i in {1..20}; do
        if qs -p "$CONFIG_DIR" ipc call launcher open >/dev/null 2>/dev/null; then
            break
        fi
        sleep 0.1
    done
fi

And here is the QML configuration (shell.qml) containing the user interface, search logic, and style bindings:

Click to view the full shell.qml (approx. 400 lines)
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtCore

PanelWindow {
    id: window

    // Centered window via Wayland Layer-Shell
    anchors {
        left: false
        right: false
        top: false
        bottom: false
    }

    color: "transparent"

    property bool opened: false
    property var recentIds: []
    property int selectedIndex: 0

    WlrLayershell.namespace: "quickshell-launcher"
    WlrLayershell.layer: WlrLayer.Overlay
    WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None

    focusable: true
    visible: true

    width: 750
    height: 550

    mask: Region {
        item: opened ? maskCover : null
    }

    Item {
        id: maskCover
        anchors.fill: parent
    }

    Settings {
        id: appSettings
        category: "QuickshellLauncher"
        property string recentIdsSerialized: "[]"
    }

    property string searchQuery: searchField.text.trim().toLowerCase()
    property var filteredApps: {
        var q = searchQuery;
        var vals = DesktopEntries.applications.values;
        if (!vals) return [];

        if (q !== "") {
            return vals.filter(function (e) {
                if (e.name && e.name.toLowerCase().indexOf(q) !== -1)
                    return true;
                if (e.genericName && e.genericName.toLowerCase().indexOf(q) !== -1)
                    return true;
                if (e.keywords) {
                    for (var i = 0; i < e.keywords.length; i++) {
                        if (e.keywords[i] && e.keywords[i].toLowerCase().indexOf(q) !== -1)
                            return true;
                    }
                }
                return false;
            }).sort(function (a, b) {
                return a.name.localeCompare(b.name);
            });
        }

        var recent = recentIds || [];
        return vals.slice().sort(function (a, b) {
            var ai = recent.indexOf(a.id);
            var bi = recent.indexOf(b.id);
            if (ai !== -1 && bi !== -1)
                return ai - bi;
            if (ai !== -1)
                return -1;
            if (bi !== -1)
                return 1;
            return a.name.localeCompare(b.name);
        });
    }

    onFilteredAppsChanged: {
        selectedIndex = 0;
    }

    // Dynamic Matugen Colors Integration
    FileView {
        id: matugenColorsFile
        path: Quickshell.env("HOME") + "/.config/ml4w/colors/colors.json"
        watchChanges: true
        onFileChanged: reload()

        JsonAdapter {
            id: matugenColors
            property string primary: "#88d1ec"
            property string background: "#0f1416"
            property string surface: "#0f1416"
            property string surface_container: "#1b2124"
            property string on_background: "#dee3e6"
            property string on_surface_variant: "#c0c8cc"
            property string outline: "#8a9296"
        }
    }

    // Global ESC shortcut to dismiss the launcher instantly
    Shortcut {
        sequence: "Escape"
        enabled: window.opened
        onActivated: window.closeLauncher()
    }

    // IPC Endpoint to open / toggle the launcher instantly
    IpcHandler {
        target: "launcher"

        property bool active: window.opened

        function toggle(): void {
            if (window.opened) {
                window.closeLauncher();
            } else {
                window.openLauncher();
            }
        }

        function open(): void {
            window.openLauncher();
        }

        function close(): void {
            window.closeLauncher();
        }
    }

    function openLauncher() {
        window.opened = true;
        selectedIndex = 0;
        searchField.text = "";
        searchField.forceActiveFocus();
    }

    function closeLauncher() {
        window.opened = false;
        searchField.focus = false;
    }

    function recordLaunch(appId) {
        var list = recentIds.slice();
        var idx = list.indexOf(appId);
        if (idx !== -1) {<details>
<summary>Click to view the full qslauncher</summary>

```bash
#!/usr/bin/env bash
CONFIG_DIR="$HOME/.config/quickshell/launcher"
if pgrep -f "quickshell -p .*quickshell/launcher" > /dev/null; then
    qs -p "$CONFIG_DIR" ipc call launcher toggle
else
    nohup quickshell -p "$CONFIG_DIR" > /dev/null 2>&1 &
    for i in {1..20}; do
        if qs -p "$CONFIG_DIR" ipc call launcher open >/dev/null 2>/dev/null; then
            break
        fi
        sleep 0.1
    done
fi
        list.splice(idx, 1);
    }
    list.unshift(appId);
    if (list.length > 10) {
        list = list.slice(0, 10);
    }
    recentIds = list;
    appSettings.recentIdsSerialized = JSON.stringify(list);
}

function launchSelected() {
    if (selectedIndex >= 0 && selectedIndex < filteredApps.length) {
        var app = filteredApps[selectedIndex];
        recordLaunch(app.id);
        app.execute();
        window.closeLauncher();
    }
}

// Modern glass-morphic container
Rectangle {
    anchors.fill: parent
    radius: 20
    focus: true

    opacity: opened ? 1.0 : 0.0

    Behavior on opacity {
        NumberAnimation {
            duration: 260
            easing.type: Easing.OutCubic
        }
    }

    Keys.onPressed: (event) => {
        if (event.key === Qt.Key_Escape) {
            window.closeLauncher();
            event.accepted = true;
        }
    }

    // Dynamically themed background and border from Matugen
    color: {
        var bg = Qt.color(matugenColors.background);
        return Qt.rgba(bg.r, bg.g, bg.b, 0.97);
    }
    border.color: matugenColors.primary
    border.width: 2

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // Top Layout containing Search Bar and Close Button
        RowLayout {
            Layout.fillWidth: true
            spacing: 15

            // Search Bar
            Rectangle {
                Layout.fillWidth: true
                height: 55
                Layout.preferredHeight: 55

                color: {
                    var sc = Qt.color(matugenColors.surface_container);
                    return Qt.rgba(sc.r, sc.g, sc.b, 0.8);
                }
                radius: 12
                border.color: searchField.activeFocus ? matugenColors.primary : "transparent"
                border.width: 1

                RowLayout {
                    anchors.fill: parent
                    anchors.leftMargin: 15
                    anchors.rightMargin: 15
                    spacing: 10

                    Text {
                        text: "➔"
                        color: matugenColors.primary
                        font.pixelSize: 18
                    }

                    TextField {
                        id: searchField
                        Layout.fillWidth: true
                        placeholderText: "Search apps..."
                        placeholderTextColor: Qt.rgba(1, 1, 1, 0.4)
                        color: matugenColors.on_background
                        font.family: "Fira Sans"
                        font.pixelSize: 18
                        background: null
                        focus: true

                        onTextChanged: {
                            selectedIndex = 0;
                        }

                        Keys.onPressed: (event) => {
                            if (event.key === Qt.Key_Down) {
                                selectedIndex = (selectedIndex + 1) % filteredApps.length;
                                event.accepted = true;
                            } else if (event.key === Qt.Key_Up) {
                                selectedIndex = (selectedIndex - 1 + filteredApps.length) % filteredApps.length;
                                event.accepted = true;
                            } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
                                window.launchSelected();
                                event.accepted = true;
                            } else if (event.key === Qt.Key_Escape) {
                                window.closeLauncher();
                                event.accepted = true;
                            }
                        }
                    }
                }
            }

            // Sleek Close Button
            Rectangle {
                id: closeButton
                width: 55
                height: 55
                Layout.preferredWidth: 55
                Layout.preferredHeight: 55
                radius: 12

                color: closeMouseArea.containsMouse ? Qt.rgba(1, 0.2, 0.2, 0.25) : Qt.rgba(1, 1, 1, 0.08)
                border.color: closeMouseArea.containsMouse ? "#ff5555" : "transparent"
                border.width: 1

                Text {
                    anchors.centerIn: parent
                    text: "✕"
                    color: closeMouseArea.containsMouse ? "#ff5555" : matugenColors.on_background
                    font.pixelSize: 18
                    font.bold: true
                }

                MouseArea {
                    id: closeMouseArea
                    anchors.fill: parent
                    hoverEnabled: true
                    onClicked: window.closeLauncher()
                }
            }
        }

        // Scrollable App ListView
        ScrollView {
            Layout.fillWidth: true
            Layout.fillHeight: true
            clip: true

            ListView {
                id: listView
                model: window.filteredApps
                spacing: 6
                currentIndex: window.selectedIndex

                delegate: Item {
                    width: listView.width
                    height: 55

                    Rectangle {
                        anchors.fill: parent
                        anchors.leftMargin: 5
                        anchors.rightMargin: 5
                        radius: 8

                        // Accent selection glow using Matugen primary color
                        color: matugenColors.primary
                        opacity: index === window.selectedIndex ? 0.2 : 0.0

                        Behavior on opacity {
                            NumberAnimation { duration: 80 }
                        }
                    }

                    // App Item layout
                    RowLayout {
                        anchors.fill: parent
                        anchors.leftMargin: 20
                        anchors.rightMargin: 20
                        spacing: 15

                        // Dynamic App Icon from system theme
                        Image {
                            width: 32
                            height: 32
                            Layout.preferredWidth: 32
                            Layout.preferredHeight: 32
                            source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/system-run"
                            fillMode: Image.PreserveAspectFit
                        }

                        // Combined Name & Description with consistent layout width
                        Text {
                            text: modelData.name + (modelData.genericName ? " <font color='" + (index === window.selectedIndex ? "#e0e0e0" : matugenColors.on_surface_variant) + "'>—  " + modelData.genericName + "</font>" : "")
                            textFormat: Text.StyledText
                            color: index === window.selectedIndex ? "#ffffff" : matugenColors.on_background
                            font.family: "Fira Sans"
                            font.pixelSize: 16
                            font.bold: index === window.selectedIndex
                            Layout.fillWidth: true
                            elide: Text.ElideRight
                        }

                        // Subtle "Recent" badge
                        Text {
                            visible: searchQuery === "" && recentIds.indexOf(modelData.id) !== -1 && recentIds.indexOf(modelData.id) < 5
                            text: "Recent"
                            color: matugenColors.primary
                            font.family: "Fira Sans"
                            font.pixelSize: 11
                            font.bold: true
                            opacity: index === window.selectedIndex ? 1.0 : 0.7
                        }
                    }

                    MouseArea {
                        anchors.fill: parent
                        hoverEnabled: true
                        onEntered: window.selectedIndex = index
                        onClicked: window.launchSelected()
                    }
                }
            }
        }
    }
}

// Auto-focus and window focus management
onOpenedChanged: {
    if (opened) {
        visible = true;
        Qt.callLater(function() {
            searchField.forceActiveFocus();
        });
    }
}

Component.onCompleted: {
    try {
        recentIds = JSON.parse(appSettings.recentIdsSerialized) || [];
    } catch(e) {
        recentIds = [];
    }
}

}

</details>


### Support or Contribution

- [x] [I am willing to contribute or support this feature](https://github.com/mylinuxforwork/dotfiles)

Activity

  1. added this to the 2.16 milestone on Aug 30, 2026
  2. mylinuxforwork commented on Aug 30, 2026

    @mylinuxforwork
    Owner

    Hi, You request makes totally sense and I am already planning a launcher for the next version. But this requires that also all other rofi based selectors must be replaced. So, I need something that is very flexible and also supporting custom menus (dmenu mode).

    Can you share a screenshot from your solution?

  3. tetam01 commented on Aug 30, 2026

    @tetam01
    Author

    Here is a screenshot of the launcher running on my desktop!

    Image

    Regarding the dmenu mode and replacing other Rofi selectors: That makes total sense! My launcher shell.qml currently focuses on desktop applications, but extending the model to accept custom lists (dmenu/stdin/JSON) to replace all Rofi selectors in ML4W would be an awesome feature for the next version.

    As an example of replacing another Rofi selector, I also built a native Quickshell Clipboard Manager with live image/text previews to replace Rofi cliphist. Here is the QML and helper script for that:

    Click to view Clipboard Manager shell.qml
    import QtQuick
    import QtQuick.Controls
    import QtQuick.Layouts
    import QtQuick.Effects
    import Quickshell
    import Quickshell.Io
    import Quickshell.Wayland
    
    FloatingWindow {
        id: window
    
        title: "Zwischenablage"
        implicitWidth: 880
        implicitHeight: 580
        visible: false
    
        color: matugenColors.background
    
        property var clipboardItems: []
        property int selectedIndex: 0
        property string searchQuery: ""
        property string currentPreviewText: ""
        property bool isImagePreview: false
    
        onVisibleChanged: {
            if (visible) {
                window.loadClipboardData();
                focusTimer.restart();
            }
        }
    
        Timer {
            id: focusTimer
            interval: 50
            repeat: false
            onTriggered: {
                searchField.forceActiveFocus();
            }
        }
    
        Component.onCompleted: {
            window.visible = true;
            window.loadClipboardData();
            focusTimer.restart();
        }
    
        IpcHandler {
            target: "clipboard"
    
            function toggle(): void {
                window.visible = !window.visible;
                if (window.visible) {
                    window.loadClipboardData();
                    searchField.forceActiveFocus();
                }
            }
    
            function open(): void {
                window.visible = true;
                window.loadClipboardData();
                searchField.forceActiveFocus();
            }
    
            function close(): void {
                window.visible = false;
            }
    
            function refresh(): void {
                window.loadClipboardData();
            }
        }
    
        // Dynamic Matugen Colors Integration
        FileView {
            id: matugenColorsFile
            path: Quickshell.env("HOME") + "/.config/ml4w/colors/colors.json"
            watchChanges: true
            onFileChanged: reload()
            
            JsonAdapter {
                id: matugenColors
                property string primary: "#faba72"
                property string primary_container: "#673d00"
                property string on_primary_container: "#ffddba"
                property string background: "#18120c"
                property string surface: "#18120c"
                property string surface_container: "#251e17"
                property string surface_container_high: "#302921"
                property string surface_container_low: "#211a14"
                property string on_background: "#eee0d5"
                property string on_surface: "#eee0d5"
                property string on_surface_variant: "#d4c4b5"
                property string outline: "#9d8e81"
                property string outline_variant: "#50453a"
                property string error: "#ffb4ab"
            }
        }
    
        function loadClipboardData() {
            cliphistProc.running = false;
            cliphistProc.running = true;
        }
    
        function parseCliphist(rawText) {
            var lines = rawText.split("\n");
            var items = [];
            for (var i = 0; i < lines.length; i++) {
                var line = lines[i].trim();
                if (line === "") continue;
                var tabIdx = line.indexOf("\t");
                if (tabIdx !== -1) {
                    var id = line.substring(0, tabIdx).trim();
                    var text = line.substring(tabIdx + 1);
                    items.push({ id: id, preview: text, raw: line });
                } else {
                    items.push({ id: line, preview: line, raw: line });
                }
            }
            window.clipboardItems = items;
            if (window.selectedIndex >= items.length) {
                window.selectedIndex = 0;
            }
            window.updatePreview();
        }
    
        function isImageEntry(text) {
            if (!text) return false;
            var lower = text.trim().toLowerCase();
            if (lower.indexOf("[image") !== -1 || lower.indexOf("binary data") !== -1) return true;
            var cleanPath = text.trim();
            if (cleanPath.startsWith("file://")) cleanPath = cleanPath.substring(7);
            if (cleanPath.startsWith("/") && (lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg") || lower.endsWith(".webp") || lower.endsWith(".svg") || lower.endsWith(".gif"))) {
                return true;
            }
            return false;
        }
    
        function updatePreview() {
            var items = window.filteredItems;
            if (!items || items.length === 0 || window.selectedIndex < 0 || window.selectedIndex >= items.length) {
                window.currentPreviewText = "";
                window.isImagePreview = false;
                previewImage.source = "";
                return;
            }
            var item = items[window.selectedIndex];
            if (!item) return;
    
            var helper = Quickshell.env("HOME") + "/.mydotfiles/custom/conf/quickshell/clipboard/preview_helper.sh";
            previewProc.command = [helper, item.id, item.preview];
            previewProc.running = false;
            previewProc.running = true;
        }
    
        function copyItem(item) {
            if (!item || !item.id) return;
            var cmd = 'raw="$(cliphist decode \'' + item.id + '\')"; clean="$(echo "$raw" | head -n 1 | sed "s/^file:\\/\\///")"; if [ -n "$clean" ] && [ -e "$clean" ]; then uri="file://$clean"; printf "copy\\n%s" "$uri" | wl-copy -t x-special/gnome-copied-files 2>/dev/null & printf "%s\\r\\n" "$uri" | wl-copy -t text/uri-list 2>/dev/null & printf "%s" "$clean" | wl-copy -t text/plain 2>/dev/null & else cliphist decode \'' + item.id + '\' | wl-copy & fi';
            Quickshell.execDetached(["bash", "-c", cmd]);
            window.visible = false;
        }
    
        function deleteItem(item) {
            if (!item || !item.raw) return;
            var cmd = "printf '%s\\n' '" + item.raw.replace(/'/g, "'\\''") + "' | cliphist delete";
            Quickshell.execDetached(["bash", "-c", cmd]);
            delTimer.restart();
        }
    
        Timer {
            id: delTimer
            interval: 100
            repeat: false
            onTriggered: window.loadClipboardData()
        }
    
        function wipeClipboard() {
            Quickshell.execDetached(["cliphist", "wipe"]);
            window.clipboardItems = [];
            window.selectedIndex = 0;
            window.updatePreview();
        }
    
        onSelectedIndexChanged: updatePreview()
    
        property var filteredItems: {
            var q = window.searchQuery.toLowerCase();
            var items = window.clipboardItems;
            if (!items) return [];
            if (q === "") return items;
            return items.filter(function(item) {
                return item.preview.toLowerCase().indexOf(q) !== -1;
            });
        }
    
        Process {
            id: cliphistProc
            command: ["cliphist", "list"]
            stdout: StdioCollector {
                onStreamFinished: {
                    window.parseCliphist(this.text);
                }
            }
        }
    
        Process {
            id: previewProc
            command: []
            stdout: StdioCollector {
                onStreamFinished: {
                    var out = this.text;
                    if (out.startsWith("TYPE:IMG_PATH\n")) {
                        var imgPath = out.substring(14).trim();
                        window.isImagePreview = true;
                        previewImage.source = "";
                        previewImage.source = "file://" + imgPath;
                    } else if (out.startsWith("TYPE:TEXT\n")) {
                        window.isImagePreview = false;
                        window.currentPreviewText = out.substring(10);
                    } else {
                        window.isImagePreview = false;
                        window.currentPreviewText = out;
                    }
                }
            }
        }
    
        // Keyboard Shortcut Handler for Navigation
        Item {
            id: keyHandler
            anchors.fill: parent
            focus: true
    
            Keys.onPressed: function(event) {
                if (event.key === Qt.Key_Escape) {
                    window.visible = false;
                    event.accepted = true;
                } else if (event.key === Qt.Key_Down) {
                    if (window.filteredItems.length > 0) {
                        window.selectedIndex = Math.min(window.filteredItems.length - 1, window.selectedIndex + 1);
                        itemList.positionViewAtIndex(window.selectedIndex, ListView.Beginning);
                    }
                    event.accepted = true;
                } else if (event.key === Qt.Key_Up) {
                    if (window.filteredItems.length > 0) {
                        window.selectedIndex = Math.max(0, window.selectedIndex - 1);
                        itemList.positionViewAtIndex(window.selectedIndex, ListView.Beginning);
                    }
                    event.accepted = true;
                } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
                    if (window.filteredItems.length > 0 && window.selectedIndex < window.filteredItems.length) {
                        window.copyItem(window.filteredItems[window.selectedIndex]);
                    }
                    event.accepted = true;
                } else if (event.key === Qt.Key_Delete) {
                    if (window.filteredItems.length > 0 && window.selectedIndex < window.filteredItems.length) {
                        window.deleteItem(window.filteredItems[window.selectedIndex]);
                    }
                    event.accepted = true;
                }
            }
    
            ColumnLayout {
                anchors.fill: parent
                anchors.margins: 20
                spacing: 16
    
                // Header Bar
                RowLayout {
                    Layout.fillWidth: true
                    spacing: 14
    
                    RowLayout {
                        spacing: 10
                        Image {
                            source: "file://" + Quickshell.env("HOME") + "/.config/quickshell/shared/icons/terminal.svg"
                            width: 22
                            height: 22
                            fillMode: Image.PreserveAspectFit
                            layer.enabled: true
                            layer.effect: MultiEffect {
                                colorization: 1.0
                                colorizationColor: matugenColors.primary
                            }
                        }
                        Text {
                            text: "Zwischenablage"
                            font.family: "Fira Sans Semibold"
                            font.pixelSize: 20
                            font.bold: true
                            color: matugenColors.primary
                        }
                    }
    
                    Item { Layout.fillWidth: true }
    
                    // Wipe All Button
                    Button {
                        id: wipeBtn
                        padding: 6
                        leftPadding: 14
                        rightPadding: 14
                        topPadding: 6
                        bottomPadding: 6
                        contentItem: RowLayout {
                            spacing: 6
                            Image {
                                source: "file://" + Quickshell.env("HOME") + "/.config/quickshell/shared/icons/logout.svg"
                                width: 15
                                height: 15
                                fillMode: Image.PreserveAspectFit
                                layer.enabled: true
                                layer.effect: MultiEffect {
                                    colorization: 1.0
                                    colorizationColor: wipeBtn.hovered ? matugenColors.background : matugenColors.primary
                                }
                            }
                            Text {
                                text: "Alle löschen"
                                font.family: "Fira Sans Semibold"
                                font.pixelSize: 15
                                font.bold: true
                                color: wipeBtn.hovered ? matugenColors.background : matugenColors.primary
                                horizontalAlignment: Text.AlignHCenter
                                verticalAlignment: Text.AlignVCenter
                            }
                        }
                        background: Rectangle {
                            color: wipeBtn.hovered ? matugenColors.primary : "transparent"
                            border.color: matugenColors.primary
                            border.width: 1
                            radius: 6
                        }
                        onClicked: window.wipeClipboard()
                    }
    
                    // Close Button
                    Button {
                        id: closeBtn
                        implicitWidth: 30
                        implicitHeight: 30
                        contentItem: Text {
                            text: "✕"
                            font.family: "Fira Sans Semibold"
                            font.pixelSize: 15
                            font.bold: true
                            color: closeBtn.hovered ? matugenColors.background : matugenColors.primary
                            horizontalAlignment: Text.AlignHCenter
                            verticalAlignment: Text.AlignVCenter
                        }
                        background: Rectangle {
                            color: closeBtn.hovered ? matugenColors.primary : "transparent"
                            border.color: matugenColors.primary
                            border.width: 1
                            radius: 6
                        }
                        onClicked: window.visible = false
                    }
                }
    
                // Main Content Area: Left Column (List) + Right Column (Preview Pane)
                RowLayout {
                    Layout.fillWidth: true
                    Layout.fillHeight: true
                    spacing: 16
    
                    // LEFT COLUMN: Search + Entry List
                    ColumnLayout {
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                        spacing: 12
    
                        // Search Input Field
                        Rectangle {
                            Layout.fillWidth: true
                            implicitHeight: 46
                            color: matugenColors.background
                            border.color: matugenColors.primary
                            border.width: 1
                            radius: 10
    
                            RowLayout {
                                anchors.fill: parent
                                anchors.leftMargin: 14
                                anchors.rightMargin: 14
                                spacing: 10
    
                                Image {
                                    source: "file://" + Quickshell.env("HOME") + "/.config/quickshell/shared/icons/wallpaper.svg"
                                    width: 18
                                    height: 18
                                    fillMode: Image.PreserveAspectFit
                                    layer.enabled: true
                                    layer.effect: MultiEffect {
                                        colorization: 1.0
                                        colorizationColor: matugenColors.primary
                                    }
                                }
    
                                TextField {
                                    id: searchField
                                    focus: true
                                    Layout.fillWidth: true
                                    placeholderText: "Zwischenablage durchsuchen..."
                                    placeholderTextColor: matugenColors.on_surface_variant
                                    color: matugenColors.primary
                                    palette.text: matugenColors.primary
                                    palette.placeholderText: matugenColors.on_surface_variant
                                    font.family: "Fira Sans Semibold"
                                    font.pixelSize: 15
                                    background: null
                                    selectByMouse: true
    
                                    onTextChanged: {
                                        window.searchQuery = text.trim();
                                        window.selectedIndex = 0;
                                        window.updatePreview();
                                    }
    
                                    Keys.onPressed: function(event) {
                                        if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Escape || event.key === Qt.Key_Delete) {
                                            keyHandler.Keys.pressed(event);
                                        }
                                    }
                                }
    
                                Text {
                                    visible: searchField.text !== ""
                                    text: "✕"
                                    font.pixelSize: 13
                                    color: matugenColors.primary
                                    MouseArea {
                                        anchors.fill: parent
                                        onClicked: {
                                            searchField.text = "";
                                            window.searchQuery = "";
                                            window.updatePreview();
                                        }
                                    }
                                }
                            }
                        }
    
                        // List View for Items
                        ListView {
                            id: itemList
                            Layout.fillWidth: true
                            Layout.fillHeight: true
                            clip: true
                            spacing: 8
                            model: window.filteredItems
    
                            delegate: Rectangle {
                                id: itemRect
                                width: itemList.width
                                implicitHeight: 50
                                radius: 8
    
                                property bool isSelected: index === window.selectedIndex
    
                                color: isSelected 
                                    ? matugenColors.primary_container 
                                    : (itemMouseArea.containsMouse 
                                        ? matugenColors.surface_container_high
                                        : matugenColors.background)
                                
                                border.color: matugenColors.primary
                                border.width: isSelected ? 2 : 1
    
                                HoverHandler {
                                    id: itemHoverHandler
                                }
    
                                MouseArea {
                                    id: itemMouseArea
                                    anchors.fill: parent
                                    hoverEnabled: true
                                    onEntered: window.selectedIndex = index
                                    onClicked: window.copyItem(modelData)
                                }
    
                                RowLayout {
                                    anchors.fill: parent
                                    anchors.leftMargin: 14
                                    anchors.rightMargin: 14
                                    spacing: 12
    
                                    // Monochrome Icon (recaught via Matugen theme)
                                    Image {
                                        source: "file://" + Quickshell.env("HOME") + "/.config/quickshell/shared/icons/" + (window.isImageEntry(modelData.preview) ? "wallpaper.svg" : "terminal.svg")
                                        width: 18
                                        height: 18
                                        fillMode: Image.PreserveAspectFit
                                        layer.enabled: true
                                        layer.effect: MultiEffect {
                                            colorization: 1.0
                                            colorizationColor: matugenColors.primary
                                        }
                                    }
    
                                    // Item Preview Text
                                    Text {
                                        Layout.fillWidth: true
                                        text: modelData.preview
                                        font.family: "Fira Sans Semibold"
                                        font.pixelSize: 15
                                        font.bold: true
                                        color: matugenColors.primary
                                        elide: Text.ElideRight
                                        maximumLineCount: 1
                                    }
    
                                    // Delete button (X) on mouse hover
                                    Rectangle {
                                        implicitWidth: 30
                                        implicitHeight: 30
                                        radius: 6
                                        color: delMouseArea.containsMouse ? matugenColors.primary : "transparent"
                                        border.color: matugenColors.primary
                                        border.width: 1
                                        visible: itemHoverHandler.hovered || delMouseArea.containsMouse
    
                                        Text {
                                            anchors.centerIn: parent
                                            text: "✕"
                                            font.family: "Fira Sans Semibold"
                                            font.pixelSize: 15
                                            font.bold: true
                                            color: delMouseArea.containsMouse ? matugenColors.background : matugenColors.primary
                                        }
    
                                        MouseArea {
                                            id: delMouseArea
                                            anchors.fill: parent
                                            hoverEnabled: true
                                            onClicked: window.deleteItem(modelData)
                                        }
                                    }
                                }
                            }
    
                            // Empty State
                            Text {
                                anchors.centerIn: parent
                                visible: window.filteredItems.length === 0
                                text: window.searchQuery !== "" ? "Keine passenden Einträge gefunden" : "Zwischenablage ist leer"
                                font.family: "Fira Sans Semibold"
                                font.pixelSize: 15
                                color: matugenColors.primary
                            }
                        }
                    }
    
                    // RIGHT COLUMN: Quick View / Preview Pane
                    Rectangle {
                        Layout.preferredWidth: 360
                        Layout.fillHeight: true
                        color: matugenColors.surface_container
                        border.color: matugenColors.primary
                        border.width: 1
                        radius: 12
    
                        ColumnLayout {
                            anchors.fill: parent
                            anchors.margins: 14
                            spacing: 12
    
                            // Preview Header
                            RowLayout {
                                Layout.fillWidth: true
    
                                RowLayout {
                                    spacing: 8
                                    Image {
                                        source: "file://" + Quickshell.env("HOME") + "/.config/quickshell/shared/icons/theme.svg"
                                        width: 18
                                        height: 18
                                        fillMode: Image.PreserveAspectFit
                                        layer.enabled: true
                                        layer.effect: MultiEffect {
                                            colorization: 1.0
                                            colorizationColor: matugenColors.primary
                                        }
                                    }
                                    Text {
                                        text: "Vorschau"
                                        font.family: "Fira Sans Semibold"
                                        font.pixelSize: 16
                                        font.bold: true
                                        color: matugenColors.primary
                                    }
                                }
    
                                Item { Layout.fillWidth: true }
    
                                Text {
                                    visible: window.filteredItems.length > 0 && window.selectedIndex < window.filteredItems.length
                                    text: window.isImagePreview ? "Bild" : "Text"
                                    font.family: "Fira Sans Semibold"
                                    font.pixelSize: 12
                                    color: matugenColors.on_surface_variant
                                }
                            }
    
                            Rectangle {
                                Layout.fillWidth: true
                                height: 1
                                color: matugenColors.primary
                                opacity: 0.3
                            }
    
                            // Preview Content Area
                            Item {
                                Layout.fillWidth: true
                                Layout.fillHeight: true
    
                                // Image Preview
                                Item {
                                    anchors.fill: parent
                                    visible: window.isImagePreview
    
                                    Image {
                                        id: previewImage
                                        anchors.fill: parent
                                        fillMode: Image.PreserveAspectFit
                                        cache: false
                                        asynchronous: true
                                    }
                                }
    
                                // Text Preview
                                Flickable {
                                    anchors.fill: parent
                                    visible: !window.isImagePreview
                                    clip: true
                                    contentWidth: parent.width
                                    contentHeight: previewText.implicitHeight
    
                                    ScrollBar.vertical: ScrollBar {
                                        policy: ScrollBar.AsNeeded
                                    }
    
                                    Text {
                                        id: previewText
                                        width: parent.width
                                        text: window.currentPreviewText !== "" ? window.currentPreviewText : "Kein Eintrag ausgewählt"
                                        font.family: "Fira Sans Semibold"
                                        font.pixelSize: 14
                                        color: matugenColors.primary
                                        wrapMode: Text.WrapAnywhere
                                    }
                                }
                            }
                        }
                    }
                }
    
                // Footer Bar
                RowLayout {
                    Layout.fillWidth: true
    
                    Text {
                        text: window.filteredItems.length + " Eintrag" + (window.filteredItems.length === 1 ? "" : "e")
                        font.family: "Fira Sans Semibold"
                        font.pixelSize: 15
                        color: matugenColors.primary
                    }
    
                    Item { Layout.fillWidth: true }
    
                    Text {
                        text: "↑/↓ Navigieren | Enter Kopieren | Del Löschen | ESC Schließen"
                        font.family: "Fira Sans Semibold"
                        font.pixelSize: 15
                        color: matugenColors.primary
                    }
                }
            }
        }
    }
    Click to view preview_helper.sh
    #!/usr/bin/env bash
    # Quick Preview Helper for Quickshell Clipboard Manager
    
    item_id="$1"
    raw_text="$2"
    
    clean="$(echo "$raw_text" | head -n 1 | sed 's/^file:\/\///')"
    
    if [ -n "$clean" ] && [ -f "$clean" ]; then
        lower="$(echo "$clean" | tr '[:upper:]' '[:lower:]')"
    
        # 1. Image Files
        if [[ "$lower" =~ \.(png|jpg|jpeg|webp|svg|gif|bmp)$ ]]; then
            echo "TYPE:IMG_PATH"
            echo "$clean"
            exit 0
        fi
    
        # 2. PDF Files (Render 1st Page)
        if [[ "$lower" =~ \.pdf$ ]]; then
            out_img="/tmp/qs_pdf_preview_$$.png"
            rm -f /tmp/qs_pdf_preview_*.png
            pdftoppm -png -f 1 -l 1 -singlefile -scale-to 800 "$clean" "${out_img%.png}" 2>/dev/null
            if [ -f "$out_img" ]; then
                echo "TYPE:IMG_PATH"
                echo "$out_img"
                exit 0
            fi
        fi
    
        # 3. EML Mail Files
        if [[ "$lower" =~ \.eml$ ]]; then
            echo "TYPE:TEXT"
            echo "📧 E-Mail Vorschau ($clean):"
            echo "----------------------------------------"
            python3 -c '
    import email, sys
    from email import policy
    try:
        with open(sys.argv[1], "rb") as f:
            msg = email.message_from_binary_file(f, policy=policy.default)
        print(f"Betreff: {msg.get(\"subject\", \"\").strip()}")
        print(f"Von:     {msg.get(\"from\", \"\").strip()}")
        print(f"An:      {msg.get(\"to\", \"\").strip()}")
        print(f"Datum:   {msg.get(\"date\", \"\").strip()}")
        print("----------------------------------------")
        body = msg.get_body(preferencelist=("plain", "html"))
        if body:
            print(body.get_content()[:3000])
    except Exception as e:
        print(f"Fehler beim Lesen der EML-Datei: {e}")
    ' "$clean" 2>/dev/null
            exit 0
        fi
    
        # 4. Text, Code, Shell Scripts, Configs, Logs, Markdowns
        if [[ "$lower" =~ \.(sh|bash|zsh|py|js|ts|json|yaml|yml|toml|conf|ini|md|txt|log|csv|xml|html|css|qml|lua|c|cpp|h|rs|go|service|desktop)$ ]] || file "$clean" | grep -qi "text"; then
            echo "TYPE:TEXT"
            echo "📄 Dateivorschau ($clean):"
            echo "----------------------------------------"
            head -n 250 "$clean"
            exit 0
        fi
    fi
    
    # 5. Binary cliphist images
    if [[ "$raw_text" =~ \[image|binary\ data ]]; then
        out_img="/tmp/qs_clip_preview_$$.png"
        rm -f /tmp/qs_clip_preview_*.png
        cliphist decode "$item_id" > "$out_img" 2>/dev/null
        if [ -f "$out_img" ]; then
            echo "TYPE:IMG_PATH"
            echo "$out_img"
            exit 0
        fi
    fi
    
    # 6. Fallback: Full decoded text from cliphist
    echo "TYPE:TEXT"
    cliphist decode "$item_id"
    Image Image
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions