Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/LauncherPanel.qml

Raw
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Widgets
import "Util.js" as Util

FocusScope {
    id: root

    required property Theme theme
    required property Launcher launcherService
    required property SurfaceManager surfaces
    property var sessionState: ({
            "query": "",
            "selectedId": "",
            "scrollY": 0
        })
    readonly property string query: String(sessionState.query || "")
    property bool stateReady: false
    readonly property var panelState: launcherService ? Util.parseJson(launcherService.search(query), {
        "results": [],
        "selectedIndex": -1,
        "providerIntent": "app",
        "query": ""
    }) : ({
            "results": [],
            "selectedIndex": -1,
            "providerIntent": "app",
            "query": ""
        })
    readonly property var results: panelState.results || []
    readonly property int selectedIndex: selectionIndex()
    readonly property var selectedRow: results.length > 0 ? results[selectedIndex] : ({})
    readonly property var selectedActions: selectedRow.actions || []
    readonly property var selectedDetails: selectedRow.details || []
    readonly property int rowHeight: 52
    readonly property int rowSpacing: 4
    readonly property int rowStride: rowHeight + rowSpacing
    readonly property int rightWidth: Math.min(300, Math.max(260, Math.round(width * 0.36)))

    function updateState(values) {
        Object.assign(sessionState, values);
        // The owner may supply a plain JS object rather than a QtObject.
        sessionStateChanged();
    }

    function selectionIndex() {
        const index = results.findIndex(row => String(row.id) === String(sessionState.selectedId));
        return index >= 0 ? index : (results.length > 0 ? 0 : -1);
    }

    function reconcileSelection() {
        const id = selectedIndex >= 0 ? String(results[selectedIndex].id) : "";
        if (sessionState.selectedId !== id)
            updateState({
                "selectedId": id
            });
        if (stateReady)
            ensureSelectedVisible();
    }

    function setQuery(text) {
        updateState({
            "query": String(text || "")
        });
    }

    function rememberScroll() {
        if (stateReady && sessionState.scrollY !== resultsFlick.contentY)
            updateState({
                "scrollY": resultsFlick.contentY
            });
    }

    function ensureSelectedVisible() {
        if (selectedIndex < 0)
            return;
        const top = selectedIndex * rowStride;
        const bottom = top + rowHeight;
        if (top < resultsFlick.contentY)
            updateState({
                "scrollY": top
            });
        else if (bottom > resultsFlick.contentY + resultsFlick.height)
            updateState({
                "scrollY": bottom - resultsFlick.height
            });
    }

    function revealAction(item) {
        const top = item.mapToItem(detailContent, 0, 0).y;
        if (top < detailFlick.contentY)
            detailFlick.contentY = top;
        else if (top + item.height > detailFlick.contentY + detailFlick.height)
            detailFlick.contentY = top + item.height - detailFlick.height;
    }

    function close() {
        if (surfaces)
            surfaces.closePopout("launcher-activated");
    }

    function activateSelected() {
        if (!launcherService || results.length === 0)
            return;

        launcherService.activate(JSON.stringify(selectedRow));
        close();
    }

    function activateAction(index) {
        if (!launcherService || results.length === 0 || index < 0 || index >= selectedActions.length)
            return;

        launcherService.activateAction(JSON.stringify(selectedRow), index);
        close();
    }

    function select(index) {
        if (results.length === 0)
            return;

        const i = Math.max(0, Math.min(results.length - 1, Number(index)));
        updateState({
            "selectedId": String(results[i].id)
        });
        ensureSelectedVisible();
    }

    function keyAction(event) {
        if (event.modifiers & Qt.ControlModifier) {
            const n = Number(event.text);
            if (n >= 1 && n <= selectedActions.length) {
                event.accepted = true;
                if (!event.isAutoRepeat)
                    activateAction(n - 1);
            }
        }
    }

    width: parent ? parent.width : 0
    height: parent ? parent.height : 0
    focus: true
    onResultsChanged: selectionTimer.restart()

    Timer {
        id: selectionTimer
        interval: 0
        running: true
        onTriggered: {
            root.reconcileSelection();
            if (!root.stateReady) {
                root.stateReady = true;
                searchInput.input.forceActiveFocus();
            }
        }
    }

    Keys.onUpPressed: event => {
        event.accepted = true;
        select(selectedIndex - 1);
    }
    Keys.onDownPressed: event => {
        event.accepted = true;
        select(selectedIndex + 1);
    }
    Keys.onReturnPressed: event => {
        event.accepted = true;
        if (!event.isAutoRepeat)
            activateSelected();
    }
    Keys.onEnterPressed: event => {
        event.accepted = true;
        if (!event.isAutoRepeat)
            activateSelected();
    }
    Keys.onPressed: event => keyAction(event)

    Shortcut {
        sequences: ["Return", "Enter"]
        enabled: searchInput.input.activeFocus
        autoRepeat: false
        onActivated: root.activateSelected()
    }

    StableListModel {
        id: resultRows
        rows: root.results
    }

    StableListModel {
        id: actionRows
        rows: root.selectedActions
    }

    Row {
        anchors.fill: parent
        anchors.margins: 1

        Item {
            width: Math.max(1, parent.width - root.rightWidth)
            height: parent.height

            PopupTextField {
                id: searchInput

                theme: root.theme
                anchors.left: parent.left
                anchors.right: parent.right
                anchors.top: parent.top
                anchors.margins: 8
                height: 38
                icon: ""
                placeholderText: "search apps"
                text: root.query
                onEdited: text => root.setQuery(text)
                Keys.onUpPressed: event => {
                    event.accepted = true;
                    root.select(root.selectedIndex - 1);
                }
                Keys.onDownPressed: event => {
                    event.accepted = true;
                    root.select(root.selectedIndex + 1);
                }
                Keys.onPressed: event => root.keyAction(event)
            }

            Flickable {
                id: resultsFlick
                objectName: "launcherResultList"

                anchors.left: parent.left
                anchors.right: parent.right
                anchors.top: searchInput.bottom
                anchors.bottom: parent.bottom
                anchors.margins: 8
                contentHeight: resultColumn.implicitHeight
                contentY: Math.max(0, Math.min(Number(root.sessionState.scrollY || 0), contentHeight - height))
                onContentYChanged: root.rememberScroll()
                clip: true
                boundsBehavior: Flickable.StopAtBounds

                Column {
                    id: resultColumn

                    width: resultsFlick.width
                    spacing: root.rowSpacing

                    Repeater {
                        model: resultRows

                        delegate: PopupCard {
                            id: rrow

                            required property int index
                            required property var rowData
                            readonly property var modelData: rowData
                            readonly property bool isSelected: index === root.selectedIndex

                            theme: root.theme
                            padding: 0
                            active: isSelected
                            hoverable: true
                            externallyPressed: rrowArea.pressed
                            activeFocusOnTab: isSelected
                            Accessible.role: Accessible.ListItem
                            Accessible.name: String(modelData.title || modelData.id || "app")
                            Accessible.selected: isSelected
                            onActiveFocusChanged: if (activeFocus)
                                root.select(index)
                            width: parent.width
                            height: root.rowHeight

                            Item {
                                id: iconSlot

                                anchors.left: parent.left
                                anchors.leftMargin: 11
                                anchors.verticalCenter: parent.verticalCenter
                                width: 30
                                height: 30

                                Rectangle {
                                    anchors.fill: parent
                                    radius: 8
                                    color: root.theme ? root.theme.content.backdrop : "#121212"
                                }

                                IconImage {
                                    id: appIcon

                                    anchors.fill: parent
                                    anchors.margins: 4
                                    visible: appIcon.status === Image.Ready
                                    source: String(rrow.modelData.icon || "").length > 0 ? Quickshell.iconPath(String(rrow.modelData.icon), true) : ""
                                }

                                Text {
                                    anchors.centerIn: parent
                                    visible: !appIcon.visible
                                    text: String(rrow.modelData.glyph || "")
                                    font.family: "Symbols Nerd Font Mono"
                                    font.pixelSize: 17
                                    color: root.theme ? root.theme.ui.accent : "#d86dd8"
                                }
                            }

                            Column {
                                anchors.left: iconSlot.right
                                anchors.leftMargin: 12
                                anchors.right: ordinal.left
                                anchors.rightMargin: 10
                                anchors.verticalCenter: parent.verticalCenter
                                spacing: 2

                                NuguText {
                                    theme: root.theme
                                    selected: rrow.isSelected
                                    text: rrow.modelData.title || rrow.modelData.id || "app"
                                    width: parent.width
                                }

                                NuguText {
                                    theme: root.theme
                                    variant: "meta"
                                    selected: rrow.isSelected
                                    text: rrow.modelData.subtitle || rrow.modelData.comment || rrow.modelData.desktopId || "app"
                                    width: parent.width
                                    opacity: rrow.isSelected ? 0.82 : 0.72
                                }
                            }

                            NuguText {
                                id: ordinal

                                anchors.right: parent.right
                                anchors.rightMargin: 12
                                anchors.verticalCenter: parent.verticalCenter
                                theme: root.theme
                                variant: "mono"
                                selected: rrow.isSelected
                                text: rrow.isSelected ? "enter" : ""
                                width: 42
                                horizontalAlignment: Text.AlignRight
                                opacity: rrow.isSelected ? 0.8 : 0.52
                            }

                            MouseArea {
                                id: rrowArea
                                anchors.fill: parent
                                hoverEnabled: true
                                cursorShape: Qt.PointingHandCursor
                                onClicked: {
                                    root.select(rrow.index);
                                    rrow.forceActiveFocus();
                                }
                                onDoubleClicked: root.activateSelected()
                            }
                        }
                    }
                }
            }
        }

        Item {
            width: root.rightWidth
            height: parent.height

            PopupDivider {
                anchors.left: parent.left
                anchors.top: parent.top
                anchors.bottom: parent.bottom
                theme: root.theme
            }

            Flickable {
                id: detailFlick
                objectName: "launcherDetails"

                anchors.fill: parent
                anchors.margins: 22
                contentHeight: detailContent.implicitHeight
                clip: true
                boundsBehavior: Flickable.StopAtBounds

                Column {
                    id: detailContent

                    width: detailFlick.width
                    spacing: 6

                    Row {
                        width: parent.width
                        height: 58
                        spacing: 14

                        Item {
                            width: 56
                            height: 56

                            Rectangle {
                                anchors.fill: parent
                                radius: 15
                                color: root.theme ? root.theme.content.backdrop : "#121212"
                            }

                            IconImage {
                                id: detailIcon

                                anchors.fill: parent
                                anchors.margins: 8
                                visible: detailIcon.status === Image.Ready
                                source: String(root.selectedRow.icon || "").length > 0 ? Quickshell.iconPath(String(root.selectedRow.icon), true) : ""
                            }

                            Text {
                                anchors.centerIn: parent
                                visible: !detailIcon.visible
                                text: String(root.selectedRow.glyph || "")
                                font.family: "Symbols Nerd Font Mono"
                                font.pixelSize: 27
                                color: root.theme ? root.theme.ui.accent : "#d86dd8"
                            }
                        }

                        Column {
                            width: Math.max(1, parent.width - 70)
                            anchors.verticalCenter: parent.verticalCenter
                            spacing: 3

                            NuguText {
                                theme: root.theme
                                variant: "title"
                                text: root.selectedRow.title || "no apps"
                                width: parent.width
                                font.pixelSize: 22
                            }

                            NuguText {
                                theme: root.theme
                                variant: "mono"
                                text: root.selectedRow.genericName || root.selectedRow.desktopId || "app"
                                width: parent.width
                                opacity: 0.62
                            }
                        }
                    }

                    NuguText {
                        theme: root.theme
                        variant: "mono"
                        text: "actions"
                        width: parent.width
                        topPadding: 24
                        bottomPadding: 8
                        opacity: 0.68
                    }

                    PopupButton {
                        id: openButton

                        theme: root.theme
                        width: parent.width
                        height: 42
                        text: "open"
                        role: "primary"
                        enabled: root.results.length > 0 && !!root.launcherService
                        onActiveFocusChanged: if (activeFocus)
                            root.revealAction(openButton)
                        onClicked: root.activateSelected()
                    }

                    Repeater {
                        model: actionRows

                        delegate: PopupButton {
                            id: actionRow

                            required property int index
                            required property var rowData
                            readonly property var modelData: rowData

                            theme: root.theme
                            width: parent.width
                            height: 42
                            text: modelData.title || modelData.id || "action"
                            onActiveFocusChanged: if (activeFocus)
                                root.revealAction(actionRow)
                            onClicked: root.activateAction(index)
                        }
                    }

                    NuguText {
                        theme: root.theme
                        variant: "mono"
                        text: "desktop entry"
                        width: parent.width
                        topPadding: 24
                        bottomPadding: 8
                        opacity: 0.68
                    }

                    PopupCard {
                        theme: root.theme
                        padding: 0
                        width: parent.width
                        height: detailColumn.implicitHeight + 20

                        Column {
                            id: detailColumn

                            anchors.left: parent.left
                            anchors.right: parent.right
                            anchors.top: parent.top
                            anchors.margins: 10
                            spacing: 6

                            Repeater {
                                model: root.selectedDetails

                                delegate: Row {
                                    id: detailRow

                                    required property var modelData

                                    width: parent.width
                                    spacing: 10

                                    NuguText {
                                        theme: root.theme
                                        variant: "meta"
                                        text: detailRow.modelData.label || ""
                                        width: 70
                                        opacity: 0.62
                                    }

                                    NuguText {
                                        theme: root.theme
                                        variant: "meta"
                                        text: detailRow.modelData.value || "-"
                                        width: Math.max(1, parent.width - 80)
                                        horizontalAlignment: Text.AlignRight
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}