repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/LauncherPanel.qml
Rawpragma 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
}
}
}
}
}
}
}
}
}
}