diff --git a/bar/modules/Crypto.qml b/bar/modules/Crypto.qml index 64dd323..32f6edc 100644 --- a/bar/modules/Crypto.qml +++ b/bar/modules/Crypto.qml @@ -16,7 +16,9 @@ ModuleChip { id: configWriter } - width: chipRow.implicitWidth + 24 + width: cryptoIcon.visible + ? (12 + cryptoIcon.implicitWidth + 7 + chipRow.implicitWidth + 12) + : (chipRow.implicitWidth + 24) radius: panelRadius chipColor: t.cryptoBg @@ -152,6 +154,7 @@ ModuleChip { if (root.localPairs.length <= 1) return root.activeCoinIndex = (root.activeCoinIndex - 1 + root.localPairs.length) % root.localPairs.length root._applyFromCache() + root._restartTyping() rotateTimer.restart() } @@ -159,6 +162,7 @@ ModuleChip { if (root.localPairs.length <= 1) return root.activeCoinIndex = (root.activeCoinIndex + 1) % root.localPairs.length root._applyFromCache() + root._restartTyping() rotateTimer.restart() } @@ -184,6 +188,51 @@ ModuleChip { sparklineData = entry.sparkline || [] change7d = entry.change7d || 0 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() { @@ -291,9 +340,26 @@ ModuleChip { onLocalRefreshSecChanged: refreshTimer.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 { id: chipRow - anchors.centerIn: parent + anchors { + left: cryptoIcon.visible ? cryptoIcon.right : parent.left + leftMargin: cryptoIcon.visible ? 7 : 12 + verticalCenter: parent.verticalCenter + } spacing: 0 SequentialAnimation on opacity { @@ -310,34 +376,61 @@ ModuleChip { } } - Text { - visible: root.localShowIcon - text: "󰿤" - color: t.cryptoIcon - font.pixelSize: 14 - Layout.alignment: Qt.AlignVCenter - Layout.rightMargin: 7 + Item { + implicitWidth: labelSizer.implicitWidth + implicitHeight: labelSizer.implicitHeight + + Text { + id: labelSizer + 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: root.coinSymbol + "/" + root.displayCurrency.toUpperCase() + ": " - color: t.cryptoLabel - font.pixelSize: 12 - font.bold: true - 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 ? "▲" : "▼" + visible: root._typing || root.price >= 0 + text: root._typing + ? (root.price >= 0 ? (root.change24h >= 0 ? "▲" : "▼") : "").substring(0, root._typedIcon) + : (root.price >= 0 ? (root.change24h >= 0 ? "▲" : "▼") : "") color: root.change24h >= 0 ? (t.cryptoUp) : (t.cryptoDown) font.pixelSize: 9 font.bold: true diff --git a/components/PopupPanel.qml b/components/PopupPanel.qml index 989855d..281d022 100644 --- a/components/PopupPanel.qml +++ b/components/PopupPanel.qml @@ -21,7 +21,13 @@ PanelWindow { property bool popupOpen: false 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() { _doOpen() } diff --git a/config/config.qml b/config/config.qml index 76dda17..a29b9ae 100644 --- a/config/config.qml +++ b/config/config.qml @@ -118,8 +118,6 @@ QtObject { // ──────────────────────────────────────────────────────────────────────── // LAUNCHER // ──────────────────────────────────────────────────────────────────────── - property string launcherScript: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher.sh" - property string launcherCacheDir: "/tmp/qs_launcher" property bool launcherCloseOnClickOutside: true property bool launcherSelectFirst: false property bool launcherMipmapIcons: false @@ -142,7 +140,7 @@ QtObject { // // This list is the *default*; pairs can be added/removed live in the // 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 ──────────────────────────────────────────────── // 0 → "80k" diff --git a/launcher/Launcher.qml b/launcher/Launcher.qml index e701877..0a7ded0 100644 --- a/launcher/Launcher.qml +++ b/launcher/Launcher.qml @@ -3,8 +3,8 @@ import QtQuick.Layouts import QtQuick.Controls import Quickshell import Quickshell.Wayland -import Quickshell.Io import Quickshell.Hyprland +import Quickshell.Io import "../config" as Cfg import "../components" @@ -21,6 +21,78 @@ PanelWindow { 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 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 { id: launcherShortcut name: "toggle" @@ -30,13 +102,14 @@ PanelWindow { property bool _isOpen: false readonly property bool isOpen: _isOpen property bool _keyboardNav: false - property var allItems: [] - property bool loading: false - property int lastCacheUpdate: 0 - property var favoriteIds: [] - property var accessCounts: ({}) - readonly property string statePath: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher_state.json" + property string _expandingActions: "" + property string _savedExpandId: "" + property real _savedContentY: 0 + property bool _pendingRestore: false + property bool _inRestore: false + + property string expandedItemId: "" readonly property int defaultIndex: Cfg.Config.launcherSelectFirst ? 0 : -1 @@ -52,15 +125,33 @@ PanelWindow { let rest = filtered.filter(i => !root.favoriteIds.includes(i.id)) rest.sort((a, b) => (root.accessCounts[b.id] || 0) - (root.accessCounts[a.id] || 0)) 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) { result.push({ isHeader: true, section: "Favorites" }) for (let i = 0; i < favs.length; i++) - result.push(Object.assign({}, favs[i], { isHeader: false })) + pushItem(favs[i]) } if (rest.length > 0) { result.push({ isHeader: true, section: "All Applications" }) for (let i = 0; i < rest.length; i++) - result.push(Object.assign({}, rest[i], { isHeader: false })) + pushItem(rest[i]) } return result } @@ -88,16 +179,16 @@ PanelWindow { closeTimer.stop() visible = true _isOpen = true + expandedItemId = "" searchInput.text = "" resultsList.currentIndex = root.defaultIndex Qt.callLater(() => resultsList.positionViewAtBeginning()) - loadItems() - loadState() Qt.callLater(() => searchInput.forceActiveFocus()) } function close() { _isOpen = false + expandedItemId = "" 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) { - 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() - const logPath = "/tmp/qml_launch.log" - - const args = parseExec(cleanExec) - 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) + function executeAction(item) { + if (!item || !item.actionObj) return + item.actionObj.execute() + console.log("Executing action: " + item.name) close() } @@ -228,61 +229,9 @@ PanelWindow { PopupHideTimer { id: closeTimer - onTriggered: { - root.visible = false - root.allItems = [] - } + onTriggered: root.visible = false } - 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 { anchors.fill: parent @@ -355,12 +304,48 @@ PanelWindow { verticalAlignment: TextInput.AlignVCenter selectionColor: t.launcherAccent - onTextChanged: resultsList.currentIndex = root.defaultIndex + onTextChanged: { + resultsList.currentIndex = root.defaultIndex + root.expandedItemId = "" + } Keys.onReturnPressed: { const idx = root.skipHeader(Math.max(0, resultsList.currentIndex), 1) - if (idx >= 0) - root.launch(root.filteredItems[idx]) + if (idx < 0) return + 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: { @@ -432,19 +417,51 @@ PanelWindow { flickableDirection: Flickable.VerticalFlick onModelChanged: { - currentIndex = root.defaultIndex - for (let i = 0; i < count; i++) { - const md = root.filteredItems[i] - if (md && !md.isHeader) { - currentIndex = i - break + if (root._expandingActions !== "") { + root._expandingActions = "" + if (root._savedExpandId !== "") { + Qt.callLater(() => { + for (let i = 0; i < resultsList.count; i++) { + 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 } + 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 { + id: itemDelegate width: resultsList.width height: 38 radius: modelData.isHeader ? 0 : 8 @@ -452,6 +469,7 @@ PanelWindow { : (starHovered || hovered) ? t.launcherHover : (resultsList.currentIndex === index && index !== -1 ? t.launcherSelected : "transparent") + clip: modelData.isAction property bool hovered: false property bool starHovered: false @@ -467,11 +485,12 @@ PanelWindow { RowLayout { visible: !modelData.isHeader anchors.fill: parent - anchors.leftMargin: 12 + anchors.leftMargin: modelData.isAction ? 28 : 12 anchors.rightMargin: 12 spacing: 12 Item { + visible: !modelData.isAction width: 22; height: 22 Layout.alignment: Qt.AlignVCenter @@ -496,14 +515,31 @@ PanelWindow { } Text { - text: modelData.name - color: t.launcherText + visible: modelData.isAction + text: "›" + color: t.launcherDim 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 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 { + visible: !modelData.isAction width: 26; height: 26 Layout.alignment: Qt.AlignVCenter @@ -525,20 +561,40 @@ PanelWindow { } MouseArea { + acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent hoverEnabled: true onEntered: parent.hovered = true onExited: { parent.hovered = false; parent.starHovered = false } onPositionChanged: mouse => { - parent.starHovered = !modelData.isHeader && (mouse.x >= width - 46) + parent.starHovered = !modelData.isHeader && !modelData.isAction && (mouse.x >= width - 46) } onClicked: mouse => { if (modelData.isHeader) return - if (mouse.x >= width - 46) { - root.toggleFavorite(modelData.id) - } else { - root.launch(modelData) + + if (mouse.button === Qt.RightButton) { + if (!modelData.isAction && modelData.actions && modelData.actions.length > 0) { + 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) } } } diff --git a/launcher/launcher.sh b/launcher/launcher.sh deleted file mode 100755 index a997272..0000000 --- a/launcher/launcher.sh +++ /dev/null @@ -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