import Quickshell import Quickshell.Io import Quickshell.Wayland 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 filterText: "" property int selectedIndex: 0 property bool cursorActive: false property var emojis: [] property var filteredEmojis: [] // Shares the [menu] surface tokens — themes that style the menu also // style emojis. Selected-cell colors composed in the // singleton so consumers drop them straight into Rectangle bindings. property color background: Color.menu.background property color foreground: Color.menu.text property color border: Color.menu.border property color scrim: Color.menu.scrim property color selectedBackground: Color.menu.selectedBackground property color selectedText: Color.menu.selectedText property color selectedBorder: Color.menu.selectedBorder readonly property int cornerRadius: Style.cornerRadius property string fontFamily: Quickshell.env("OMARCHY_MENU_FONT") || "monospace" property int contentMargin: Style.spacing.panelPadding property int headerHeight: Math.max(Style.space(34), Style.font.title + Style.spacing.controlPaddingY * 2) property int contentSpacing: Style.spacing.md property int cardWidth: Math.min(Style.space(400), panel.width - Style.gapsOut * 2) property int cardHeight: Math.min(Style.space(500), panel.height - Style.gapsOut * 2) property int cellWidth: Math.max(Style.space(44), Style.font.display + Style.spacing.md) property int cellHeight: Math.max(Style.space(44), Style.font.display + Style.spacing.md) property int columns: Math.floor((cardWidth - contentMargin * 2) / cellWidth) function open(payloadJson) { root.opened = true root.filterText = "" root.selectedIndex = 0 root.cursorActive = 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.emojis") } function toggle() { if (root.opened) root.dismiss() else root.open("{}") } function loadEmojis(raw) { try { var data = JSON.parse(raw) root.emojis = data || [] } catch (e) { console.warn("Failed to parse emojis.json:", e) root.emojis = [] } if (root.opened) root.rebuildDisplay() } function rebuildDisplay() { var query = root.filterText.trim().toLowerCase() var out = [] for (var i = 0; i < root.emojis.length; i++) { var item = root.emojis[i] if (!query || item.k.indexOf(query) >= 0) { out.push(item) if (out.length >= 1000) break // limit to keep it fast } } root.filteredEmojis = out displayModel.clear() for (var j = 0; j < out.length; j++) { displayModel.append({ emoji: out[j].e, index: j }) } 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) resultGrid.positionViewAtIndex(root.selectedIndex, GridView.Contain) }) } function select(delta) { if (displayModel.count === 0) return if (!cursorActive) { cursorActive = true selectedIndex = delta < 0 ? displayModel.count - 1 : 0 } else { selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count } resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain) } function selectRow(delta) { if (displayModel.count === 0) return if (!cursorActive) { cursorActive = true selectedIndex = delta < 0 ? displayModel.count - 1 : 0 resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain) return } var newIndex = selectedIndex + delta * columns if (newIndex < 0) newIndex = 0 if (newIndex >= displayModel.count) newIndex = displayModel.count - 1 selectedIndex = newIndex resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain) } function selectPage(delta) { if (displayModel.count === 0) return if (!cursorActive) { cursorActive = true selectedIndex = delta < 0 ? displayModel.count - 1 : 0 resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain) return } var visibleRows = Math.max(1, Math.floor(resultGrid.height / cellHeight)) var newIndex = selectedIndex + delta * columns * visibleRows if (newIndex < 0) newIndex = 0 if (newIndex >= displayModel.count) newIndex = displayModel.count - 1 selectedIndex = newIndex resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain) } function setFilter(nextFilter) { root.filterText = nextFilter root.selectedIndex = 0 root.cursorActive = false root.rebuildDisplay() } function activateIndex(index) { if (index < 0 || index >= displayModel.count) return var row = displayModel.get(index) root.applySelected(row.emoji) } function applySelected(emoji) { if (!emoji) return root.dismiss() var escEmoji = emoji.replace(/'/g, "'\\''") Quickshell.execDetached(["bash", "-lc", "wl-copy '" + escEmoji + "'; sleep 0.15; wtype '" + escEmoji + "' 2>/dev/null || true"]) } ListModel { id: displayModel } FileView { path: root.omarchyPath + "/shell/plugins/emojis/emojis.json" onLoaded: root.loadEmojis(text()) } PanelWindow { id: panel visible: root.opened anchors { top: true; bottom: true; left: true; right: true } color: "transparent" WlrLayershell.namespace: "omarchy-emojis" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive exclusionMode: ExclusionMode.Ignore Rectangle { anchors.fill: parent color: root.scrim } MouseArea { anchors.fill: parent onClicked: root.dismiss() } Rectangle { id: card width: root.cardWidth height: root.cardHeight radius: root.cornerRadius anchors.centerIn: parent color: root.background border.color: root.border border.width: Math.max(1, Style.space(2)) 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) 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_Left) { root.select(-1) event.accepted = true } else if (event.key === Qt.Key_Right) { root.select(1) event.accepted = true } else if (event.key === Qt.Key_Up) { root.selectRow(-1) event.accepted = true } else if (event.key === Qt.Key_Down) { root.selectRow(1) event.accepted = true } else if (event.key === Qt.Key_PageUp) { root.selectPage(-1) event.accepted = true } else if (event.key === Qt.Key_PageDown) { root.selectPage(1) event.accepted = true } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { 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) { 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.headerHeight radius: root.cornerRadius color: "transparent" border.width: 0 Text { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: root.filterText || "Search emojis…" color: root.foreground opacity: root.filterText ? 1 : 0.58 font.family: root.fontFamily font.pixelSize: Style.font.heading elide: Text.ElideRight } } Item { width: parent.width height: parent.height - root.headerHeight - root.contentSpacing GridView { id: resultGrid anchors.fill: parent model: displayModel clip: true cellWidth: root.cellWidth cellHeight: root.cellHeight boundsBehavior: Flickable.StopAtBounds delegate: Rectangle { required property int index required property string emoji readonly property bool hasCursor: root.cursorActive && index === root.selectedIndex width: root.cellWidth height: root.cellHeight radius: root.cornerRadius color: hasCursor ? root.selectedBackground : "transparent" border.color: hasCursor ? root.selectedBorder : "transparent" border.width: (hasCursor && root.selectedBorder.a > 0) ? Style.hoverBorderWidth : 0 Text { text: parent.emoji font.family: root.fontFamily font.pixelSize: Style.font.display anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onContainsMouseChanged: if (containsMouse) { root.cursorActive = true root.selectedIndex = index } onClicked: { root.cursorActive = true root.selectedIndex = index root.activateIndex(index) } } } } Column { anchors.centerIn: parent spacing: Style.space(8) visible: displayModel.count === 0 Text { text: "󰈉" color: root.selectedText opacity: 0.8 font.family: root.fontFamily font.pixelSize: Style.font.displayLarge horizontalAlignment: Text.AlignHCenter width: parent.width } Text { text: "No matches for “" + root.filterText + "”" color: root.foreground opacity: 0.7 font.family: root.fontFamily font.pixelSize: Style.font.title horizontalAlignment: Text.AlignHCenter width: parent.width } } } } } } }