cleaned and optimized launcher + added actionsview on listview items on launcher. TAB to open it or right click. Added typing animation for Crypto.qml and also anchored the icon on the left so it doesnt keep moving around when changing sizes.
This commit is contained in:
@@ -16,7 +16,9 @@ ModuleChip {
|
|||||||
id: configWriter
|
id: configWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
width: chipRow.implicitWidth + 24
|
width: cryptoIcon.visible
|
||||||
|
? (12 + cryptoIcon.implicitWidth + 7 + chipRow.implicitWidth + 12)
|
||||||
|
: (chipRow.implicitWidth + 24)
|
||||||
radius: panelRadius
|
radius: panelRadius
|
||||||
|
|
||||||
chipColor: t.cryptoBg
|
chipColor: t.cryptoBg
|
||||||
@@ -152,6 +154,7 @@ ModuleChip {
|
|||||||
if (root.localPairs.length <= 1) return
|
if (root.localPairs.length <= 1) return
|
||||||
root.activeCoinIndex = (root.activeCoinIndex - 1 + root.localPairs.length) % root.localPairs.length
|
root.activeCoinIndex = (root.activeCoinIndex - 1 + root.localPairs.length) % root.localPairs.length
|
||||||
root._applyFromCache()
|
root._applyFromCache()
|
||||||
|
root._restartTyping()
|
||||||
rotateTimer.restart()
|
rotateTimer.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +162,7 @@ ModuleChip {
|
|||||||
if (root.localPairs.length <= 1) return
|
if (root.localPairs.length <= 1) return
|
||||||
root.activeCoinIndex = (root.activeCoinIndex + 1) % root.localPairs.length
|
root.activeCoinIndex = (root.activeCoinIndex + 1) % root.localPairs.length
|
||||||
root._applyFromCache()
|
root._applyFromCache()
|
||||||
|
root._restartTyping()
|
||||||
rotateTimer.restart()
|
rotateTimer.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +188,51 @@ ModuleChip {
|
|||||||
sparklineData = entry.sparkline || []
|
sparklineData = entry.sparkline || []
|
||||||
change7d = entry.change7d || 0
|
change7d = entry.change7d || 0
|
||||||
hasError = false
|
hasError = false
|
||||||
|
root._restartTyping()
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool _typing: false
|
||||||
|
property int _typedLabel: 0
|
||||||
|
property int _typedValue: 0
|
||||||
|
property int _typedIcon: 0
|
||||||
|
|
||||||
|
function _restartTyping() {
|
||||||
|
typeAnim.stop()
|
||||||
|
_typing = true
|
||||||
|
_typedLabel = 0
|
||||||
|
_typedValue = 0
|
||||||
|
_typedIcon = 0
|
||||||
|
typeAnimLabel.to = (root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": ").length
|
||||||
|
typeAnimValue.to = (root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)).length
|
||||||
|
typeAnimIcon.to = (root.price >= 0 ? 1 : 0)
|
||||||
|
typeAnim.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: typeAnim
|
||||||
|
onStopped: { root._typing = false }
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
id: typeAnimLabel
|
||||||
|
target: root
|
||||||
|
property: "_typedLabel"
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
id: typeAnimValue
|
||||||
|
target: root
|
||||||
|
property: "_typedValue"
|
||||||
|
duration: 200
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
id: typeAnimIcon
|
||||||
|
target: root
|
||||||
|
property: "_typedIcon"
|
||||||
|
duration: 50
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchPrice() {
|
function fetchPrice() {
|
||||||
@@ -291,9 +340,26 @@ ModuleChip {
|
|||||||
onLocalRefreshSecChanged: refreshTimer.restart()
|
onLocalRefreshSecChanged: refreshTimer.restart()
|
||||||
onLocalRotateSecChanged: rotateTimer.restart()
|
onLocalRotateSecChanged: rotateTimer.restart()
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: cryptoIcon
|
||||||
|
visible: root.localShowIcon
|
||||||
|
text: ""
|
||||||
|
color: t.cryptoIcon
|
||||||
|
font.pixelSize: 14
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 12
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
id: chipRow
|
id: chipRow
|
||||||
anchors.centerIn: parent
|
anchors {
|
||||||
|
left: cryptoIcon.visible ? cryptoIcon.right : parent.left
|
||||||
|
leftMargin: cryptoIcon.visible ? 7 : 12
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
SequentialAnimation on opacity {
|
SequentialAnimation on opacity {
|
||||||
@@ -310,34 +376,61 @@ ModuleChip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Item {
|
||||||
visible: root.localShowIcon
|
implicitWidth: labelSizer.implicitWidth
|
||||||
text: ""
|
implicitHeight: labelSizer.implicitHeight
|
||||||
color: t.cryptoIcon
|
|
||||||
font.pixelSize: 14
|
Text {
|
||||||
Layout.alignment: Qt.AlignVCenter
|
id: labelSizer
|
||||||
Layout.rightMargin: 7
|
text: root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": "
|
||||||
|
color: "transparent"
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root._typing
|
||||||
|
? (root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": ").substring(0, root._typedLabel)
|
||||||
|
: root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": "
|
||||||
|
color: t.cryptoLabel
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
anchors.fill: labelSizer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
implicitWidth: valueSizer.implicitWidth
|
||||||
|
implicitHeight: valueSizer.implicitHeight
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: valueSizer
|
||||||
|
text: root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)
|
||||||
|
color: "transparent"
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root._typing
|
||||||
|
? (root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)).substring(0, root._typedValue)
|
||||||
|
: root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)
|
||||||
|
color: t.cryptoValue
|
||||||
|
font.pixelSize: 13
|
||||||
|
font.bold: true
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
anchors.fill: valueSizer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": "
|
visible: root._typing || root.price >= 0
|
||||||
color: t.cryptoLabel
|
text: root._typing
|
||||||
font.pixelSize: 12
|
? (root.price >= 0 ? (root.change24h >= 0 ? "▲" : "▼") : "").substring(0, root._typedIcon)
|
||||||
font.bold: true
|
: (root.price >= 0 ? (root.change24h >= 0 ? "▲" : "▼") : "")
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: root.hasError ? "err" : root.price < 0 ? "…" : root.compactPrice(root.price)
|
|
||||||
color: t.cryptoValue
|
|
||||||
font.pixelSize: 13
|
|
||||||
font.bold: true
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
visible: root.price >= 0
|
|
||||||
text: root.change24h >= 0 ? "▲" : "▼"
|
|
||||||
color: root.change24h >= 0 ? (t.cryptoUp) : (t.cryptoDown)
|
color: root.change24h >= 0 ? (t.cryptoUp) : (t.cryptoDown)
|
||||||
font.pixelSize: 9
|
font.pixelSize: 9
|
||||||
font.bold: true
|
font.bold: true
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ PanelWindow {
|
|||||||
property bool popupOpen: false
|
property bool popupOpen: false
|
||||||
property real popupCenterX: -1
|
property real popupCenterX: -1
|
||||||
|
|
||||||
function _doOpen() { hideTimer.stop(); visible = true; popupOpen = true }
|
function _doOpen() {
|
||||||
|
hideTimer.stop()
|
||||||
|
if (chipRoot) {
|
||||||
|
popupCenterX = chipRoot.mapToItem(null, 0, 0).x + chipRoot.width / 2
|
||||||
|
}
|
||||||
|
visible = true; popupOpen = true
|
||||||
|
}
|
||||||
function open() {
|
function open() {
|
||||||
_doOpen()
|
_doOpen()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,8 +118,6 @@ QtObject {
|
|||||||
// ────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────
|
||||||
// LAUNCHER
|
// LAUNCHER
|
||||||
// ────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────
|
||||||
property string launcherScript: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher.sh"
|
|
||||||
property string launcherCacheDir: "/tmp/qs_launcher"
|
|
||||||
property bool launcherCloseOnClickOutside: true
|
property bool launcherCloseOnClickOutside: true
|
||||||
property bool launcherSelectFirst: false
|
property bool launcherSelectFirst: false
|
||||||
property bool launcherMipmapIcons: false
|
property bool launcherMipmapIcons: false
|
||||||
@@ -142,7 +140,7 @@ QtObject {
|
|||||||
//
|
//
|
||||||
// This list is the *default*; pairs can be added/removed live in the
|
// This list is the *default*; pairs can be added/removed live in the
|
||||||
// Settings popup and those changes are persisted across restarts.
|
// Settings popup and those changes are persisted across restarts.
|
||||||
property var cryptoPairs: [{ coin: "bitcoin", currency: "usd" }, { coin: "ethereum", currency: "eur" }, { coin: "monero", currency: "usd" }, { coin: "bitcoin", currency: "eur" }, { coin: "bitcoin", currency: "brl" }, { coin: "ethereum", currency: "usd" }]
|
property var cryptoPairs: [{ coin: "bitcoin", currency: "usd" }, { coin: "ethereum", currency: "eur" }, { coin: "monero", currency: "usd" }, { coin: "bitcoin", currency: "eur" }, { coin: "bitcoin", currency: "brl" }, { coin: "ethereum", currency: "usd" }, { coin: "zcash", currency: "usd" }]
|
||||||
|
|
||||||
// ── Compact label decimals ────────────────────────────────────────────────
|
// ── Compact label decimals ────────────────────────────────────────────────
|
||||||
// 0 → "80k"
|
// 0 → "80k"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import QtQuick.Layouts
|
|||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
import "../config" as Cfg
|
import "../config" as Cfg
|
||||||
import "../components"
|
import "../components"
|
||||||
@@ -21,6 +21,78 @@ PanelWindow {
|
|||||||
|
|
||||||
readonly property int launcherGap: 12
|
readonly property int launcherGap: 12
|
||||||
|
|
||||||
|
property var allItems: []
|
||||||
|
property var favoriteIds: []
|
||||||
|
property var accessCounts: ({})
|
||||||
|
property bool _loaded: false
|
||||||
|
|
||||||
|
readonly property string _statePath: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher_state.json"
|
||||||
|
|
||||||
|
function buildItems() {
|
||||||
|
const apps = DesktopEntries.applications.values
|
||||||
|
const items = []
|
||||||
|
for (let i = 0; i < apps.length; i++) {
|
||||||
|
const e = apps[i]
|
||||||
|
items.push({
|
||||||
|
name: e.name,
|
||||||
|
exec: e.execString,
|
||||||
|
id: e.id,
|
||||||
|
icon: e.icon,
|
||||||
|
entry: e,
|
||||||
|
actions: e.actions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
items.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
||||||
|
root.allItems = items
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: buildItems()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: DesktopEntries
|
||||||
|
function onApplicationsChanged() { root.buildItems() }
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: stateFile
|
||||||
|
path: root._statePath
|
||||||
|
printErrors: false
|
||||||
|
watchChanges: true
|
||||||
|
onFileChanged: reload()
|
||||||
|
onAdapterUpdated: writeAdapter()
|
||||||
|
adapter: JsonAdapter {
|
||||||
|
id: stateAdapter
|
||||||
|
property list<string> favorites: []
|
||||||
|
property var access: ({})
|
||||||
|
}
|
||||||
|
onLoaded: {
|
||||||
|
root.favoriteIds = stateAdapter.favorites
|
||||||
|
root.accessCounts = stateAdapter.access
|
||||||
|
root._loaded = true
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root._loaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFavorite(itemId) {
|
||||||
|
const arr = root.favoriteIds.slice()
|
||||||
|
const idx = arr.indexOf(itemId)
|
||||||
|
if (idx >= 0)
|
||||||
|
arr.splice(idx, 1)
|
||||||
|
else
|
||||||
|
arr.push(itemId)
|
||||||
|
root.favoriteIds = arr
|
||||||
|
stateAdapter.favorites = root.favoriteIds
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementAccess(itemId) {
|
||||||
|
const counts = Object.assign({}, root.accessCounts)
|
||||||
|
counts[itemId] = (counts[itemId] || 0) + 1
|
||||||
|
root.accessCounts = counts
|
||||||
|
stateAdapter.access = root.accessCounts
|
||||||
|
}
|
||||||
|
|
||||||
GlobalShortcut {
|
GlobalShortcut {
|
||||||
id: launcherShortcut
|
id: launcherShortcut
|
||||||
name: "toggle"
|
name: "toggle"
|
||||||
@@ -30,13 +102,14 @@ PanelWindow {
|
|||||||
property bool _isOpen: false
|
property bool _isOpen: false
|
||||||
readonly property bool isOpen: _isOpen
|
readonly property bool isOpen: _isOpen
|
||||||
property bool _keyboardNav: false
|
property bool _keyboardNav: false
|
||||||
property var allItems: []
|
property string _expandingActions: ""
|
||||||
property bool loading: false
|
property string _savedExpandId: ""
|
||||||
property int lastCacheUpdate: 0
|
property real _savedContentY: 0
|
||||||
property var favoriteIds: []
|
|
||||||
property var accessCounts: ({})
|
|
||||||
readonly property string statePath: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher_state.json"
|
|
||||||
|
|
||||||
|
property bool _pendingRestore: false
|
||||||
|
property bool _inRestore: false
|
||||||
|
|
||||||
|
property string expandedItemId: ""
|
||||||
|
|
||||||
readonly property int defaultIndex: Cfg.Config.launcherSelectFirst ? 0 : -1
|
readonly property int defaultIndex: Cfg.Config.launcherSelectFirst ? 0 : -1
|
||||||
|
|
||||||
@@ -52,15 +125,33 @@ PanelWindow {
|
|||||||
let rest = filtered.filter(i => !root.favoriteIds.includes(i.id))
|
let rest = filtered.filter(i => !root.favoriteIds.includes(i.id))
|
||||||
rest.sort((a, b) => (root.accessCounts[b.id] || 0) - (root.accessCounts[a.id] || 0))
|
rest.sort((a, b) => (root.accessCounts[b.id] || 0) - (root.accessCounts[a.id] || 0))
|
||||||
const result = []
|
const result = []
|
||||||
|
|
||||||
|
function pushItem(item) {
|
||||||
|
result.push(Object.assign({}, item, { isHeader: false, isAction: false }))
|
||||||
|
if (root.expandedItemId && item.id === root.expandedItemId && item.actions && item.actions.length > 0) {
|
||||||
|
for (let j = 0; j < item.actions.length; j++) {
|
||||||
|
const a = item.actions[j]
|
||||||
|
result.push({
|
||||||
|
isHeader: false,
|
||||||
|
isAction: true,
|
||||||
|
name: a.name,
|
||||||
|
id: item.id + ":" + a.id,
|
||||||
|
icon: a.icon,
|
||||||
|
actionObj: a
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (favs.length > 0) {
|
if (favs.length > 0) {
|
||||||
result.push({ isHeader: true, section: "Favorites" })
|
result.push({ isHeader: true, section: "Favorites" })
|
||||||
for (let i = 0; i < favs.length; i++)
|
for (let i = 0; i < favs.length; i++)
|
||||||
result.push(Object.assign({}, favs[i], { isHeader: false }))
|
pushItem(favs[i])
|
||||||
}
|
}
|
||||||
if (rest.length > 0) {
|
if (rest.length > 0) {
|
||||||
result.push({ isHeader: true, section: "All Applications" })
|
result.push({ isHeader: true, section: "All Applications" })
|
||||||
for (let i = 0; i < rest.length; i++)
|
for (let i = 0; i < rest.length; i++)
|
||||||
result.push(Object.assign({}, rest[i], { isHeader: false }))
|
pushItem(rest[i])
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -88,16 +179,16 @@ PanelWindow {
|
|||||||
closeTimer.stop()
|
closeTimer.stop()
|
||||||
visible = true
|
visible = true
|
||||||
_isOpen = true
|
_isOpen = true
|
||||||
|
expandedItemId = ""
|
||||||
searchInput.text = ""
|
searchInput.text = ""
|
||||||
resultsList.currentIndex = root.defaultIndex
|
resultsList.currentIndex = root.defaultIndex
|
||||||
Qt.callLater(() => resultsList.positionViewAtBeginning())
|
Qt.callLater(() => resultsList.positionViewAtBeginning())
|
||||||
loadItems()
|
|
||||||
loadState()
|
|
||||||
Qt.callLater(() => searchInput.forceActiveFocus())
|
Qt.callLater(() => searchInput.forceActiveFocus())
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
_isOpen = false
|
_isOpen = false
|
||||||
|
expandedItemId = ""
|
||||||
closeTimer.restart()
|
closeTimer.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,108 +199,18 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadItems() {
|
|
||||||
loading = true
|
|
||||||
var now = Date.now()
|
|
||||||
var staleMs = 300000
|
|
||||||
if (now - lastCacheUpdate < staleMs) {
|
|
||||||
readCache()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
readCache()
|
|
||||||
lastCacheUpdate = now
|
|
||||||
updateProc.command = [Cfg.Config.launcherScript, "--update-only"]
|
|
||||||
updateProc.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function readCache() {
|
|
||||||
const path = Cfg.Config.launcherCacheDir + "/drun.txt"
|
|
||||||
dataProc.rawOutput = ""
|
|
||||||
dataProc.command = ["cat", path]
|
|
||||||
dataProc.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function shellQuote(str) {
|
|
||||||
return "'" + str.replace(/'/g, "'\\''") + "'"
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseExec(str) {
|
|
||||||
const args = []
|
|
||||||
let current = ""
|
|
||||||
let inSingle = false
|
|
||||||
let inDouble = false
|
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
const ch = str[i]
|
|
||||||
|
|
||||||
if (inSingle) {
|
|
||||||
if (ch === "'") inSingle = false
|
|
||||||
else current += ch
|
|
||||||
} else if (inDouble) {
|
|
||||||
if (ch === '"') inDouble = false
|
|
||||||
else current += ch
|
|
||||||
} else if (ch === "'") {
|
|
||||||
inSingle = true
|
|
||||||
} else if (ch === '"') {
|
|
||||||
inDouble = true
|
|
||||||
} else if (/\s/.test(ch)) {
|
|
||||||
if (current.length > 0) {
|
|
||||||
args.push(current)
|
|
||||||
current = ""
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current += ch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (current.length > 0) args.push(current)
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadState() {
|
|
||||||
stateReadProc.command = ["sh", "-c", "cat " + shellQuote(statePath) + " 2>/dev/null || true"]
|
|
||||||
stateReadProc.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveState() {
|
|
||||||
const data = JSON.stringify({ favorites: favoriteIds, access: accessCounts })
|
|
||||||
stateWriteProc.command = ["sh", "-c",
|
|
||||||
"printf '%s' " + shellQuote(data) + " > " + shellQuote(statePath)]
|
|
||||||
stateWriteProc.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleFavorite(itemId) {
|
|
||||||
const arr = favoriteIds.slice()
|
|
||||||
const idx = arr.indexOf(itemId)
|
|
||||||
if (idx >= 0) {
|
|
||||||
arr.splice(idx, 1)
|
|
||||||
} else {
|
|
||||||
arr.push(itemId)
|
|
||||||
}
|
|
||||||
favoriteIds = arr
|
|
||||||
saveState()
|
|
||||||
}
|
|
||||||
|
|
||||||
function incrementAccess(itemId) {
|
|
||||||
const counts = Object.assign({}, accessCounts)
|
|
||||||
counts[itemId] = (counts[itemId] || 0) + 1
|
|
||||||
accessCounts = counts
|
|
||||||
saveState()
|
|
||||||
}
|
|
||||||
|
|
||||||
function launch(item) {
|
function launch(item) {
|
||||||
if (!item || !item.exec) return
|
if (!item || !item.entry) return
|
||||||
|
item.entry.execute()
|
||||||
|
console.log("Launching: " + item.name)
|
||||||
|
root.incrementAccess(item.id)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
let cleanExec = item.exec.replace(/%[fFuUik]/g, "").trim()
|
function executeAction(item) {
|
||||||
const logPath = "/tmp/qml_launch.log"
|
if (!item || !item.actionObj) return
|
||||||
|
item.actionObj.execute()
|
||||||
const args = parseExec(cleanExec)
|
console.log("Executing action: " + item.name)
|
||||||
const cmd = args.map(a => shellQuote(a)).join(" ") + " > " + shellQuote(logPath) + " 2>&1 < /dev/null &"
|
|
||||||
|
|
||||||
launchProc.command = ["sh", "-c", cmd]
|
|
||||||
launchProc.running = true
|
|
||||||
|
|
||||||
console.log("Launching: " + cleanExec)
|
|
||||||
incrementAccess(item.id)
|
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,61 +229,9 @@ PanelWindow {
|
|||||||
|
|
||||||
PopupHideTimer {
|
PopupHideTimer {
|
||||||
id: closeTimer
|
id: closeTimer
|
||||||
onTriggered: {
|
onTriggered: root.visible = false
|
||||||
root.visible = false
|
|
||||||
root.allItems = []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: updateProc
|
|
||||||
onRunningChanged: if (!running) root.readCache()
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: dataProc
|
|
||||||
property string rawOutput: ""
|
|
||||||
stdout: SplitParser { onRead: data => dataProc.rawOutput += data + "\n" }
|
|
||||||
onRunningChanged: {
|
|
||||||
if (!running && rawOutput) {
|
|
||||||
const lines = rawOutput.trim().split("\n")
|
|
||||||
const items = []
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const p = lines[i].split("\t")
|
|
||||||
if (p.length >= 3) items.push({ name: p[0], exec: p[1], id: p[2], icon: p[3] || "" })
|
|
||||||
}
|
|
||||||
root.allItems = items
|
|
||||||
root.loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process { id: launchProc }
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: stateReadProc
|
|
||||||
property string rawData: ""
|
|
||||||
stdout: SplitParser { onRead: data => stateReadProc.rawData += data + "\n" }
|
|
||||||
onRunningChanged: {
|
|
||||||
if (!running) {
|
|
||||||
if (rawData.trim()) {
|
|
||||||
try {
|
|
||||||
const state = JSON.parse(rawData.trim())
|
|
||||||
if (Array.isArray(state.favorites))
|
|
||||||
root.favoriteIds = state.favorites
|
|
||||||
if (state.access && typeof state.access === "object")
|
|
||||||
root.accessCounts = state.access
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Failed to parse launcher state: " + e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rawData = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process { id: stateWriteProc }
|
|
||||||
|
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -355,12 +304,48 @@ PanelWindow {
|
|||||||
verticalAlignment: TextInput.AlignVCenter
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
selectionColor: t.launcherAccent
|
selectionColor: t.launcherAccent
|
||||||
|
|
||||||
onTextChanged: resultsList.currentIndex = root.defaultIndex
|
onTextChanged: {
|
||||||
|
resultsList.currentIndex = root.defaultIndex
|
||||||
|
root.expandedItemId = ""
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
const idx = root.skipHeader(Math.max(0, resultsList.currentIndex), 1)
|
const idx = root.skipHeader(Math.max(0, resultsList.currentIndex), 1)
|
||||||
if (idx >= 0)
|
if (idx < 0) return
|
||||||
root.launch(root.filteredItems[idx])
|
const item = root.filteredItems[idx]
|
||||||
|
if (item.isAction)
|
||||||
|
root.executeAction(item)
|
||||||
|
else
|
||||||
|
root.launch(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onTabPressed: (event) => {
|
||||||
|
event.accepted = true
|
||||||
|
root._keyboardNav = true
|
||||||
|
const idx = root.skipHeader(Math.max(0, resultsList.currentIndex), 1)
|
||||||
|
if (idx < 0) return
|
||||||
|
const item = root.filteredItems[idx]
|
||||||
|
if (!item || item.isHeader || item.isAction) return
|
||||||
|
root._savedContentY = resultsList.contentY
|
||||||
|
root._savedExpandId = item.id
|
||||||
|
root._expandingActions = item.id
|
||||||
|
if (root.expandedItemId === item.id) {
|
||||||
|
root._pendingRestore = true
|
||||||
|
root.expandedItemId = ""
|
||||||
|
} else if (item.actions && item.actions.length > 0) {
|
||||||
|
root._pendingRestore = true
|
||||||
|
root.expandedItemId = item.id
|
||||||
|
}
|
||||||
|
Qt.callLater(() => {
|
||||||
|
for (let i = 0; i < resultsList.count; i++) {
|
||||||
|
const it = root.filteredItems[i]
|
||||||
|
if (it.id === item.id && !it.isAction && !it.isHeader) {
|
||||||
|
resultsList.currentIndex = i
|
||||||
|
resultsList.positionViewAtIndex(i, ListView.Contain)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onDownPressed: {
|
Keys.onDownPressed: {
|
||||||
@@ -432,19 +417,51 @@ PanelWindow {
|
|||||||
flickableDirection: Flickable.VerticalFlick
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
|
||||||
onModelChanged: {
|
onModelChanged: {
|
||||||
currentIndex = root.defaultIndex
|
if (root._expandingActions !== "") {
|
||||||
for (let i = 0; i < count; i++) {
|
root._expandingActions = ""
|
||||||
const md = root.filteredItems[i]
|
if (root._savedExpandId !== "") {
|
||||||
if (md && !md.isHeader) {
|
Qt.callLater(() => {
|
||||||
currentIndex = i
|
for (let i = 0; i < resultsList.count; i++) {
|
||||||
break
|
const it = root.filteredItems[i]
|
||||||
|
if (it.id === root._savedExpandId && !it.isAction && !it.isHeader) {
|
||||||
|
resultsList.currentIndex = i
|
||||||
|
resultsList.positionViewAtIndex(i, ListView.Contain)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root._pendingRestore = false
|
||||||
|
root._savedExpandId = ""
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
root._pendingRestore = false
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
currentIndex = root.defaultIndex
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const md = root.filteredItems[i]
|
||||||
|
if (md && !md.isHeader && !md.isAction) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Qt.callLater(() => positionViewAtBeginning())
|
||||||
}
|
}
|
||||||
Qt.callLater(() => positionViewAtBeginning())
|
|
||||||
}
|
}
|
||||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: resultsList
|
||||||
|
function onContentYChanged() {
|
||||||
|
if (root._pendingRestore && !root._inRestore && Math.abs(resultsList.contentY - root._savedContentY) > 1) {
|
||||||
|
root._inRestore = true
|
||||||
|
resultsList.contentY = root._savedContentY
|
||||||
|
root._inRestore = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
|
id: itemDelegate
|
||||||
width: resultsList.width
|
width: resultsList.width
|
||||||
height: 38
|
height: 38
|
||||||
radius: modelData.isHeader ? 0 : 8
|
radius: modelData.isHeader ? 0 : 8
|
||||||
@@ -452,6 +469,7 @@ PanelWindow {
|
|||||||
: (starHovered || hovered) ? t.launcherHover
|
: (starHovered || hovered) ? t.launcherHover
|
||||||
: (resultsList.currentIndex === index && index !== -1
|
: (resultsList.currentIndex === index && index !== -1
|
||||||
? t.launcherSelected : "transparent")
|
? t.launcherSelected : "transparent")
|
||||||
|
clip: modelData.isAction
|
||||||
property bool hovered: false
|
property bool hovered: false
|
||||||
property bool starHovered: false
|
property bool starHovered: false
|
||||||
|
|
||||||
@@ -467,11 +485,12 @@ PanelWindow {
|
|||||||
RowLayout {
|
RowLayout {
|
||||||
visible: !modelData.isHeader
|
visible: !modelData.isHeader
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: 12
|
anchors.leftMargin: modelData.isAction ? 28 : 12
|
||||||
anchors.rightMargin: 12
|
anchors.rightMargin: 12
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
visible: !modelData.isAction
|
||||||
width: 22; height: 22
|
width: 22; height: 22
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
@@ -496,14 +515,31 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: modelData.name
|
visible: modelData.isAction
|
||||||
color: t.launcherText
|
text: "›"
|
||||||
|
color: t.launcherDim
|
||||||
font.pixelSize: 14
|
font.pixelSize: 14
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.name
|
||||||
|
color: modelData.isAction ? t.launcherDim : t.launcherText
|
||||||
|
font.pixelSize: modelData.isAction ? 13 : 14
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: !modelData.isAction && modelData.actions && modelData.actions.length > 0
|
||||||
|
text: root.expandedItemId === modelData.id ? "▲" : "▼"
|
||||||
|
color: t.launcherDim
|
||||||
|
font.pixelSize: 9
|
||||||
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
visible: !modelData.isAction
|
||||||
width: 26; height: 26
|
width: 26; height: 26
|
||||||
Layout.alignment: Qt.AlignVCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
@@ -525,20 +561,40 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onEntered: parent.hovered = true
|
onEntered: parent.hovered = true
|
||||||
onExited: { parent.hovered = false; parent.starHovered = false }
|
onExited: { parent.hovered = false; parent.starHovered = false }
|
||||||
onPositionChanged: mouse => {
|
onPositionChanged: mouse => {
|
||||||
parent.starHovered = !modelData.isHeader && (mouse.x >= width - 46)
|
parent.starHovered = !modelData.isHeader && !modelData.isAction && (mouse.x >= width - 46)
|
||||||
}
|
}
|
||||||
onClicked: mouse => {
|
onClicked: mouse => {
|
||||||
if (modelData.isHeader) return
|
if (modelData.isHeader) return
|
||||||
if (mouse.x >= width - 46) {
|
|
||||||
root.toggleFavorite(modelData.id)
|
if (mouse.button === Qt.RightButton) {
|
||||||
} else {
|
if (!modelData.isAction && modelData.actions && modelData.actions.length > 0) {
|
||||||
root.launch(modelData)
|
root._savedContentY = resultsList.contentY
|
||||||
|
root._savedExpandId = modelData.id
|
||||||
|
root._expandingActions = modelData.id
|
||||||
|
root._pendingRestore = true
|
||||||
|
if (root.expandedItemId === modelData.id)
|
||||||
|
root.expandedItemId = ""
|
||||||
|
else
|
||||||
|
root.expandedItemId = modelData.id
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (modelData.isAction) {
|
||||||
|
root.executeAction(modelData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse.x >= width - 46)
|
||||||
|
root.toggleFavorite(modelData.id)
|
||||||
|
else
|
||||||
|
root.launch(modelData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
CACHE_DIR="/tmp/qs_launcher"
|
|
||||||
|
|
||||||
DRUN_CACHE="$CACHE_DIR/drun.txt"
|
|
||||||
RUN_CACHE="$CACHE_DIR/run.txt"
|
|
||||||
|
|
||||||
mkdir -p "$CACHE_DIR"
|
|
||||||
|
|
||||||
|
|
||||||
get_desktop_files() {
|
|
||||||
IFS=':' read -ra DIRS <<< \
|
|
||||||
"${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
|
||||||
|
|
||||||
for dir in "${DIRS[@]}"; do
|
|
||||||
APP_DIR="$dir/applications"
|
|
||||||
|
|
||||||
[ -d "$APP_DIR" ] || continue
|
|
||||||
|
|
||||||
find "$APP_DIR" \
|
|
||||||
-maxdepth 1 \
|
|
||||||
-name "*.desktop" \
|
|
||||||
2>/dev/null
|
|
||||||
done | while read -r f; do
|
|
||||||
|
|
||||||
name=$(
|
|
||||||
grep -m1 '^Name=' "$f" \
|
|
||||||
| cut -d= -f2-
|
|
||||||
)
|
|
||||||
|
|
||||||
exec=$(
|
|
||||||
grep -m1 '^Exec=' "$f" \
|
|
||||||
| cut -d= -f2- \
|
|
||||||
| sed 's/%[fFuUik]//g'
|
|
||||||
)
|
|
||||||
|
|
||||||
type=$(
|
|
||||||
grep -m1 '^Type=' "$f" \
|
|
||||||
| cut -d= -f2-
|
|
||||||
)
|
|
||||||
|
|
||||||
nodisplay=$(
|
|
||||||
grep -m1 '^NoDisplay=' "$f" \
|
|
||||||
| cut -d= -f2- \
|
|
||||||
| tr '[:upper:]' '[:lower:]'
|
|
||||||
)
|
|
||||||
|
|
||||||
icon=$(
|
|
||||||
grep -m1 '^Icon=' "$f" \
|
|
||||||
| cut -d= -f2-
|
|
||||||
)
|
|
||||||
|
|
||||||
[[ "$type" == "Application" ]] || continue
|
|
||||||
[[ "$nodisplay" != "true" ]] || continue
|
|
||||||
[[ -n "$name" ]] || continue
|
|
||||||
|
|
||||||
printf "%s\t%s\t%s\t%s\n" \
|
|
||||||
"$name" \
|
|
||||||
"$exec" \
|
|
||||||
"$(basename "$f" .desktop)" \
|
|
||||||
"$icon"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
get_binaries() {
|
|
||||||
compgen -c \
|
|
||||||
| sort -u \
|
|
||||||
| head -600 \
|
|
||||||
| awk '{
|
|
||||||
print $1 "\t" $1 "\t" $1
|
|
||||||
}'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sort_and_dedup() {
|
|
||||||
sort -f -t$'\t' -k1,1 \
|
|
||||||
| awk -F'\t' '!seen[$1]++'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
build_drun() {
|
|
||||||
get_desktop_files | sort_and_dedup
|
|
||||||
}
|
|
||||||
|
|
||||||
build_run() {
|
|
||||||
{ get_desktop_files; get_binaries; } | sort_and_dedup
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
update_cache() {
|
|
||||||
build_drun > "$DRUN_CACHE.tmp"
|
|
||||||
build_run > "$RUN_CACHE.tmp"
|
|
||||||
|
|
||||||
mv "$DRUN_CACHE.tmp" "$DRUN_CACHE"
|
|
||||||
mv "$RUN_CACHE.tmp" "$RUN_CACHE"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if [ "$1" == "--update-only" ]; then
|
|
||||||
update_cache
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
Reference in New Issue
Block a user