Merge #14433 input methods and remove Rime

The keyboard choice and omarchy-setup-input own Chinese input now. The
new passwordless sudo and input tooltips get Chinese text.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 23:30:41 -04:00
commit fdd457c0fa
86 files changed
+2545 -207

No files matched your search

+10 -1
View File
@@ -601,5 +601,14 @@ var table = {
"Pause": "暂停",
"Play": "播放",
"Next": "下一首",
"Previous": "上一首"
"Previous": "上一首",
"Disable Passwordless Sudo": "关闭免密码 sudo",
"Passwordless Sudo": "免密码 sudo",
"Could not disable passwordless sudo": "无法关闭免密码 sudo",
"Check the sudo configuration and try again.": "请检查 sudo 配置后重试。",
"Click to set up an input language": "点击设置输入语言",
"Click to switch input language": "点击切换输入语言",
"Ctrl + Shift + Space to toggle input": "Ctrl + Shift + Space 切换输入",
"Japanese: type a word, then Space twice for character choices": "日语:输入词语后按两次空格选字",
"Right-click to switch keyboard layout": "右键切换键盘布局"
}
+36
View File
@@ -0,0 +1,36 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property var state: ({})
function cycle() {
if (watcher.running) watcher.write("cycle\n")
}
// One reader and pending selection for every monitor's keyboard widget.
Process {
id: watcher
command: ["/usr/bin/python", Quickshell.env("OMARCHY_PATH") + "/default/input-methods/indicator.py"]
running: true
stdinEnabled: true
stdout: SplitParser {
onRead: data => {
try { root.state = JSON.parse(data) } catch (e) {}
}
}
onExited: {
root.state = ({})
restart.restart()
}
}
Timer {
id: restart
interval: 5000
onTriggered: watcher.running = true
}
}
+1
View File
@@ -3,6 +3,7 @@ singleton Border 1.0 Border.qml
singleton Color 1.0 Color.qml
singleton I18n 1.0 I18n.qml
singleton IpcRegistry 1.0 IpcRegistry.qml
singleton InputMethodState 1.0 InputMethodState.qml
singleton Style 1.0 Style.qml
singleton Util 1.0 Util.qml
ShellIpc 1.0 ShellIpc.qml
+3 -2
View File
@@ -19,8 +19,9 @@ Every subscription on one page, limits first.
per limit window: its meter and the time until it resets (the exact percentage
on hover). A model-scoped allowance on the same clock (Claude's Fable weekly
limit) is a tick on that window's meter rather than a line of its own; the
row's tooltip names it. Sign-in and endpoint trouble shows under the name in the urgent
color. Limits kept from an earlier check after a failed one dim, and their
row's tooltip names it. A lapsed or missing sign-in shows a _Sign-in required_ link that signs
that account in again from the panel; other endpoint trouble shows under the
name in the urgent color. Limits kept from an earlier check after a failed one dim, and their
tooltip says how old they are.
- **Accounts** — an agent with more than one subscription account (see
`omarchy agent account`) lists each: name and plan on one line (the email on hover), and its own limit
@@ -0,0 +1,72 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Ui
BarIndicator {
id: root
property bool granted: false
property bool refreshPending: false
active: granted
activeText: "󰟵"
inactiveText: "󰟵"
activeTooltipText: I18n.tr("Disable Passwordless Sudo")
inactiveTooltipText: I18n.tr("Passwordless Sudo")
useActiveColor: true
activeColor: Color.urgent
function refresh() {
if (!root.bar) return
if (statusProc.running) {
root.refreshPending = true
return
}
root.refreshPending = false
statusProc.running = true
}
onBarChanged: refresh()
Component.onCompleted: refresh()
Connections {
target: root.indicatorHost
ignoreUnknownSignals: true
function onRefreshRequested() { root.refresh() }
}
Timer {
interval: 5000
repeat: true
running: !!root.bar
onTriggered: root.refresh()
}
Process {
id: statusProc
command: ["omarchy-sudo-passwordless", "--active"]
onExited: function(exitCode, exitStatus) {
root.granted = exitCode === 0 && exitStatus === 0
if (root.refreshPending) Qt.callLater(root.refresh)
}
}
Process {
id: disableProc
command: ["omarchy-sudo-passwordless", "--disable"]
onExited: function(exitCode, exitStatus) {
if ((exitCode !== 0 || exitStatus !== 0) && root.bar)
Quickshell.execDetached([Quickshell.env("OMARCHY_PATH") + "/bin/omarchy-notification-send", I18n.tr("Could not disable passwordless sudo"), I18n.tr("Check the sudo configuration and try again.")])
if (root.indicatorHost) root.indicatorHost.refresh()
else root.refresh()
}
}
onPressed: function() {
if (!root.bar || disableProc.running) return
if (root.granted) disableProc.running = true
else root.bar.run("omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless")
}
}
@@ -26,6 +26,11 @@
"placeholderText": "Search indicators...",
"emptyText": "No indicators",
"options": [
{
"value": "PasswordlessSudo",
"label": "Passwordless sudo",
"description": "Warning while passwordless root access is enabled"
},
{
"value": "Dictation",
"label": "Dictation",
+1 -1
View File
@@ -8,7 +8,7 @@ BarWidget {
id: root
moduleName: "omarchy.indicators"
readonly property var defaultIndicatorEntries: [ "Dictation", "ScreenRecording", "Reminder", "NightLight", "Dnd", "StayAwake" ]
readonly property var defaultIndicatorEntries: [ "PasswordlessSudo", "Dictation", "ScreenRecording", "Reminder", "NightLight", "Dnd", "StayAwake" ]
readonly property var indicatorEntries: indicatorEntriesFromSettings(settings)
property var activeIndicatorIds: []
property var indicatorActiveStates: ({})
@@ -4,7 +4,7 @@
"name": "Keyboard layout",
"version": "1.0.0",
"author": "Omarchy",
"description": "Current xkb layout, click cycles",
"description": "Current keyboard layout and input language, click cycles",
"kinds": [
"bar-widget"
],
@@ -13,7 +13,7 @@
},
"barWidget": {
"displayName": "Keyboard layout",
"description": "Current xkb layout, click cycles",
"description": "Current keyboard layout and input language, click cycles",
"category": "Compositor",
"allowMultiple": false
},
+15 -4
View File
@@ -36,6 +36,9 @@ BarWidget {
// xkb's own table rather than maintained by hand.
property var layoutBriefs: ({})
readonly property string layoutLabel: KeyboardLayoutModel.shortLabel(layoutFull, layoutBriefs)
readonly property var inputState: InputMethodState.state
readonly property bool multipleInputs: (inputState.methods || []).length > 1
readonly property string inputLabel: KeyboardLayoutModel.inputLabel(inputState, layoutLabel)
// A query already in flight was started before this event, so it may read the
// layout the switch replaced. Remember the request and re-run once it lands
@@ -228,7 +231,7 @@ BarWidget {
onTriggered: root.refresh()
}
visible: layoutLabel !== "" && multipleLayouts
visible: inputLabel !== "" && KeyboardLayoutModel.showIndicator(layoutLabel, multipleLayouts, multipleInputs)
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
@@ -236,10 +239,18 @@ BarWidget {
id: button
anchors.fill: parent
bar: root.bar
text: root.layoutLabel
text: root.inputLabel
fontSize: Style.font.caption
horizontalMargin: 6
tooltipText: root.layoutFull
onPressed: function() { root.cycleLayout() }
tooltipText: KeyboardLayoutModel.inputTooltip(root.inputState, root.layoutFull, root.multipleLayouts).split("\n").map(line => I18n.tr(line)).join("\n")
onPressed: function(button) {
if (root.multipleInputs && button === Qt.LeftButton) {
InputMethodState.cycle()
} else if (root.multipleLayouts) {
root.cycleLayout()
} else if (!root.multipleInputs && root.bar) {
root.bar.run("omarchy-launch-floating-terminal-with-presentation 'omarchy-setup-input'")
}
}
}
}
@@ -110,6 +110,28 @@ function selectKeyboard(typed, namedByEvent) {
}, keyboards[0])
}
function inputLabel(state, fallback) {
if (!state || !state.current || state.current.indexOf("keyboard-") === 0) return fallback
if (state.label) return String(state.label).substring(0, 3)
var labels = { mozc: "あ", hangul: "한", pinyin: "拼", chewing: "注" }
return labels[state.current] || shortLabel(state.language || state.name, {})
}
function inputTooltip(state, layout, multipleLayouts) {
if (!state || !state.methods || state.methods.length < 2)
return layout + (multipleLayouts ? "" : "\nClick to set up an input language")
var text = state.name + "\nClick to switch input language\nCtrl + Shift + Space to toggle input"
if (state.methods.indexOf("mozc") !== -1)
text += "\nJapanese: type a word, then Space twice for character choices"
if (multipleLayouts) text += "\nRight-click to switch keyboard layout"
return text
}
function showIndicator(layoutLabel, multipleLayouts, multipleInputs) {
return multipleInputs || (layoutLabel !== "" &&
(multipleLayouts || (layoutLabel !== "EN" && layoutLabel !== "ENG")))
}
function layoutIndex(keyboard) {
return (keyboard && keyboard.active_layout_index) || 0
}
@@ -120,6 +142,9 @@ if (typeof module !== "undefined") {
isTypedKeyboard: isTypedKeyboard,
layoutBriefs: layoutBriefs,
selectKeyboard: selectKeyboard,
shortLabel: shortLabel
shortLabel: shortLabel,
inputLabel: inputLabel,
inputTooltip: inputTooltip,
showIndicator: showIndicator
}
}
@@ -185,6 +185,12 @@ function snapshotOf(notification, timestamp) {
var id = n.id || 0
var expireTimeout = Number(n.expireTimeout || 0)
if (!isFinite(expireTimeout) || expireTimeout < 0) expireTimeout = 0
var actions = []
for (var i = 0; n.actions && i < n.actions.length; i++) {
var action = n.actions[i]
if (action && action.identifier && action.identifier !== "default")
actions.push({ identifier: String(action.identifier), text: String(action.text || action.identifier) })
}
return {
id: id,
originalId: id,
@@ -195,6 +201,7 @@ function snapshotOf(notification, timestamp) {
image: n.image || "",
glyph: glyphFromHints(n.hints),
execArgv: execArgvFromHints(n.hints),
actionsJson: JSON.stringify(actions),
urgency: n.urgency,
expireTimeout: expireTimeout,
timestamp: timestamp === undefined ? Date.now() : timestamp
@@ -203,7 +210,7 @@ function snapshotOf(notification, timestamp) {
// Everything the popup card draws, and therefore everything an in-place
// update has to write through to the row and its file.
var POPUP_ROLES = ["app", "appIcon", "summary", "body", "image", "glyph", "execArgv", "urgency", "expireTimeout"]
var POPUP_ROLES = ["app", "appIcon", "summary", "body", "image", "glyph", "execArgv", "actionsJson", "urgency", "expireTimeout"]
function popupRoles() {
return POPUP_ROLES
@@ -263,6 +270,8 @@ function historyEntry(value, normalUrgency) {
image: e.image || "",
glyph: e.glyph || "",
execArgv: e.execArgv || "",
// Libnotify actions belong to a live sender; never replay stale buttons.
actionsJson: "[]",
urgency: typeof e.urgency === "number" ? e.urgency : normalUrgency,
expireTimeout: 0,
timestamp: e.timestamp || 0
+21 -1
View File
@@ -235,7 +235,7 @@ Item {
// about after the popup exists.
readonly property var updateSignals: [
"summaryChanged", "bodyChanged", "appNameChanged", "appIconChanged",
"imageChanged", "urgencyChanged", "expireTimeoutChanged", "hintsChanged"
"imageChanged", "urgencyChanged", "expireTimeoutChanged", "hintsChanged", "actionsChanged"
]
// A client that updates a notification through replaces_id does not produce
@@ -427,10 +427,26 @@ Item {
// libnotify action — they just expect clicking the notification to
// focus their window. Fall back to focusing the sending app by class so
// that click-to-jump actually works.
if (!invoked && entry.actionsJson && entry.actionsJson !== "[]") return
if (!invoked) focusApp(entry)
dismissPopup(index)
}
function invokePopupAction(index, identifier) {
if (index < 0 || index >= popupModel.count) return
var entry = popupModel.get(index)
var ref = entry && !isRestoredRow(entry) ? liveRefs[entry.originalId] : null
if (!ref || !ref.actions) return
for (var i = 0; i < ref.actions.length; i++) {
var action = ref.actions[i]
if (action && action.identifier === identifier) {
action.invoke()
dismissPopup(index)
return
}
}
}
// Try to focus an existing Hyprland window matching the notification's
// sender. The helper handles case-insensitive class matching.
function focusApp(entry) {
@@ -726,6 +742,7 @@ Item {
image: "",
glyph: "󰂚",
execArgv: "",
actionsJson: "[]",
urgency: NotificationUrgency.Low,
expireTimeout: 0,
timestamp: Date.now()
@@ -1044,6 +1061,7 @@ Item {
required property string body
required property string image
required property string glyph
required property string actionsJson
required property int urgency
required property double expireTimeout
required property double timestamp
@@ -1095,9 +1113,11 @@ Item {
cornerRadius: service.cornerRadius
fontFamily: service.shell && service.shell.bar ? service.shell.bar.fontFamily : ""
glyph: cardSlot.glyph
actions: JSON.parse(cardSlot.actionsJson || "[]")
onCloseRequested: service.dismissPopup(cardSlot.index)
onCardClicked: service.invokePopupDefault(cardSlot.index)
onActionClicked: identifier => service.invokePopupAction(cardSlot.index, identifier)
}
}
}
@@ -26,6 +26,7 @@ BorderSurface {
property int urgency: 1
property double timestamp: 0
property int cornerRadius: 0
property var actions: []
// System monospace font injected by the container.
property string fontFamily: ""
@@ -34,6 +35,7 @@ BorderSurface {
signal closeRequested()
signal cardClicked()
signal actionClicked(string identifier)
// Prefer per-notification media/avatar data, then fall back to the app icon.
// The `check` flag avoids Qt's missing-texture placeholder for unknown names.
readonly property string smallIconSource: image.length > 0 ? image : iconSource(appIcon)
@@ -193,6 +195,27 @@ BorderSurface {
}
}
}
Flow {
Layout.fillWidth: true
Layout.leftMargin: Style.space(12)
Layout.rightMargin: Style.space(12)
Layout.bottomMargin: visible ? Style.space(10) : 0
visible: root.actions.length > 0
spacing: Style.space(8)
Repeater {
model: root.actions
delegate: Button {
required property var modelData
text: modelData.text
bordered: true
foreground: Color.notifications.text
fontFamily: root.fontFamily
onClicked: root.actionClicked(modelData.identifier)
}
}
}
}
// Hover-revealed close. Stacked after mainColumn so its MouseArea sits