Drop walker + elephant
This commit is contained in:
+10
-1
@@ -13,6 +13,7 @@ User-installed plugins live alongside these conceptually but on disk under
|
||||
|---------------|---------------------------|-----------|---------------------------------------|
|
||||
| Bar | `omarchy.bar` | `bar` | `bar/Bar.qml` |
|
||||
| Bar settings | `omarchy.settings` | `panel` | `settings/SettingsPanel.qml` |
|
||||
| App launcher | `omarchy.app-launcher` | `overlay` | `app-launcher/AppLauncher.qml` |
|
||||
| Image picker | `omarchy.image-picker` | `overlay` | `image-picker/ImagePicker.qml` |
|
||||
| Emoji picker | `omarchy.emoji-picker` | `overlay` | `emoji-picker/EmojiPicker.qml` |
|
||||
| Clipboard mgr | `omarchy.clipboard-picker`| `overlay` | `clipboard-picker/ClipboardPicker.qml`|
|
||||
@@ -43,6 +44,14 @@ Visual editor for the bar layout. Summoned by
|
||||
- dynamic per-widget settings forms that write inline back to the
|
||||
corresponding shell.json entry
|
||||
|
||||
## App launcher
|
||||
|
||||
Quickshell-powered app launcher. It uses Quickshell's native
|
||||
`DesktopEntries` model for discovery/activation and renders inside the
|
||||
long-running shell with the legacy launcher card dimensions, colors, row
|
||||
spacing, icon sizing, and keyboard behavior. Summoned directly over shell IPC
|
||||
by the `SUPER + SPACE` binding and the Omarchy menu Apps row.
|
||||
|
||||
## Image picker
|
||||
|
||||
Fullscreen image-grid selector overlay. Used by `omarchy-menu-images`
|
||||
@@ -87,7 +96,7 @@ runs inside the long-lived `omarchy-shell` process, replacing the old
|
||||
|
||||
## Omarchy menu
|
||||
|
||||
Quickshell-powered replacement for the legacy Walker-driven `omarchy-menu`.
|
||||
Quickshell-powered Omarchy command menu.
|
||||
The menu UI lives in `menu/Menu.qml` as a first-party `menu` plugin and is
|
||||
summoned through the shell (`omarchy-shell shell summon omarchy.menu ...`),
|
||||
so it shares the long-running `omarchy-shell` process instead of starting a
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
||||
property var shell: null
|
||||
property var manifest: null
|
||||
|
||||
property bool opened: false
|
||||
property string placeholder: "\uf002 Search..."
|
||||
property string filterText: ""
|
||||
property int selectedIndex: 0
|
||||
property bool cursorActive: true
|
||||
property bool hoverArmed: false
|
||||
property var filteredEntries: []
|
||||
|
||||
property color accent: Color.menu.selected
|
||||
property color background: Color.menu.background
|
||||
property color foreground: Color.menu.text
|
||||
property color border: foreground
|
||||
property string fontFamily: Quickshell.env("OMARCHY_MENU_FONT") || "monospace"
|
||||
|
||||
property int cardWidth: 644
|
||||
property int cardHeight: 400
|
||||
property int contentMargin: 20
|
||||
property int contentSpacing: 10
|
||||
property int searchHeight: 44
|
||||
property int rowHeight: 50
|
||||
property int iconSlotWidth: 44
|
||||
property int iconSize: 24
|
||||
readonly property int listHeight: cardHeight - contentMargin * 2 - searchHeight - contentSpacing
|
||||
|
||||
function open(payloadJson) {
|
||||
var payload = ({})
|
||||
try { payload = JSON.parse(payloadJson || "{}") } catch (e) { payload = ({}) }
|
||||
|
||||
root.placeholder = payload.placeholder || "\uf002 Search..."
|
||||
root.cardWidth = Math.max(300, Number(payload.width || 644))
|
||||
var requestedListHeight = Number(payload.listHeight || payload.maxHeight || 0)
|
||||
root.cardHeight = requestedListHeight > 0
|
||||
? root.contentMargin * 2 + root.searchHeight + root.contentSpacing + requestedListHeight
|
||||
: 400
|
||||
|
||||
root.opened = true
|
||||
root.filterText = payload.query || ""
|
||||
root.selectedIndex = 0
|
||||
root.cursorActive = true
|
||||
root.hoverArmed = false
|
||||
root.rebuildDisplay()
|
||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
|
||||
function close() {
|
||||
root.opened = false
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
root.opened = false
|
||||
if (root.shell && typeof root.shell.hide === "function")
|
||||
root.shell.hide((root.manifest && root.manifest.id) || "omarchy.app-launcher")
|
||||
}
|
||||
|
||||
function withAlpha(color, alpha) {
|
||||
return Qt.rgba(color.r, color.g, color.b, alpha)
|
||||
}
|
||||
|
||||
function fileUrl(path) {
|
||||
return "file://" + String(path).split("/").map(encodeURIComponent).join("/")
|
||||
}
|
||||
|
||||
function iconSource(icon) {
|
||||
var value = String(icon || "")
|
||||
if (value.length === 0) return Quickshell.iconPath("application-x-executable", true)
|
||||
if (value.indexOf("file://") === 0 || value.indexOf("image://") === 0) return value
|
||||
if (value.charAt(0) === "/") return root.fileUrl(value)
|
||||
return Quickshell.iconPath(value, true)
|
||||
}
|
||||
|
||||
function entryName(entry) {
|
||||
return String((entry && entry.name) || (entry && entry.id) || "")
|
||||
}
|
||||
|
||||
function entrySubtext(entry) {
|
||||
return String((entry && entry.genericName) || "")
|
||||
}
|
||||
|
||||
function entrySortKey(entry) {
|
||||
return String((entry && entry.id) || root.entryName(entry)).toLowerCase()
|
||||
}
|
||||
|
||||
function entrySearchText(entry) {
|
||||
if (!entry) return ""
|
||||
var keywords = ""
|
||||
try { keywords = (entry.keywords || []).join(" ") } catch (e) { keywords = "" }
|
||||
return [entry.name, entry.genericName, entry.comment, keywords, entry.id].join(" ").toLowerCase()
|
||||
}
|
||||
|
||||
function isHiddenEntry(entry) {
|
||||
var id = String((entry && entry.id) || "")
|
||||
return id === "avahi-discover" || id === "bssh" || id === "bvnc"
|
||||
}
|
||||
|
||||
function fuzzyScore(entry, query) {
|
||||
var q = String(query || "").trim().toLowerCase()
|
||||
if (!q) return 0
|
||||
|
||||
var name = root.entryName(entry).toLowerCase()
|
||||
var id = String((entry && entry.id) || "").toLowerCase()
|
||||
var haystack = root.entrySearchText(entry)
|
||||
var directName = name.indexOf(q)
|
||||
var directId = id.indexOf(q)
|
||||
if (directName === 0) return 10000 - name.length
|
||||
if (directId === 0) return 9500 - id.length
|
||||
if (directName > 0) return 8000 - directName * 10 - name.length
|
||||
if (directId > 0) return 7600 - directId * 10 - id.length
|
||||
|
||||
var hayIndex = haystack.indexOf(q)
|
||||
if (hayIndex >= 0) return 6000 - hayIndex
|
||||
|
||||
var pos = 0
|
||||
var first = -1
|
||||
var last = -1
|
||||
for (var i = 0; i < haystack.length && pos < q.length; i++) {
|
||||
if (haystack.charAt(i) !== q.charAt(pos)) continue
|
||||
if (first < 0) first = i
|
||||
last = i
|
||||
pos++
|
||||
}
|
||||
if (pos !== q.length) return -1
|
||||
return 3000 - (last - first) - haystack.length * 0.01
|
||||
}
|
||||
|
||||
function sortedEntries(query) {
|
||||
var values = DesktopEntries.applications.values || []
|
||||
var q = String(query || "").trim()
|
||||
var rows = []
|
||||
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var entry = values[i]
|
||||
if (!entry || entry.noDisplay || root.isHiddenEntry(entry)) continue
|
||||
var name = root.entryName(entry)
|
||||
if (!name) continue
|
||||
var score = root.fuzzyScore(entry, q)
|
||||
if (score < 0) continue
|
||||
rows.push({ entry: entry, score: score, key: root.entrySortKey(entry), name: name.toLowerCase() })
|
||||
}
|
||||
|
||||
rows.sort(function(a, b) {
|
||||
if (q && a.score !== b.score) return b.score - a.score
|
||||
if (a.key < b.key) return -1
|
||||
if (a.key > b.key) return 1
|
||||
if (a.name < b.name) return -1
|
||||
if (a.name > b.name) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
function rebuildDisplay() {
|
||||
displayModel.clear()
|
||||
var rows = root.sortedEntries(root.filterText)
|
||||
var entries = []
|
||||
var count = Math.min(rows.length, 256)
|
||||
for (var i = 0; i < count; i++) {
|
||||
var entry = rows[i].entry
|
||||
entries.push(entry)
|
||||
displayModel.append({
|
||||
name: root.entryName(entry),
|
||||
subtext: root.entrySubtext(entry),
|
||||
icon: String(entry.icon || "")
|
||||
})
|
||||
}
|
||||
root.filteredEntries = entries
|
||||
|
||||
if (displayModel.count === 0) root.selectedIndex = 0
|
||||
else if (root.selectedIndex >= displayModel.count) root.selectedIndex = displayModel.count - 1
|
||||
else if (root.selectedIndex < 0) root.selectedIndex = 0
|
||||
|
||||
Qt.callLater(function() {
|
||||
if (displayModel.count > 0) resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
||||
})
|
||||
}
|
||||
|
||||
function setFilter(nextFilter) {
|
||||
root.filterText = nextFilter
|
||||
root.selectedIndex = 0
|
||||
root.cursorActive = true
|
||||
root.rebuildDisplay()
|
||||
}
|
||||
|
||||
function select(delta) {
|
||||
if (displayModel.count === 0) return
|
||||
root.cursorActive = true
|
||||
root.selectedIndex = (root.selectedIndex + delta + displayModel.count) % displayModel.count
|
||||
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
function activateIndex(index) {
|
||||
if (index < 0 || index >= root.filteredEntries.length) return
|
||||
var entry = root.filteredEntries[index]
|
||||
if (!entry) return
|
||||
root.dismiss()
|
||||
entry.execute()
|
||||
}
|
||||
|
||||
ListModel { id: displayModel }
|
||||
|
||||
Connections {
|
||||
target: DesktopEntries.applications
|
||||
function onValuesChanged() {
|
||||
if (root.opened) root.rebuildDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: panel
|
||||
visible: root.opened
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "omarchy-app-launcher"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.withAlpha(root.background, 0.5)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.dismiss()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
width: Math.min(root.cardWidth, panel.width - Style.gapsOut * 2)
|
||||
height: Math.min(root.cardHeight, panel.height - Style.gapsOut * 2)
|
||||
radius: Style.cornerRadius
|
||||
anchors.centerIn: parent
|
||||
color: root.withAlpha(root.background, 0.95)
|
||||
border.color: root.border
|
||||
border.width: 2
|
||||
clip: true
|
||||
|
||||
MouseArea { anchors.fill: parent; onClicked: {} }
|
||||
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.priority: Keys.BeforeItem
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (root.filterText.length > 0) root.setFilter("")
|
||||
else root.dismiss()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
if (root.filterText.length > 0) root.setFilter(root.filterText.slice(0, -1))
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
root.select(-1)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
root.select(1)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
root.select(-6)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_PageDown) {
|
||||
root.select(6)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Home) {
|
||||
if (displayModel.count > 0) {
|
||||
root.cursorActive = true
|
||||
root.selectedIndex = 0
|
||||
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_End) {
|
||||
if (displayModel.count > 0) {
|
||||
root.cursorActive = true
|
||||
root.selectedIndex = displayModel.count - 1
|
||||
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
root.activateIndex(root.selectedIndex)
|
||||
event.accepted = true
|
||||
} else if (event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier)) {
|
||||
root.setFilter(root.filterText + event.text)
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.contentMargin
|
||||
spacing: root.contentSpacing
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: root.searchHeight
|
||||
radius: 0
|
||||
color: root.background
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.filterText || root.placeholder
|
||||
color: root.foreground
|
||||
opacity: root.filterText ? 1 : 0.5
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 18
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - root.searchHeight - root.contentSpacing
|
||||
|
||||
ListView {
|
||||
id: resultList
|
||||
anchors.fill: parent
|
||||
model: displayModel
|
||||
clip: true
|
||||
spacing: 0
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
delegate: Rectangle {
|
||||
id: row
|
||||
required property int index
|
||||
required property string name
|
||||
required property string subtext
|
||||
required property string icon
|
||||
|
||||
readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex
|
||||
|
||||
width: ListView.view.width
|
||||
height: root.rowHeight
|
||||
radius: 0
|
||||
color: row.hasCursor ? root.withAlpha(root.foreground, 0.07) : "transparent"
|
||||
|
||||
Item {
|
||||
id: iconSlot
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: root.iconSlotWidth
|
||||
height: parent.height
|
||||
|
||||
IconImage {
|
||||
id: appIcon
|
||||
anchors.centerIn: parent
|
||||
implicitSize: root.iconSize
|
||||
width: root.iconSize
|
||||
height: root.iconSize
|
||||
source: root.iconSource(row.icon)
|
||||
asynchronous: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: appIcon.status === Image.Error
|
||||
text: "?"
|
||||
color: row.hasCursor ? root.accent : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 18
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: iconSlot.right
|
||||
anchors.leftMargin: 14
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 14
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: row.name
|
||||
color: row.hasCursor ? root.accent : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 18
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPositionChanged: function(mouse) {
|
||||
root.hoverArmed = true
|
||||
root.cursorActive = true
|
||||
root.selectedIndex = row.index
|
||||
}
|
||||
onContainsMouseChanged: if (containsMouse && root.hoverArmed) {
|
||||
root.cursorActive = true
|
||||
root.selectedIndex = row.index
|
||||
}
|
||||
onClicked: {
|
||||
root.cursorActive = true
|
||||
root.selectedIndex = row.index
|
||||
root.activateIndex(row.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 14
|
||||
visible: displayModel.count === 0
|
||||
text: "No Results"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 18
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "omarchy.app-launcher",
|
||||
"name": "App launcher",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "A Quickshell-powered application launcher",
|
||||
"kinds": [
|
||||
"overlay"
|
||||
],
|
||||
"keepLoaded": true,
|
||||
"entryPoints": {
|
||||
"overlay": "AppLauncher.qml"
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,15 @@ import qs.Commons
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
||||
property bool opened: false
|
||||
property string filterText: ""
|
||||
property int selectedIndex: 0
|
||||
property bool cursorActive: false
|
||||
property var items: []
|
||||
property var history: []
|
||||
|
||||
property string historyPath: Quickshell.env("HOME") + "/.local/state/omarchy/clipboard-history.json"
|
||||
property string captureScript: root.omarchyPath + "/shell/scripts/clipboard-capture.sh"
|
||||
property color accent: Color.menu.selected
|
||||
property color background: Color.menu.background
|
||||
property color foreground: Color.menu.text
|
||||
@@ -31,12 +34,7 @@ Item {
|
||||
root.filterText = ""
|
||||
root.selectedIndex = 0
|
||||
root.cursorActive = false
|
||||
|
||||
// Trigger fetch
|
||||
fetchProc.collected = ""
|
||||
fetchProc.command = ["bash", "-lc", "elephant query --json 'clipboard;;100'"]
|
||||
fetchProc.running = true
|
||||
|
||||
root.rebuildDisplay()
|
||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
|
||||
@@ -53,35 +51,111 @@ Item {
|
||||
return Qt.rgba(color.r, color.g, color.b, alpha)
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
return "'" + String(value || "").replace(/'/g, "'\\''") + "'"
|
||||
}
|
||||
|
||||
function fileUrl(path) {
|
||||
return "file://" + String(path).split("/").map(encodeURIComponent).join("/")
|
||||
}
|
||||
|
||||
function normalizeEntry(value) {
|
||||
if (typeof value === "string") {
|
||||
return value.length > 0 ? { type: "text", text: value } : null
|
||||
}
|
||||
if (!value || typeof value !== "object") return null
|
||||
|
||||
var type = String(value.type || value.kind || "")
|
||||
if (type === "text") {
|
||||
var text = String(value.text || "")
|
||||
return text.length > 0 ? { type: "text", text: text } : null
|
||||
}
|
||||
if (type === "image") {
|
||||
var path = String(value.path || "")
|
||||
if (!path) return null
|
||||
return {
|
||||
type: "image",
|
||||
path: path,
|
||||
mime: String(value.mime || "image/png")
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function entryKey(entry) {
|
||||
if (!entry) return ""
|
||||
if (entry.type === "image") return "image:" + String(entry.path || "")
|
||||
return "text:" + String(entry.text || "")
|
||||
}
|
||||
|
||||
function loadHistory(raw) {
|
||||
try {
|
||||
var parsed = JSON.parse(String(raw || "[]"))
|
||||
var next = []
|
||||
if (Array.isArray(parsed)) {
|
||||
for (var i = 0; i < parsed.length; i++) {
|
||||
var entry = root.normalizeEntry(parsed[i])
|
||||
if (entry) next.push(entry)
|
||||
}
|
||||
}
|
||||
root.history = next
|
||||
} catch (e) {
|
||||
root.history = []
|
||||
}
|
||||
if (root.opened) root.rebuildDisplay()
|
||||
}
|
||||
|
||||
function saveHistory() {
|
||||
historyFile.setText(JSON.stringify(root.history.slice(0, 100), null, 2) + "\n")
|
||||
}
|
||||
|
||||
function addClipboardEntry(entry) {
|
||||
var normalized = root.normalizeEntry(entry)
|
||||
if (!normalized) return
|
||||
|
||||
var key = root.entryKey(normalized)
|
||||
var next = [normalized]
|
||||
for (var i = 0; i < root.history.length && next.length < 100; i++) {
|
||||
var existing = root.normalizeEntry(root.history[i])
|
||||
if (!existing || root.entryKey(existing) === key) continue
|
||||
next.push(existing)
|
||||
}
|
||||
root.history = next
|
||||
root.saveHistory()
|
||||
if (root.opened) root.rebuildDisplay()
|
||||
}
|
||||
|
||||
function addClipboardJson(line) {
|
||||
var raw = String(line || "").trim()
|
||||
if (!raw) return
|
||||
try { root.addClipboardEntry(JSON.parse(raw)) } catch (e) {}
|
||||
}
|
||||
|
||||
function rebuildDisplay() {
|
||||
var query = root.filterText.trim().toLowerCase()
|
||||
|
||||
|
||||
displayModel.clear()
|
||||
var outCount = 0
|
||||
|
||||
for (var i = 0; i < root.items.length; i++) {
|
||||
var entry = root.items[i]
|
||||
var isPassword = (entry.meta === "password" || entry.preview_type === "password")
|
||||
|
||||
var textMatch = false
|
||||
if (isPassword) {
|
||||
textMatch = false // Passwords shouldn't match plain text search queries
|
||||
} else {
|
||||
textMatch = (entry.preview && entry.preview.toLowerCase().indexOf(query) >= 0)
|
||||
}
|
||||
|
||||
if (!query || textMatch) {
|
||||
displayModel.append({
|
||||
identifier: entry.identifier,
|
||||
previewText: entry.preview_type === "text" ? entry.preview.replace(/\n/g, " ") : "",
|
||||
previewImage: entry.preview_type === "file" ? ("file://" + entry.preview) : "",
|
||||
previewType: entry.preview_type || "text",
|
||||
isPassword: isPassword,
|
||||
index: outCount
|
||||
})
|
||||
outCount++
|
||||
if (outCount >= 50) break
|
||||
}
|
||||
|
||||
for (var i = 0; i < root.history.length; i++) {
|
||||
var entry = root.normalizeEntry(root.history[i])
|
||||
if (!entry) continue
|
||||
|
||||
var isImage = entry.type === "image"
|
||||
var searchable = isImage ? ("image " + String(entry.mime || "")) : String(entry.text || "")
|
||||
if (query && searchable.toLowerCase().indexOf(query) < 0) continue
|
||||
|
||||
displayModel.append({
|
||||
entryType: entry.type,
|
||||
fullText: isImage ? "" : String(entry.text || ""),
|
||||
previewText: isImage ? "Image" : String(entry.text || "").replace(/\s+/g, " "),
|
||||
previewImage: isImage ? root.fileUrl(entry.path) : "",
|
||||
path: isImage ? String(entry.path || "") : "",
|
||||
mime: isImage ? String(entry.mime || "image/png") : "text/plain",
|
||||
index: outCount
|
||||
})
|
||||
outCount++
|
||||
if (outCount >= 50) break
|
||||
}
|
||||
|
||||
if (displayModel.count === 0) selectedIndex = 0
|
||||
@@ -114,40 +188,60 @@ Item {
|
||||
function activateIndex(index) {
|
||||
if (index < 0 || index >= displayModel.count) return
|
||||
var row = displayModel.get(index)
|
||||
root.applySelected(row.identifier)
|
||||
root.applySelected(row)
|
||||
}
|
||||
|
||||
function applySelected(identifier) {
|
||||
if (!identifier) return
|
||||
function applySelected(row) {
|
||||
if (!row) return
|
||||
root.opened = false
|
||||
var escId = identifier.replace(/'/g, "'\\''")
|
||||
Quickshell.execDetached(["bash", "-lc", "elephant activate 'clipboard;" + escId + ";copy;;'; sleep 0.15; wtype -M shift -k Insert -m shift 2>/dev/null || true"])
|
||||
if (row.entryType === "image") {
|
||||
Quickshell.execDetached(["bash", "-lc", "wl-copy --type " + root.shellQuote(row.mime) + " < " + root.shellQuote(row.path) + "; sleep 0.15; wtype -M shift -k Insert -m shift 2>/dev/null || true"])
|
||||
} else if (row.fullText) {
|
||||
Quickshell.execDetached(["bash", "-lc", "printf %s " + root.shellQuote(row.fullText) + " | wl-copy; sleep 0.15; wtype -M shift -k Insert -m shift 2>/dev/null || true"])
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: initProc.running = true
|
||||
|
||||
ListModel { id: displayModel }
|
||||
|
||||
FileView {
|
||||
id: historyFile
|
||||
path: root.historyPath
|
||||
watchChanges: true
|
||||
atomicWrites: true
|
||||
printErrors: false
|
||||
onLoaded: root.loadHistory(text())
|
||||
onLoadFailed: root.loadHistory("[]")
|
||||
onFileChanged: reload()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fetchProc
|
||||
property string collected: ""
|
||||
stdout: SplitParser {
|
||||
onRead: function(data) { fetchProc.collected += data + "\n" }
|
||||
}
|
||||
id: initProc
|
||||
command: ["bash", "-lc", "mkdir -p ~/.local/state/omarchy"]
|
||||
onExited: {
|
||||
var lines = fetchProc.collected.split("\n")
|
||||
var newItems = []
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim()
|
||||
if (!line) continue
|
||||
try {
|
||||
var parsed = JSON.parse(line)
|
||||
if (parsed && parsed.item) {
|
||||
newItems.push(parsed.item)
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
root.items = newItems
|
||||
root.rebuildDisplay()
|
||||
currentProc.command = [root.captureScript]
|
||||
currentProc.running = true
|
||||
watchProc.command = ["wl-paste", "--watch", root.captureScript]
|
||||
watchProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: currentProc
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.addClipboardJson(text)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: watchProc
|
||||
stdout: SplitParser {
|
||||
onRead: function(data) { root.addClipboardJson(data) }
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: panel
|
||||
visible: root.opened
|
||||
@@ -261,10 +355,10 @@ Item {
|
||||
|
||||
delegate: Rectangle {
|
||||
required property int index
|
||||
required property string identifier
|
||||
required property string entryType
|
||||
required property string previewText
|
||||
required property string previewType
|
||||
required property bool isPassword
|
||||
required property string fullText
|
||||
required property string previewImage
|
||||
|
||||
readonly property bool hasCursor: root.cursorActive && index === root.selectedIndex
|
||||
|
||||
@@ -275,33 +369,33 @@ Item {
|
||||
border.color: hasCursor ? Style.hoverBorderFor(root.foreground, root.accent) : "transparent"
|
||||
border.width: hasCursor ? Style.hoverBorderWidth : 0
|
||||
|
||||
Rectangle {
|
||||
visible: false
|
||||
width: Style.space(4)
|
||||
height: parent.height - Style.space(18)
|
||||
radius: Math.min(root.cornerRadius, Style.space(4))
|
||||
color: root.accent
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Style.space(8)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.space(12)
|
||||
anchors.rightMargin: Style.space(12)
|
||||
anchors.topMargin: Style.space(8)
|
||||
anchors.bottomMargin: Style.space(8)
|
||||
spacing: Style.space(10)
|
||||
|
||||
Image {
|
||||
visible: parent.parent.entryType === "image"
|
||||
width: visible ? parent.height : 0
|
||||
height: parent.height
|
||||
source: parent.parent.previewImage
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
width: parent.width - (parent.parent.entryType === "image" ? parent.height + parent.spacing : 0)
|
||||
height: parent.height
|
||||
text: parent.parent.isPassword ? "••••••••" : (parent.parent.previewType === "text" ? parent.parent.previewText : "Image")
|
||||
text: parent.parent.previewText
|
||||
color: parent.parent.hasCursor ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.title
|
||||
font.italic: parent.parent.previewType === "file" || parent.parent.isPassword
|
||||
opacity: (parent.parent.previewType === "file" || parent.parent.isPassword) ? 0.6 : 1.0
|
||||
font.italic: parent.parent.entryType === "image"
|
||||
opacity: parent.parent.entryType === "image" ? 0.72 : 1.0
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -309,7 +403,6 @@ Item {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
@@ -335,13 +428,13 @@ Item {
|
||||
border.width: Style.normalBorderWidth
|
||||
clip: true
|
||||
|
||||
property var activeRow: root.cursorActive && displayModel.count > 0 && root.selectedIndex >= 0 && root.selectedIndex < displayModel.count ? displayModel.get(root.selectedIndex) : null
|
||||
property var activeRow: displayModel.count > 0 && root.selectedIndex >= 0 && root.selectedIndex < displayModel.count ? displayModel.get(root.selectedIndex) : null
|
||||
|
||||
Text {
|
||||
visible: parent.activeRow && parent.activeRow.previewType === "text"
|
||||
visible: parent.activeRow && parent.activeRow.entryType === "text"
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.space(16)
|
||||
text: parent.activeRow ? (parent.activeRow.isPassword ? "••••••••" : parent.activeRow.previewText) : ""
|
||||
text: parent.activeRow ? parent.activeRow.fullText : ""
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.title
|
||||
@@ -351,11 +444,13 @@ Item {
|
||||
}
|
||||
|
||||
Image {
|
||||
visible: parent.activeRow && parent.activeRow.previewType === "file"
|
||||
visible: parent.activeRow && parent.activeRow.entryType === "image"
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.space(16)
|
||||
source: parent.activeRow ? parent.activeRow.previewImage : ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,7 +471,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.items.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”"
|
||||
text: root.history.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”"
|
||||
color: root.foreground
|
||||
opacity: 0.7
|
||||
font.family: root.fontFamily
|
||||
|
||||
+161
-9
@@ -18,10 +18,14 @@ Item {
|
||||
var payload = ({})
|
||||
try { payload = JSON.parse(payloadJson || "{}") } catch (e) { payload = ({}) }
|
||||
|
||||
root.pendingInitialMenu = payload.initialMenu || payload.menu || "root"
|
||||
if (payload.fontFamily) root.fontFamily = payload.fontFamily
|
||||
|
||||
root.openExistingMenu(root.pendingInitialMenu)
|
||||
if (payload.mode === "select" || payload.mode === "input") {
|
||||
root.openDmenu(payload)
|
||||
} else {
|
||||
root.pendingInitialMenu = payload.initialMenu || payload.menu || "root"
|
||||
root.openExistingMenu(root.pendingInitialMenu)
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -37,6 +41,15 @@ Item {
|
||||
property var defaultMenuItems: []
|
||||
property var userMenuItems: []
|
||||
property bool opened: false
|
||||
property string mode: "menu"
|
||||
readonly property bool dmenuActive: mode === "select" || mode === "input"
|
||||
property string dmenuPrompt: ""
|
||||
property var dmenuOptions: []
|
||||
property string selectionFile: ""
|
||||
property string doneFile: ""
|
||||
property int dmenuWidth: 300
|
||||
property int dmenuMaxHeight: 0
|
||||
property bool requestActive: false
|
||||
property bool rowsLoaded: false
|
||||
property string activeMenu: "root"
|
||||
property string filterText: ""
|
||||
@@ -64,14 +77,40 @@ Item {
|
||||
property int dividerHeight: Style.space(17)
|
||||
property bool searchDivider: false
|
||||
property int layoutSerial: 0
|
||||
property int cardWidth: Math.min((root.activeMenu === "trigger.capture.screenrecord" || root.activeMenu === "style.font") ? Style.space(520) : Style.space(300), panel.width - Style.gapsOut * 2)
|
||||
property int visibleRowsHeight: rowListHeight(layoutSerial, displayModel.count, filterText, searchDivider)
|
||||
property int cardHeight: Math.min(Math.max(Style.space(220), contentMargin * 2 + headerHeight + contentSpacing + visibleRowsHeight), panel.height - Style.gapsOut * 2)
|
||||
property int cardWidth: Math.min(root.dmenuActive ? Style.space(root.dmenuWidth) : ((root.activeMenu === "trigger.capture.screenrecord" || root.activeMenu === "style.font") ? Style.space(520) : Style.space(300)), panel.width - Style.gapsOut * 2)
|
||||
property int visibleRowsHeight: root.dmenuActive ? dmenuRowListHeight(layoutSerial, displayModel.count, filterText) : rowListHeight(layoutSerial, displayModel.count, filterText, searchDivider)
|
||||
property int cardHeight: root.dmenuActive
|
||||
? Math.min(contentMargin * 2 + headerHeight + (mode === "input" ? 0 : contentSpacing + visibleRowsHeight), panel.height - Style.gapsOut * 2)
|
||||
: Math.min(Math.max(Style.space(220), contentMargin * 2 + headerHeight + contentSpacing + visibleRowsHeight), panel.height - Style.gapsOut * 2)
|
||||
|
||||
function withAlpha(color, alpha) {
|
||||
return Qt.rgba(color.r, color.g, color.b, alpha)
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
return "'" + String(value || "").replace(/'/g, "'\\''") + "'"
|
||||
}
|
||||
|
||||
function finishRequest(selection) {
|
||||
if (!root.requestActive || !root.doneFile) {
|
||||
root.opened = false
|
||||
return
|
||||
}
|
||||
|
||||
var activeSelectionFile = root.selectionFile
|
||||
var activeDoneFile = root.doneFile
|
||||
root.requestActive = false
|
||||
root.selectionFile = ""
|
||||
root.doneFile = ""
|
||||
|
||||
if (selection === null || selection === undefined) {
|
||||
resultProc.command = ["bash", "-lc", ": > " + root.shellQuote(activeDoneFile)]
|
||||
} else {
|
||||
resultProc.command = ["bash", "-lc", "printf '%s\\n' " + root.shellQuote(selection) + " > " + root.shellQuote(activeSelectionFile) + "; : > " + root.shellQuote(activeDoneFile)]
|
||||
}
|
||||
resultProc.running = true
|
||||
}
|
||||
|
||||
function rowHeightForDetail(detail) {
|
||||
return root.filterText && detail ? root.detailRowHeight : root.baseRowHeight
|
||||
}
|
||||
@@ -94,6 +133,20 @@ Item {
|
||||
return total
|
||||
}
|
||||
|
||||
function dmenuRowListHeight(_serial, _count, _filter) {
|
||||
if (root.mode === "input") return 0
|
||||
if (displayModel.count === 0) return root.baseRowHeight
|
||||
|
||||
var count = Math.min(displayModel.count, 10)
|
||||
var total = 0
|
||||
for (var i = 0; i < count; i++) {
|
||||
if (i > 0) total += root.rowSpacing
|
||||
total += root.baseRowHeight
|
||||
}
|
||||
|
||||
return root.dmenuMaxHeight > 0 ? Math.min(total, Style.space(root.dmenuMaxHeight)) : total
|
||||
}
|
||||
|
||||
function item(id) {
|
||||
return root.items[id] || null
|
||||
}
|
||||
@@ -458,7 +511,52 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildDmenuDisplay() {
|
||||
displayModel.clear()
|
||||
root.searchDivider = false
|
||||
|
||||
if (root.mode === "input") {
|
||||
layoutSerial += 1
|
||||
return
|
||||
}
|
||||
|
||||
var query = root.filterText.trim().toLowerCase()
|
||||
for (var i = 0; i < root.dmenuOptions.length; i++) {
|
||||
var label = String(root.dmenuOptions[i] || "")
|
||||
if (query && label.toLowerCase().indexOf(query) < 0) continue
|
||||
displayModel.append({
|
||||
itemId: "dmenu." + i,
|
||||
kind: "dmenu",
|
||||
icon: "",
|
||||
label: label,
|
||||
target: "",
|
||||
detail: "",
|
||||
path: "",
|
||||
childCount: 0,
|
||||
action: "",
|
||||
provider: "",
|
||||
score: i,
|
||||
section: ""
|
||||
})
|
||||
}
|
||||
|
||||
layoutSerial += 1
|
||||
|
||||
if (displayModel.count === 0) selectedIndex = 0
|
||||
else if (selectedIndex >= displayModel.count) selectedIndex = displayModel.count - 1
|
||||
else if (selectedIndex < 0) selectedIndex = 0
|
||||
|
||||
Qt.callLater(function() {
|
||||
if (displayModel.count > 0) resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
||||
})
|
||||
}
|
||||
|
||||
function rebuildDisplay() {
|
||||
if (root.dmenuActive) {
|
||||
root.rebuildDmenuDisplay()
|
||||
return
|
||||
}
|
||||
|
||||
displayModel.clear()
|
||||
|
||||
if (!root.rowsLoaded) return
|
||||
@@ -540,7 +638,7 @@ Item {
|
||||
root.filterText = nextFilter
|
||||
root.selectedIndex = 0
|
||||
root.cursorActive = false
|
||||
if (root.filterText.trim()) root.loadProvidersForSearch()
|
||||
if (!root.dmenuActive && root.filterText.trim()) root.loadProvidersForSearch()
|
||||
root.rebuildDisplay()
|
||||
}
|
||||
|
||||
@@ -571,6 +669,16 @@ Item {
|
||||
}
|
||||
|
||||
function activateIndex(index) {
|
||||
if (root.dmenuActive) {
|
||||
if (root.mode === "input") {
|
||||
root.applyDmenuSelection(root.filterText)
|
||||
return
|
||||
}
|
||||
if (index < 0 || index >= displayModel.count) return
|
||||
root.applyDmenuSelection(displayModel.get(index).label)
|
||||
return
|
||||
}
|
||||
|
||||
if (index < 0 || index >= displayModel.count) return
|
||||
|
||||
var row = displayModel.get(index)
|
||||
@@ -583,6 +691,13 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function applyDmenuSelection(value) {
|
||||
applySerial = requestSerial
|
||||
opened = false
|
||||
filterText = ""
|
||||
root.finishRequest(value)
|
||||
}
|
||||
|
||||
function applySelected(id, action) {
|
||||
if (!id) { cancel(); return }
|
||||
applySerial = requestSerial
|
||||
@@ -592,12 +707,17 @@ Item {
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (root.dmenuActive) root.finishRequest(null)
|
||||
opened = false
|
||||
filterText = ""
|
||||
}
|
||||
|
||||
function openExistingMenu(initialMenu) {
|
||||
requestSerial += 1
|
||||
mode = "menu"
|
||||
requestActive = false
|
||||
selectionFile = ""
|
||||
doneFile = ""
|
||||
activeMenu = root.item(initialMenu) ? initialMenu : "root"
|
||||
navStack = []
|
||||
filterText = ""
|
||||
@@ -610,6 +730,27 @@ Item {
|
||||
|
||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
|
||||
function openDmenu(payload) {
|
||||
requestSerial += 1
|
||||
mode = payload.mode === "input" ? "input" : "select"
|
||||
dmenuPrompt = String(payload.prompt || (mode === "input" ? "Input" : "Select"))
|
||||
dmenuOptions = Array.isArray(payload.options) ? payload.options : []
|
||||
selectionFile = String(payload.selectionFile || "")
|
||||
doneFile = String(payload.doneFile || "")
|
||||
requestActive = !!doneFile
|
||||
dmenuWidth = Math.max(1, Number(payload.width || 300))
|
||||
dmenuMaxHeight = Math.max(0, Number(payload.maxHeight || 0))
|
||||
activeMenu = "root"
|
||||
navStack = []
|
||||
filterText = ""
|
||||
selectedIndex = 0
|
||||
cursorActive = false
|
||||
opened = true
|
||||
rebuildDisplay()
|
||||
|
||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
ListModel { id: displayModel }
|
||||
|
||||
// ----------------------------------------------------------- IPC surface
|
||||
@@ -697,6 +838,14 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: resultProc
|
||||
onExited: {
|
||||
if (root.applySerial === root.requestSerial)
|
||||
root.opened = false
|
||||
}
|
||||
}
|
||||
|
||||
// The JSONC sources are watched so live edits to the default file (or the
|
||||
// user extension at ~/.config/omarchy/extensions/omarchy-menu.jsonc) take
|
||||
// effect without restarting the shell.
|
||||
@@ -840,7 +989,10 @@ Item {
|
||||
if (!root.filterText) root.goBack()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Right) {
|
||||
if (root.cursorActive) root.activateIndex(root.selectedIndex)
|
||||
if (root.dmenuActive) {
|
||||
if (root.mode === "input") root.applyDmenuSelection(root.filterText)
|
||||
else if (displayModel.count > 0) root.activateIndex(root.cursorActive ? root.selectedIndex : 0)
|
||||
} else if (root.cursorActive) root.activateIndex(root.selectedIndex)
|
||||
else if (displayModel.count > 0) root.cursorActive = true
|
||||
event.accepted = true
|
||||
} else if (event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier)) {
|
||||
@@ -866,7 +1018,7 @@ Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.filterText || ((root.item(root.activeMenu) ? root.item(root.activeMenu).label : "Go") + "…")
|
||||
text: root.filterText || (root.dmenuActive ? (root.dmenuPrompt + "…") : ((root.item(root.activeMenu) ? root.item(root.activeMenu).label : "Go") + "…"))
|
||||
color: root.foreground
|
||||
opacity: root.filterText ? 1 : 0.58
|
||||
font.family: root.fontFamily
|
||||
@@ -1034,7 +1186,7 @@ Item {
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Style.space(8)
|
||||
visible: displayModel.count === 0
|
||||
visible: displayModel.count === 0 && root.mode !== "input"
|
||||
|
||||
Text {
|
||||
text: ""
|
||||
|
||||
Reference in New Issue
Block a user