Just emojis

This commit is contained in:
David Heinemeier Hansson committed 2026-05-20 14:12:06 +02:00
1 parent b7c0e10288
commit ed3320cb8d
12 files changed
+30 -28

No files matched your search

+353
View File
@@ -0,0 +1,353 @@
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
}
}
}
}
}
}
}
+1
View File
@@ -0,0 +1 @@
[{"e":"\ud83d\ude00","k":"grinning face smile grinning happy"},{"e":"\ud83d\ude03","k":"smiley grinning face with big eyes joy haha happy"},{"e":"\ud83d\ude04","k":"grinning face with smiling eyes pleased laugh smile joy happy"},{"e":"\ud83d\ude01","k":"beaming face with smiling eyes grin"},{"e":"\ud83d\ude06","k":"laughing satisfied haha grinning squinting face happy"},{"e":"\ud83d\ude05","k":"hot grinning face with sweat sweat_smile"},{"e":"\ud83e\udd23","k":"rofl laughing rolling on the floor laughing lol"},{"e":"\ud83d\ude02","k":"face with tears of joy joy tears"},{"e":"\ud83d\ude42","k":"slightly_smiling_face slightly smiling face"},{"e":"\ud83d\ude43","k":"upside-down face upside_down_face"},{"e":"\ud83e\udee0","k":"melting_face melting face dread sarcasm"},{"e":"\ud83d\ude09","k":"wink flirt winking face"},{"e":"\ud83d\ude0a","k":"proud smiling face with smiling eyes blush"},{"e":"\ud83d\ude07","k":"innocent smiling face with halo angel"},{"e":"\ud83e\udd70","k":"smiling_face_with_three_hearts smiling face with hearts love"},{"e":"\ud83d\ude0d","k":"smiling face with heart-eyes heart_eyes crush love"},{"e":"\ud83e\udd29","k":"star_struck eyes star-struck"},{"e":"\ud83d\ude18","k":"kissing_heart flirt face blowing a kiss"},{"e":"\ud83d\ude17","k":"kissing face kissing"},{"e":"\u263a\ufe0f","k":"pleased smiling face blush relaxed"},{"e":"\ud83d\ude1a","k":"kissing_closed_eyes kissing face with closed eyes"},{"e":"\ud83d\ude19","k":"kissing face with smiling eyes kissing_smiling_eyes"},{"e":"\ud83e\udd72","k":"smiling_face_with_tear smiling face with tear"},{"e":"\ud83d\ude0b","k":"lick yum tongue face savoring food"},{"e":"\ud83d\ude1b","k":"face with tongue stuck_out_tongue"},{"e":"\ud83d\ude1c","k":"silly stuck_out_tongue_winking_eye winking face with tongue prank"},{"e":"\ud83e\udd2a","k":"goofy wacky zany_face zany face"},{"e":"\ud83d\ude1d","k":"stuck_out_tongue_closed_eyes squinting face with tongue prank"},{"e":"\ud83e\udd11","k":"rich money-mouth face money_mouth_face"},{"e":"\ud83e\udd17","k":"hugs smiling face with open hands"},{"e":"\ud83e\udd2d","k":"quiet whoops face with hand over mouth hand_over_mouth"},{"e":"\ud83e\udee2","k":"face with open eyes and hand over mouth gasp face_with_open_eyes_and_hand_over_mouth shock"},{"e":"\ud83e\udee3","k":"face with peeking eye face_with_peeking_eye"},{"e":"\ud83e\udd2b","k":"shushing face silence shushing_face quiet"},{"e":"\ud83e\udd14","k":"thinking face thinking"},{"e":"\ud83e\udee1","k":"saluting face respect saluting_face"},{"e":"\ud83e\udd10","k":"hush silence zipper-mouth face zipper_mouth_face"},{"e":"\ud83e\udd28","k":"face with raised eyebrow suspicious raised_eyebrow"},{"e":"\ud83d\ude10","k":"neutral face neutral_face meh"},{"e":"\ud83d\ude11","k":"expressionless expressionless face"},{"e":"\ud83d\ude36","k":"silence face without mouth no_mouth mute"},{"e":"\ud83e\udee5","k":"dotted_line_face invisible dotted line face"},{"e":"\ud83d\ude36\u200d\ud83c\udf2b\ufe0f","k":"face in clouds face_in_clouds"},{"e":"\ud83d\ude0f","k":"smug smirk smirking face"},{"e":"\ud83d\ude12","k":"unamused unamused face meh"},{"e":"\ud83d\ude44","k":"roll_eyes face with rolling eyes"},{"e":"\ud83d\ude2c","k":"grimacing grimacing face"},{"e":"\ud83d\ude2e\u200d\ud83d\udca8","k":"face_exhaling face exhaling"},{"e":"\ud83e\udd25","k":"liar lying_face lying face"},{"e":"\ud83e\udee8","k":"shaking face shaking_face shock"},{"e":"\ud83d\ude0c","k":"whew relieved relieved face"},{"e":"\ud83d\ude14","k":"pensive face pensive"},{"e":"\ud83d\ude2a","k":"tired sleepy face sleepy"},{"e":"\ud83e\udd24","k":"drooling_face drooling face"},{"e":"\ud83d\ude34","k":"zzz sleeping sleeping face"},{"e":"\ud83d\ude37","k":"ill mask sick face with medical mask"},{"e":"\ud83e\udd12","k":"face with thermometer face_with_thermometer sick"},{"e":"\ud83e\udd15","k":"face with head-bandage hurt face_with_head_bandage"},{"e":"\ud83e\udd22","k":"barf nauseated_face disgusted sick nauseated face"},{"e":"\ud83e\udd2e","k":"sick vomiting_face barf face vomiting"},{"e":"\ud83e\udd27","k":"sick achoo sneezing face sneezing_face"},{"e":"\ud83e\udd75","k":"heat sweating hot_face hot face"},{"e":"\ud83e\udd76","k":"ice cold_face cold face freezing"},{"e":"\ud83e\udd74","k":"woozy face groggy woozy_face"},{"e":"\ud83d\ude35","k":"dizzy_face face with crossed-out eyes"},{"e":"\ud83d\ude35\u200d\ud83d\udcab","k":"face with spiral eyes face_with_spiral_eyes"},{"e":"\ud83e\udd2f","k":"mind exploding head exploding_head blown"},{"e":"\ud83e\udd20","k":"cowboy_hat_face cowboy hat face"},{"e":"\ud83e\udd73","k":"celebration partying face birthday partying_face"},{"e":"\ud83e\udd78","k":"disguised_face disguised face"},{"e":"\ud83d\ude0e","k":"cool sunglasses smiling face with sunglasses"},{"e":"\ud83e\udd13","k":"nerd_face nerd face geek glasses"},{"e":"\ud83e\uddd0","k":"face with monocle monocle_face"},{"e":"\ud83d\ude15","k":"confused face confused"},{"e":"\ud83e\udee4","k":"face with diagonal mouth Line truncated
+15
View File
@@ -0,0 +1,15 @@
{
"schemaVersion": 1,
"id": "omarchy.emojis",
"name": "Emojis",
"version": "1.0.0",
"author": "Omarchy",
"description": "Search, copy, and type emojis",
"kinds": [
"overlay"
],
"keepLoaded": true,
"entryPoints": {
"overlay": "Emojis.qml"
}
}