mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-01 20:28:16 +02:00
Add shell bar config commands
This commit is contained in:
@@ -1,76 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Mutate the user shell.json bar layout
|
||||
# omarchy:args=<left|center|right> <plugin>
|
||||
# omarchy:examples=omarchy config shell right omarchy.tailscale
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG_FILE="$HOME/.config/omarchy/shell.json"
|
||||
OMARCHY_ROOT="${OMARCHY_PATH:-}"
|
||||
DEFAULTS_FILE="$OMARCHY_ROOT/config/omarchy/shell.json"
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-config-shell <left|center|right> <plugin>" >&2
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "omarchy-config-shell: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
section="${1:-}"
|
||||
plugin="${2:-}"
|
||||
|
||||
if (( $# != 2 )); then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[[ $section =~ ^(left|center|right)$ ]] || fail "section must be left, center, or right"
|
||||
[[ -n $plugin ]] || fail "plugin is required"
|
||||
[[ -n $OMARCHY_ROOT ]] || fail "OMARCHY_PATH is not set"
|
||||
|
||||
mkdir -p "$(dirname "$CONFIG_FILE")"
|
||||
|
||||
source_file="$CONFIG_FILE"
|
||||
if [[ ! -s $source_file ]]; then
|
||||
source_file="$DEFAULTS_FILE"
|
||||
fi
|
||||
[[ -s $source_file ]] || fail "could not find shell config or defaults"
|
||||
|
||||
tmp=$(mktemp)
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
|
||||
jq --arg section "$section" --arg plugin "$plugin" '
|
||||
def object_or_empty: if type == "object" then . else {} end;
|
||||
def array_or_empty: if type == "array" then . else [] end;
|
||||
def entry_id: if type == "object" then (.id // "" | tostring) else tostring end;
|
||||
def append_anchor($section):
|
||||
{
|
||||
left: "omarchy.workspaces",
|
||||
center: "omarchy.weather",
|
||||
right: "omarchy.tray"
|
||||
}[$section];
|
||||
def insert_after_anchor($entries; $entry; $anchor):
|
||||
($entries | map(entry_id) | index($anchor)) as $index
|
||||
| if $index == null then
|
||||
$entries + [$entry]
|
||||
else
|
||||
$entries[0:$index + 1] + [$entry] + $entries[$index + 1:]
|
||||
end;
|
||||
|
||||
object_or_empty
|
||||
| .version = 1
|
||||
| .bar = (.bar | object_or_empty)
|
||||
| .bar.layout = (.bar.layout | object_or_empty)
|
||||
| .bar.layout.left = (.bar.layout.left | array_or_empty | map(select(entry_id != $plugin)))
|
||||
| .bar.layout.center = (.bar.layout.center | array_or_empty | map(select(entry_id != $plugin)))
|
||||
| .bar.layout.right = (.bar.layout.right | array_or_empty | map(select(entry_id != $plugin)))
|
||||
| .plugins = (.plugins | array_or_empty)
|
||||
| .bar.layout[$section] = insert_after_anchor(.bar.layout[$section]; { id: $plugin }; append_anchor($section))
|
||||
' "$source_file" >"$tmp"
|
||||
|
||||
mv "$tmp" "$CONFIG_FILE"
|
||||
trap - EXIT
|
||||
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Mutate the user shell.json bar layout
|
||||
# omarchy:args=add <plugin> [left|center|right] | drop <plugin> | show
|
||||
# omarchy:examples=omarchy config shell bar add omarchy.tailscale | omarchy config shell bar drop omarchy.tailscale | omarchy config shell bar show
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG_FILE="$HOME/.config/omarchy/shell.json"
|
||||
OMARCHY_ROOT="${OMARCHY_PATH:-}"
|
||||
DEFAULTS_FILE="$OMARCHY_ROOT/config/omarchy/shell.json"
|
||||
|
||||
usage() {
|
||||
echo "Usage: omarchy-config-shell-bar add <plugin> [left|center|right] | drop <plugin> | show" >&2
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "omarchy-config-shell-bar: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
source_file() {
|
||||
local source="$CONFIG_FILE"
|
||||
|
||||
if [[ ! -s $source ]]; then
|
||||
[[ -n $OMARCHY_ROOT ]] || fail "OMARCHY_PATH is not set"
|
||||
source="$DEFAULTS_FILE"
|
||||
fi
|
||||
|
||||
[[ -s $source ]] || fail "could not find shell config or defaults"
|
||||
printf '%s\n' "$source"
|
||||
}
|
||||
|
||||
command="${1:-}"
|
||||
|
||||
case "$command" in
|
||||
show)
|
||||
if (( $# != 1 )); then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jq . "$(source_file)"
|
||||
;;
|
||||
add)
|
||||
if (( $# < 2 || $# > 3 )); then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
plugin="${2:-}"
|
||||
section="${3:-right}"
|
||||
|
||||
[[ -n $plugin ]] || fail "plugin is required"
|
||||
[[ $section =~ ^(left|center|right)$ ]] || fail "section must be left, center, or right"
|
||||
|
||||
mkdir -p "$(dirname "$CONFIG_FILE")"
|
||||
|
||||
source="$(source_file)"
|
||||
tmp=$(mktemp)
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
|
||||
jq --arg section "$section" --arg plugin "$plugin" '
|
||||
def object_or_empty: if type == "object" then . else {} end;
|
||||
def array_or_empty: if type == "array" then . else [] end;
|
||||
def entry_id: if type == "object" then (.id // "" | tostring) else tostring end;
|
||||
def append_anchor($section):
|
||||
{
|
||||
left: "omarchy.workspaces",
|
||||
center: "omarchy.weather",
|
||||
right: "omarchy.tray"
|
||||
}[$section];
|
||||
def insert_after_anchor($entries; $entry; $anchor):
|
||||
($entries | map(entry_id) | index($anchor)) as $index
|
||||
| if $index == null then
|
||||
$entries + [$entry]
|
||||
else
|
||||
$entries[0:$index + 1] + [$entry] + $entries[$index + 1:]
|
||||
end;
|
||||
|
||||
object_or_empty
|
||||
| .version = 1
|
||||
| .bar = (.bar | object_or_empty)
|
||||
| .bar.layout = (.bar.layout | object_or_empty)
|
||||
| .bar.layout.left = (.bar.layout.left | array_or_empty | map(select(entry_id != $plugin)))
|
||||
| .bar.layout.center = (.bar.layout.center | array_or_empty | map(select(entry_id != $plugin)))
|
||||
| .bar.layout.right = (.bar.layout.right | array_or_empty | map(select(entry_id != $plugin)))
|
||||
| .plugins = (.plugins | array_or_empty)
|
||||
| .bar.layout[$section] = insert_after_anchor(.bar.layout[$section]; { id: $plugin }; append_anchor($section))
|
||||
' "$source" >"$tmp"
|
||||
|
||||
mv "$tmp" "$CONFIG_FILE"
|
||||
trap - EXIT
|
||||
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
|
||||
;;
|
||||
drop)
|
||||
if (( $# != 2 )); then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
plugin="${2:-}"
|
||||
|
||||
[[ -n $plugin ]] || fail "plugin is required"
|
||||
|
||||
mkdir -p "$(dirname "$CONFIG_FILE")"
|
||||
|
||||
source="$(source_file)"
|
||||
tmp=$(mktemp)
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
|
||||
jq --arg plugin "$plugin" '
|
||||
def object_or_empty: if type == "object" then . else {} end;
|
||||
def array_or_empty: if type == "array" then . else [] end;
|
||||
def entry_id: if type == "object" then (.id // "" | tostring) else tostring end;
|
||||
|
||||
object_or_empty
|
||||
| .version = 1
|
||||
| .bar = (.bar | object_or_empty)
|
||||
| .bar.layout = (.bar.layout | object_or_empty)
|
||||
| .bar.layout.left = (.bar.layout.left | array_or_empty | map(select(entry_id != $plugin)))
|
||||
| .bar.layout.center = (.bar.layout.center | array_or_empty | map(select(entry_id != $plugin)))
|
||||
| .bar.layout.right = (.bar.layout.right | array_or_empty | map(select(entry_id != $plugin)))
|
||||
| .plugins = (.plugins | array_or_empty)
|
||||
' "$source" >"$tmp"
|
||||
|
||||
mv "$tmp" "$CONFIG_FILE"
|
||||
trap - EXIT
|
||||
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -14,6 +14,6 @@ echo -e "\nAllowing $USER to manage Tailscale..."
|
||||
sudo tailscale set --operator="$USER"
|
||||
|
||||
echo -e "\nAdding Tailscale to the bar..."
|
||||
omarchy-config-shell right omarchy.tailscale
|
||||
omarchy-config-shell-bar add omarchy.tailscale
|
||||
|
||||
omarchy-webapp-install "Tailscale" "https://login.tailscale.com/admin/machines" https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/tailscale-light.png
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real iconSize: Style.font.icon
|
||||
property color color: Color.foreground
|
||||
|
||||
width: iconSize * 1.18
|
||||
height: iconSize
|
||||
implicitWidth: iconSize * 1.18
|
||||
implicitHeight: iconSize
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
layer.enabled: true
|
||||
layer.samples: 4
|
||||
scale: 0.95
|
||||
|
||||
Tile { cx: root.width * 0.25; cy: root.height * 0.188 }
|
||||
Tile { cx: root.width * 0.75; cy: root.height * 0.188 }
|
||||
Tile { cx: root.width * 0.25; cy: root.height * 0.564 }
|
||||
Tile { cx: root.width * 0.75; cy: root.height * 0.564 }
|
||||
Tile { cx: root.width * 0.50; cy: root.height * 0.812 }
|
||||
}
|
||||
|
||||
component Tile: ShapePath {
|
||||
property real cx: 0
|
||||
property real cy: 0
|
||||
readonly property real tileWidth: root.width * 0.50
|
||||
readonly property real tileHeight: root.height * 0.376
|
||||
|
||||
fillColor: root.color
|
||||
strokeWidth: 0
|
||||
startX: cx
|
||||
startY: cy - tileHeight / 2
|
||||
PathLine { x: cx + tileWidth / 2; y: cy }
|
||||
PathLine { x: cx; y: cy + tileHeight / 2 }
|
||||
PathLine { x: cx - tileWidth / 2; y: cy }
|
||||
PathLine { x: cx; y: cy - tileHeight / 2 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
var IMAGE_EXTENSIONS = {
|
||||
jpg: true, jpeg: true, png: true, gif: true, webp: true, avif: true, heic: true,
|
||||
svg: true, bmp: true, tif: true, tiff: true
|
||||
}
|
||||
|
||||
var VIDEO_EXTENSIONS = {
|
||||
mp4: true, mov: true, mkv: true, webm: true, avi: true, m4v: true, mpg: true,
|
||||
mpeg: true, wmv: true
|
||||
}
|
||||
|
||||
var DOCUMENT_EXTENSIONS = {
|
||||
pdf: true, txt: true, md: true, doc: true, docx: true, xls: true, xlsx: true,
|
||||
ppt: true, pptx: true, odt: true, ods: true, odp: true, rtf: true, csv: true,
|
||||
pages: true, numbers: true, key: true
|
||||
}
|
||||
|
||||
function parseStatus(raw) {
|
||||
var text = String(raw || "").trim()
|
||||
if (text === "") return defaultStatus()
|
||||
try {
|
||||
var parsed = JSON.parse(text)
|
||||
if (!parsed || typeof parsed !== "object") return defaultStatus()
|
||||
parsed.files = Array.isArray(parsed.files) ? parsed.files : []
|
||||
return parsed
|
||||
} catch (e) {
|
||||
var failed = defaultStatus()
|
||||
failed.ok = false
|
||||
failed.lastError = "Failed to parse Dropbox status"
|
||||
return failed
|
||||
}
|
||||
}
|
||||
|
||||
function defaultStatus() {
|
||||
return {
|
||||
ok: true,
|
||||
installed: false,
|
||||
running: false,
|
||||
authenticated: false,
|
||||
statusText: "Unavailable",
|
||||
accountPath: "",
|
||||
plan: "",
|
||||
usedBytes: 0,
|
||||
quotaBytes: 0,
|
||||
usagePercent: 0,
|
||||
quotaKnown: false,
|
||||
files: []
|
||||
}
|
||||
}
|
||||
|
||||
function fileExtension(name) {
|
||||
var value = String(name || "").toLowerCase()
|
||||
var index = value.lastIndexOf(".")
|
||||
return index >= 0 ? value.substring(index + 1) : ""
|
||||
}
|
||||
|
||||
function fileKind(name) {
|
||||
var ext = fileExtension(name)
|
||||
if (IMAGE_EXTENSIONS[ext]) return "image"
|
||||
if (VIDEO_EXTENSIONS[ext]) return "video"
|
||||
if (DOCUMENT_EXTENSIONS[ext]) return "document"
|
||||
return "misc"
|
||||
}
|
||||
|
||||
function fileGlyph(name) {
|
||||
var kind = fileKind(name)
|
||||
if (kind === "image") return ""
|
||||
if (kind === "video") return ""
|
||||
if (kind === "document") return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
var value = Number(bytes || 0)
|
||||
if (!isFinite(value) || value <= 0) return "0 B"
|
||||
var units = ["B", "KB", "MB", "GB", "TB"]
|
||||
var index = 0
|
||||
while (value >= 1000 && index < units.length - 1) {
|
||||
value = value / 1000
|
||||
index++
|
||||
}
|
||||
var decimals = value >= 100 || index === 0 ? 0 : (value >= 10 ? 1 : 2)
|
||||
return value.toFixed(decimals).replace(/\.0+$/, "").replace(/(\.\d)0$/, "$1") + " " + units[index]
|
||||
}
|
||||
|
||||
function formatPercent(value) {
|
||||
var number = Number(value || 0)
|
||||
if (!isFinite(number) || number <= 0) return "0%"
|
||||
if (number >= 10) return Math.round(number) + "%"
|
||||
return number.toFixed(1).replace(/\.0$/, "") + "%"
|
||||
}
|
||||
|
||||
function usageText(usedBytes, quotaBytes, quotaKnown) {
|
||||
if (quotaKnown && Number(quotaBytes || 0) > 0) {
|
||||
return formatBytes(usedBytes) + " of " + formatBytes(quotaBytes)
|
||||
}
|
||||
return formatBytes(usedBytes)
|
||||
}
|
||||
|
||||
function relativeTime(timestampSec, nowMs) {
|
||||
var ts = Number(timestampSec || 0)
|
||||
if (!isFinite(ts) || ts <= 0) return "Unknown time"
|
||||
var now = nowMs === undefined ? Date.now() : Number(nowMs)
|
||||
var diff = Math.max(0, Math.floor((now - ts * 1000) / 1000))
|
||||
if (diff < 60) return "Just now"
|
||||
var minutes = Math.floor(diff / 60)
|
||||
if (minutes < 60) return minutes + "m ago"
|
||||
var hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return hours + "h ago"
|
||||
var days = Math.floor(hours / 24)
|
||||
if (days < 30) return days + "d ago"
|
||||
var months = Math.floor(days / 30)
|
||||
if (months < 12) return months + "mo ago"
|
||||
return Math.floor(days / 365) + "y ago"
|
||||
}
|
||||
|
||||
function fileMeta(file, nowMs) {
|
||||
if (!file) return ""
|
||||
var parts = [relativeTime(file.modifiedTs, nowMs)]
|
||||
var folder = String(file.folder || "")
|
||||
if (folder !== "") parts.push(folder)
|
||||
return parts.join(" · ")
|
||||
}
|
||||
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
parseStatus: parseStatus,
|
||||
defaultStatus: defaultStatus,
|
||||
fileExtension: fileExtension,
|
||||
fileKind: fileKind,
|
||||
fileGlyph: fileGlyph,
|
||||
formatBytes: formatBytes,
|
||||
formatPercent: formatPercent,
|
||||
usageText: usageText,
|
||||
relativeTime: relativeTime,
|
||||
fileMeta: fileMeta
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
import "Model.js" as Model
|
||||
|
||||
Panel {
|
||||
id: root
|
||||
moduleName: "omarchy.dropbox"
|
||||
ipcTarget: "omarchy.dropbox"
|
||||
manageIpc: false
|
||||
|
||||
property string focusSection: "login"
|
||||
property int fileIndex: 0
|
||||
property bool cursorActive: false
|
||||
property int phraseIndex: 0
|
||||
|
||||
readonly property var activePhrases: [
|
||||
"Filing files",
|
||||
"Distributing data",
|
||||
"Shuffling folders",
|
||||
"Boxing bytes",
|
||||
"Sorting stuff",
|
||||
"Syncing secrets",
|
||||
"Packing packets",
|
||||
"Moving memories",
|
||||
"Wrangling revisions",
|
||||
"Cataloging chaos"
|
||||
]
|
||||
readonly property string heroPhraseText: activePhrases[phraseIndex % activePhrases.length]
|
||||
readonly property color foreground: bar ? bar.foreground : Color.foreground
|
||||
readonly property color urgent: bar ? bar.urgent : Color.urgent
|
||||
readonly property color dim: Qt.darker(foreground, 1.55)
|
||||
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
||||
readonly property color iconColor: dropbox.authenticated ? foreground : dim
|
||||
|
||||
function ensureCursor() {
|
||||
if (!dropbox.authenticated) {
|
||||
focusSection = "login"
|
||||
fileIndex = 0
|
||||
return
|
||||
}
|
||||
if (dropbox.files.length === 0) {
|
||||
focusSection = "header"
|
||||
fileIndex = 0
|
||||
return
|
||||
}
|
||||
if (focusSection !== "files") focusSection = "files"
|
||||
if (fileIndex >= dropbox.files.length) fileIndex = Math.max(0, dropbox.files.length - 1)
|
||||
if (fileIndex < 0) fileIndex = 0
|
||||
}
|
||||
|
||||
function moveCursor(dx, dy) {
|
||||
cursorActive = true
|
||||
ensureCursor()
|
||||
if (focusSection === "files" && dy !== 0) {
|
||||
fileIndex = Math.max(0, Math.min(dropbox.files.length - 1, fileIndex + dy))
|
||||
scrollCursorIntoView()
|
||||
}
|
||||
}
|
||||
|
||||
function activateCursor() {
|
||||
ensureCursor()
|
||||
if (focusSection === "login") dropbox.login()
|
||||
else if (focusSection === "files") dropbox.openFile(selectedFile())
|
||||
}
|
||||
|
||||
function selectedFile() {
|
||||
if (dropbox.files.length === 0) return null
|
||||
return dropbox.files[Math.max(0, Math.min(fileIndex, dropbox.files.length - 1))]
|
||||
}
|
||||
|
||||
function setFileCursor(index) {
|
||||
cursorActive = true
|
||||
focusSection = "files"
|
||||
fileIndex = index
|
||||
scrollCursorIntoView()
|
||||
}
|
||||
|
||||
function scrollItemIntoView(item) {
|
||||
if (!panelFlick || !item) return
|
||||
Qt.callLater(function() {
|
||||
if (!item) return
|
||||
var margin = Style.space(6)
|
||||
var point = item.mapToItem(panelFlick.contentItem, 0, 0)
|
||||
var top = point.y
|
||||
var bottom = top + item.height
|
||||
var viewTop = panelFlick.contentY
|
||||
var viewBottom = viewTop + panelFlick.height
|
||||
var maxY = Math.max(0, panelFlick.contentHeight - panelFlick.height)
|
||||
if (top < viewTop + margin) panelFlick.contentY = Math.max(0, top - margin)
|
||||
else if (bottom > viewBottom - margin) panelFlick.contentY = Math.min(maxY, bottom + margin - panelFlick.height)
|
||||
})
|
||||
}
|
||||
|
||||
function scrollCursorIntoView() {
|
||||
if (focusSection === "files" && fileColumn && fileIndex >= 0 && fileIndex < fileColumn.children.length) {
|
||||
scrollItemIntoView(fileColumn.children[fileIndex])
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
onOpenedChanged: if (opened) {
|
||||
cursorActive = false
|
||||
if (panelFlick) panelFlick.contentY = 0
|
||||
dropbox.refresh()
|
||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
onFileIndexChanged: scrollCursorIntoView()
|
||||
|
||||
Service {
|
||||
id: dropbox
|
||||
settings: root.settings
|
||||
omarchyPath: root.bar ? root.bar.omarchyPath : Quickshell.env("OMARCHY_PATH")
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: dropbox
|
||||
function onAuthenticatedChanged() { root.ensureCursor() }
|
||||
function onFilesChanged() { root.ensureCursor() }
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: root.ipcTarget
|
||||
function open(): void { root.open() }
|
||||
function close(): void { root.close() }
|
||||
function show(): void { root.open() }
|
||||
function hide(): void { root.close() }
|
||||
function toggle(): void { root.toggle() }
|
||||
function refresh(): string { dropbox.refresh(); return "ok" }
|
||||
function login(): string { dropbox.login(); return "ok" }
|
||||
function status(): string { return dropbox.statusText }
|
||||
}
|
||||
|
||||
Item {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(32)
|
||||
implicitHeight: root.bar && root.bar.vertical ? Style.space(26) : (root.bar ? root.bar.barSize : Style.space(26))
|
||||
|
||||
property var registeredBar: null
|
||||
|
||||
function triggerPress(buttonCode) {
|
||||
if (buttonCode === Qt.RightButton) dropbox.refresh()
|
||||
else if (buttonCode === Qt.MiddleButton) dropbox.login()
|
||||
else root.toggle()
|
||||
}
|
||||
|
||||
function syncClickRegistration() {
|
||||
if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(button)
|
||||
registeredBar = root.bar
|
||||
if (registeredBar && registeredBar.registerClickTarget) registeredBar.registerClickTarget(button)
|
||||
}
|
||||
|
||||
Component.onCompleted: syncClickRegistration()
|
||||
Component.onDestruction: if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(button)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onBarChanged() { button.syncClickRegistration() }
|
||||
}
|
||||
|
||||
DropboxIcon {
|
||||
anchors.centerIn: parent
|
||||
iconSize: Style.space(12)
|
||||
color: root.iconColor
|
||||
opacity: dropbox.authenticated ? 1.0 : 0.6
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: function(mouse) { button.triggerPress(mouse.button) }
|
||||
}
|
||||
}
|
||||
|
||||
KeyboardPanel {
|
||||
id: panel
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.opened
|
||||
focusTarget: keyCatcher
|
||||
contentWidth: panel.fittedContentWidth(Style.space(380))
|
||||
contentHeight: panel.fittedContentHeight(column.implicitHeight, Style.space(560))
|
||||
|
||||
PanelKeyCatcher {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
onMoveRequested: function(dx, dy) {
|
||||
if (!root.cursorActive) { root.cursorActive = true; return }
|
||||
root.moveCursor(dx, dy)
|
||||
}
|
||||
onActivateRequested: if (root.cursorActive) root.activateCursor()
|
||||
onCloseRequested: root.close()
|
||||
onTabRequested: function(direction) { root.switchPanel(direction) }
|
||||
onTextKey: function(t) {
|
||||
if (t === "r" || t === "R") dropbox.refresh()
|
||||
else if (t === "l" || t === "L") dropbox.login()
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: panelFlick
|
||||
anchors.fill: parent
|
||||
contentWidth: width
|
||||
contentHeight: column.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
Column {
|
||||
id: column
|
||||
width: panelFlick.width
|
||||
spacing: Style.space(12)
|
||||
|
||||
PanelHero {
|
||||
id: hero
|
||||
visible: dropbox.authenticated
|
||||
width: parent.width
|
||||
title: "Dropbox"
|
||||
meta: root.heroPhraseText
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
iconOpacity: 1.0
|
||||
iconComponent: Component {
|
||||
DropboxIcon {
|
||||
iconSize: Style.font.display
|
||||
color: root.iconColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: dropbox.actionStatus !== "" || dropbox.lastError !== ""
|
||||
width: parent.width
|
||||
text: dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError
|
||||
color: dropbox.lastError !== "" && dropbox.actionStatus === "" ? root.urgent : root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
LoginButton {
|
||||
visible: !dropbox.authenticated
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Column {
|
||||
visible: dropbox.authenticated
|
||||
width: parent.width
|
||||
spacing: Style.spacing.labelGap
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Style.spacing.labelGap
|
||||
InfoPair { label: "Stored"; value: Model.usageText(dropbox.usedBytes, dropbox.quotaBytes, dropbox.quotaKnown) }
|
||||
}
|
||||
}
|
||||
|
||||
PanelSeparator {
|
||||
visible: dropbox.authenticated
|
||||
foreground: root.foreground
|
||||
}
|
||||
|
||||
Column {
|
||||
visible: dropbox.authenticated
|
||||
width: parent.width
|
||||
spacing: Style.space(10)
|
||||
|
||||
PanelSectionHeader {
|
||||
text: "RECENT FILES"
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: dropbox.files.length === 0
|
||||
width: parent.width
|
||||
text: "No synced files found."
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
id: fileColumn
|
||||
visible: dropbox.files.length > 0
|
||||
width: parent.width
|
||||
spacing: Style.space(6)
|
||||
|
||||
Repeater {
|
||||
model: dropbox.files
|
||||
FileRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: fileColumn.width
|
||||
file: modelData
|
||||
rowIndex: index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: phraseTimer
|
||||
interval: 2800
|
||||
running: root.opened && dropbox.authenticated
|
||||
repeat: true
|
||||
onTriggered: phraseSwap.restart()
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: phraseSwap
|
||||
PropertyAnimation {
|
||||
target: hero; property: "metaOpacity"
|
||||
to: 0.0; duration: 180; easing.type: Easing.OutQuad
|
||||
}
|
||||
ScriptAction {
|
||||
script: root.phraseIndex = (root.phraseIndex + 1) % root.activePhrases.length
|
||||
}
|
||||
PropertyAnimation {
|
||||
target: hero; property: "metaOpacity"
|
||||
to: 1.0; duration: 260; easing.type: Easing.InQuad
|
||||
}
|
||||
}
|
||||
|
||||
component LoginButton: CursorSurface {
|
||||
id: loginButton
|
||||
|
||||
hasCursor: root.cursorActive && root.focusSection === "login"
|
||||
foreground: root.foreground
|
||||
|
||||
implicitHeight: loginRow.implicitHeight + Style.spacing.rowPaddingX
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: dropbox.installed && !dropbox.busy ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: dropbox.installed && !dropbox.busy
|
||||
onEntered: {
|
||||
root.cursorActive = true
|
||||
root.focusSection = "login"
|
||||
}
|
||||
onClicked: dropbox.login()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: loginRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Style.space(10)
|
||||
anchors.rightMargin: Style.space(10)
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
text: ""
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.heading
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: dropbox.installed ? "Login to Dropbox" : "Dropbox CLI is not installed"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: dropbox.installed ? "Start the authentication flow" : "Install Dropbox from the service menu"
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
PanelActionButton {
|
||||
iconText: ""
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
enabled: dropbox.installed && !dropbox.busy
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: dropbox.login()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component FileRow: CursorSurface {
|
||||
id: fileRow
|
||||
property var file: null
|
||||
property int rowIndex: 0
|
||||
readonly property string fileName: file ? String(file.name || "Untitled") : "Untitled"
|
||||
|
||||
hasCursor: root.cursorActive && root.focusSection === "files" && root.fileIndex === rowIndex
|
||||
foreground: root.foreground
|
||||
|
||||
implicitHeight: fileContent.implicitHeight + Style.spacing.rowPaddingX
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: root.setFileCursor(fileRow.rowIndex)
|
||||
onClicked: dropbox.openFile(fileRow.file)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Style.space(10)
|
||||
anchors.rightMargin: Style.space(10)
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
text: Model.fileGlyph(fileRow.fileName)
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.icon
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: fileContent
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: fileRow.fileName
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: Model.fileMeta(fileRow.file)
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component InfoPair: Row {
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
|
||||
width: parent.width
|
||||
spacing: Style.space(8)
|
||||
|
||||
InfoLabel { text: label }
|
||||
Item { width: Math.max(0, parent.width - parent.children[0].implicitWidth - parent.children[2].implicitWidth - parent.spacing * 2); height: 1 }
|
||||
InfoValue { text: value }
|
||||
}
|
||||
|
||||
component InfoLabel: Text {
|
||||
color: root.foreground
|
||||
opacity: 0.6
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
}
|
||||
|
||||
component InfoValue: Text {
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import "Model.js" as Model
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var settings: ({})
|
||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
||||
|
||||
property bool installed: false
|
||||
property bool running: false
|
||||
property bool authenticated: false
|
||||
property bool refreshing: false
|
||||
property string statusText: "Checking…"
|
||||
property string accountPath: ""
|
||||
property string plan: ""
|
||||
property double usedBytes: 0
|
||||
property double quotaBytes: 0
|
||||
property double usagePercent: 0
|
||||
property bool quotaKnown: false
|
||||
property var files: []
|
||||
property string actionStatus: ""
|
||||
property string lastError: ""
|
||||
|
||||
readonly property int refreshIntervalSec: intSetting("refreshIntervalSec", 60, 10, 3600)
|
||||
readonly property bool busy: statusProcess.running || loginProcess.running
|
||||
readonly property string helperPath: (omarchyPath || "") + "/shell/plugins/panels/dropbox/status.py"
|
||||
|
||||
property string _statusOutput: ""
|
||||
property string _statusError: ""
|
||||
property string _loginOutput: ""
|
||||
property string _loginError: ""
|
||||
property bool _loginUrlOpened: false
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
function intSetting(name, fallback, min, max) {
|
||||
var n = parseInt(String(setting(name, fallback)), 10)
|
||||
if (!isFinite(n)) n = fallback
|
||||
if (n < min) n = min
|
||||
if (n > max) n = max
|
||||
return n
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (statusProcess.running || helperPath === "/shell/plugins/panels/dropbox/status.py") return
|
||||
_statusOutput = ""
|
||||
_statusError = ""
|
||||
refreshing = true
|
||||
statusProcess.command = ["python3", helperPath, "25"]
|
||||
statusProcess.running = true
|
||||
}
|
||||
|
||||
function applyStatus(raw) {
|
||||
var parsed = Model.parseStatus(raw)
|
||||
if (!parsed.ok) {
|
||||
lastError = parsed.lastError || "Failed to read Dropbox status"
|
||||
return
|
||||
}
|
||||
installed = parsed.installed === true
|
||||
running = parsed.running === true
|
||||
authenticated = parsed.authenticated === true
|
||||
statusText = String(parsed.statusText || (installed ? "Stopped" : "Not installed"))
|
||||
accountPath = String(parsed.accountPath || "")
|
||||
plan = String(parsed.plan || "")
|
||||
usedBytes = Number(parsed.usedBytes || 0)
|
||||
quotaBytes = Number(parsed.quotaBytes || 0)
|
||||
usagePercent = Number(parsed.usagePercent || 0)
|
||||
quotaKnown = parsed.quotaKnown === true
|
||||
files = parsed.files || []
|
||||
lastError = ""
|
||||
}
|
||||
|
||||
function elideStatus(text) {
|
||||
var value = String(text || "").replace(/\s+/g, " ").trim()
|
||||
return value.length > 140 ? value.substring(0, 137) + "…" : value
|
||||
}
|
||||
|
||||
function login() {
|
||||
if (!installed || loginProcess.running) return
|
||||
_loginOutput = ""
|
||||
_loginError = ""
|
||||
_loginUrlOpened = false
|
||||
actionStatus = "Starting Dropbox login…"
|
||||
loginProcess.command = ["dropbox-cli", "start"]
|
||||
loginProcess.running = true
|
||||
}
|
||||
|
||||
function openFile(file) {
|
||||
if (!file || !file.path) return
|
||||
Quickshell.execDetached(["uwsm-app", "--", "nautilus", "--select", fileUri(String(file.path))])
|
||||
}
|
||||
|
||||
function fileUri(path) {
|
||||
var parts = String(path || "").split("/")
|
||||
for (var i = 0; i < parts.length; i++) parts[i] = encodeURIComponent(parts[i])
|
||||
return "file://" + parts.join("/")
|
||||
}
|
||||
|
||||
function openAuthUrlFrom(text) {
|
||||
if (_loginUrlOpened) return true
|
||||
var match = String(text || "").match(/https?:\/\/\S+/)
|
||||
if (match && match[0]) {
|
||||
_loginUrlOpened = true
|
||||
Qt.openUrlExternally(match[0])
|
||||
actionStatus = "Opened Dropbox login"
|
||||
actionStatusTimer.restart()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function handleLoginOutput(data, isError) {
|
||||
var text = String(data || "")
|
||||
if (isError) _loginError += text + "\n"
|
||||
else _loginOutput += text + "\n"
|
||||
openAuthUrlFrom(text)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: root.refreshIntervalSec * 1000
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: delayedRefresh
|
||||
interval: 1000
|
||||
repeat: false
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: actionStatusTimer
|
||||
interval: 2200
|
||||
repeat: false
|
||||
onTriggered: root.actionStatus = ""
|
||||
}
|
||||
|
||||
Process {
|
||||
id: statusProcess
|
||||
running: false
|
||||
command: []
|
||||
stdout: StdioCollector { id: statusStdout; waitForEnd: true; onStreamFinished: root._statusOutput = text }
|
||||
stderr: StdioCollector { id: statusStderr; waitForEnd: true; onStreamFinished: root._statusError = text }
|
||||
onExited: function(exitCode) {
|
||||
root.refreshing = false
|
||||
var stdout = String(statusStdout.text || root._statusOutput || "")
|
||||
var stderr = String(statusStderr.text || root._statusError || "")
|
||||
if (exitCode === 0) root.applyStatus(stdout)
|
||||
else root.lastError = root.elideStatus(stderr || stdout || "Could not read Dropbox status")
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: loginProcess
|
||||
running: false
|
||||
command: []
|
||||
stdout: SplitParser { onRead: function(data) { root.handleLoginOutput(data, false) } }
|
||||
stderr: SplitParser { onRead: function(data) { root.handleLoginOutput(data, true) } }
|
||||
onExited: function(exitCode) {
|
||||
var combined = String(root._loginOutput || "") + "\n" + String(root._loginError || "")
|
||||
var opened = root.openAuthUrlFrom(combined)
|
||||
if (exitCode !== 0 && !opened) {
|
||||
root.lastError = root.elideStatus(combined || "Dropbox login failed")
|
||||
root.actionStatus = root.lastError
|
||||
} else if (!opened) {
|
||||
root.actionStatus = ""
|
||||
root.lastError = ""
|
||||
}
|
||||
delayedRefresh.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "omarchy.dropbox",
|
||||
"name": "Dropbox",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"license": "MIT",
|
||||
"description": "Dropbox status, storage usage, login, and recent synced files in the Omarchy bar.",
|
||||
"kinds": [
|
||||
"bar-widget"
|
||||
],
|
||||
"entryPoints": {
|
||||
"barWidget": "Panel.qml"
|
||||
},
|
||||
"barWidget": {
|
||||
"displayName": "Dropbox",
|
||||
"description": "Log in to Dropbox, view storage usage, and open recent synced files.",
|
||||
"category": "Files",
|
||||
"allowMultiple": false,
|
||||
"defaults": {
|
||||
"refreshIntervalSec": 60
|
||||
},
|
||||
"schema": [
|
||||
{
|
||||
"key": "refreshIntervalSec",
|
||||
"type": "integer",
|
||||
"label": "Refresh interval (seconds)",
|
||||
"min": 10,
|
||||
"max": 3600,
|
||||
"step": 5,
|
||||
"defaultValue": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import heapq
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PLAN_QUOTAS = {
|
||||
"basic": 2_000_000_000,
|
||||
"plus": 2_000_000_000_000,
|
||||
"pro": 3_000_000_000_000,
|
||||
"professional": 3_000_000_000_000,
|
||||
"essentials": 3_000_000_000_000,
|
||||
}
|
||||
|
||||
|
||||
def read_info():
|
||||
info_path = Path.home() / ".dropbox" / "info.json"
|
||||
if not info_path.exists():
|
||||
return {}
|
||||
try:
|
||||
with info_path.open("r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def dropbox_account(info):
|
||||
for key in ("personal", "business"):
|
||||
account = info.get(key)
|
||||
if isinstance(account, dict):
|
||||
return account
|
||||
return {}
|
||||
|
||||
|
||||
def command_output(command):
|
||||
try:
|
||||
completed = subprocess.run(command, check=False, capture_output=True, text=True, timeout=4)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return 1, ""
|
||||
return completed.returncode, (completed.stdout + completed.stderr).strip()
|
||||
|
||||
|
||||
def scan_dropbox(path, limit):
|
||||
total = 0
|
||||
counter = 0
|
||||
recent = []
|
||||
try:
|
||||
for root, dirs, files in os.walk(path):
|
||||
dirs[:] = [name for name in dirs if not os.path.islink(os.path.join(root, name))]
|
||||
for name in files:
|
||||
file_path = os.path.join(root, name)
|
||||
if os.path.islink(file_path):
|
||||
continue
|
||||
try:
|
||||
stat = os.stat(file_path)
|
||||
except OSError:
|
||||
continue
|
||||
total += stat.st_size
|
||||
rel = os.path.relpath(file_path, path)
|
||||
folder = os.path.dirname(rel)
|
||||
row = {
|
||||
"name": name,
|
||||
"path": file_path,
|
||||
"folder": "/" if folder in ("", ".") else folder,
|
||||
"modifiedTs": int(stat.st_mtime),
|
||||
"sizeBytes": stat.st_size,
|
||||
}
|
||||
counter += 1
|
||||
entry = (row["modifiedTs"], counter, row)
|
||||
if len(recent) < limit:
|
||||
heapq.heappush(recent, entry)
|
||||
else:
|
||||
heapq.heappushpop(recent, entry)
|
||||
except OSError:
|
||||
return 0, []
|
||||
rows = [entry[2] for entry in sorted(recent, reverse=True)]
|
||||
return total, rows
|
||||
|
||||
|
||||
def main():
|
||||
limit = 25
|
||||
if len(sys.argv) > 1:
|
||||
try:
|
||||
limit = max(1, min(100, int(sys.argv[1])))
|
||||
except ValueError:
|
||||
limit = 25
|
||||
|
||||
dropbox_cli = shutil.which("dropbox-cli")
|
||||
info = read_info()
|
||||
account = dropbox_account(info)
|
||||
account_path = account.get("path") if isinstance(account.get("path"), str) else ""
|
||||
plan = account.get("subscription_type") if isinstance(account.get("subscription_type"), str) else ""
|
||||
quota = PLAN_QUOTAS.get(plan.lower(), 0)
|
||||
authenticated = account_path != "" and Path(account_path).exists()
|
||||
|
||||
running = False
|
||||
status_text = "Not installed"
|
||||
if dropbox_cli:
|
||||
running_exit, _ = command_output([dropbox_cli, "running"])
|
||||
status_exit, status_output = command_output([dropbox_cli, "status"])
|
||||
status_text = status_output if status_exit == 0 and status_output else ("Running" if running else "Stopped")
|
||||
stopped = "not running" in status_text.lower() or status_text.lower() == "stopped"
|
||||
running = running_exit == 0 or (status_exit == 0 and status_text != "" and not stopped)
|
||||
|
||||
used, files = scan_dropbox(account_path, limit) if authenticated else (0, [])
|
||||
usage_percent = (used / quota * 100) if quota > 0 else 0
|
||||
|
||||
print(json.dumps({
|
||||
"ok": True,
|
||||
"installed": dropbox_cli is not None,
|
||||
"running": running,
|
||||
"authenticated": authenticated,
|
||||
"statusText": status_text,
|
||||
"accountPath": account_path,
|
||||
"plan": plan,
|
||||
"usedBytes": used,
|
||||
"quotaBytes": quota,
|
||||
"usagePercent": usage_percent,
|
||||
"quotaKnown": quota > 0,
|
||||
"files": files,
|
||||
}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -109,34 +109,49 @@ cat >"$TMPDIR/home/.config/omarchy/shell.json" <<'JSON'
|
||||
}
|
||||
JSON
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell right omarchy.tailscale
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar add omarchy.tailscale
|
||||
jq -e '
|
||||
def ids: map(.id // .);
|
||||
.bar.layout.right | ids == ["omarchy.tray", "omarchy.tailscale", "omarchy.bluetooth"]
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "shell config appends right widgets after tray"
|
||||
pass "shell config appends widgets to right by default"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell left local.left
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar add local.left left
|
||||
jq -e '
|
||||
def ids: map(.id // .);
|
||||
.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces", "local.left"]
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "shell config appends left widgets after workspaces"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell center local.center
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar add local.center center
|
||||
jq -e '
|
||||
def ids: map(.id // .);
|
||||
.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "local.center"]
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "shell config appends center widgets after weather"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell right local.first
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar add local.first right
|
||||
jq -e '
|
||||
def ids: map(.id // .);
|
||||
.bar.layout.right | ids == ["omarchy.tray", "local.first", "omarchy.tailscale", "omarchy.bluetooth"]
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "shell config moves existing widgets without duplicates"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar show | jq -e '
|
||||
def ids: map(.id // .);
|
||||
.bar.layout.right | ids == ["omarchy.tray", "local.first", "omarchy.tailscale", "omarchy.bluetooth"]
|
||||
' >/dev/null
|
||||
pass "shell config shows bar json"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-config-shell-bar drop local.left
|
||||
jq -e '
|
||||
def ids: map(.id // .);
|
||||
(.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces"]) and
|
||||
(.bar.layout.center | ids == ["omarchy.clock", "omarchy.weather", "local.center"]) and
|
||||
(.bar.layout.right | ids == ["omarchy.tray", "local.first", "omarchy.tailscale", "omarchy.bluetooth"])
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "shell config drops widgets from any section"
|
||||
|
||||
cat >"$TMPDIR/home/.config/omarchy/shell.json" <<'JSON'
|
||||
{
|
||||
"version": 1,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
run_node_test "dropbox model helpers" <<'JS'
|
||||
const dropbox = requireFromRoot('shell/plugins/panels/dropbox/Model.js')
|
||||
|
||||
assertEqual(dropbox.fileKind('photo.JPG'), 'image', 'dropbox detects image files')
|
||||
assertEqual(dropbox.fileKind('clip.webm'), 'video', 'dropbox detects video files')
|
||||
assertEqual(dropbox.fileKind('report.pdf'), 'document', 'dropbox detects document files')
|
||||
assertEqual(dropbox.fileKind('archive.zip'), 'misc', 'dropbox falls back to misc files')
|
||||
assertEqual(dropbox.formatBytes(1530), '1.53 KB', 'dropbox formats small byte counts')
|
||||
assertEqual(dropbox.formatBytes(2_000_000_000), '2 GB', 'dropbox formats gigabytes')
|
||||
assertEqual(dropbox.formatPercent(7.25), '7.3%', 'dropbox formats small percentages')
|
||||
assertEqual(dropbox.usageText(1000, 2000, true), '1 KB of 2 KB', 'dropbox formats known quota usage')
|
||||
assertEqual(dropbox.usageText(1000, 0, false), '1 KB', 'dropbox formats unknown quota usage')
|
||||
|
||||
const parsed = dropbox.parseStatus(JSON.stringify({
|
||||
installed: true,
|
||||
running: true,
|
||||
authenticated: true,
|
||||
files: [{ name: 'x.txt' }]
|
||||
}))
|
||||
assert(parsed.installed && parsed.running && parsed.authenticated, 'dropbox parses status booleans')
|
||||
assertEqual(parsed.files.length, 1, 'dropbox preserves file rows')
|
||||
|
||||
assertEqual(
|
||||
dropbox.fileMeta({ modifiedTs: 1000, folder: 'Docs' }, 1000 * 1000 + 3600 * 1000),
|
||||
'1h ago · Docs',
|
||||
'dropbox file metadata includes relative time and folder'
|
||||
)
|
||||
JS
|
||||
Reference in New Issue
Block a user