noctalia-shell/Services/KeyboardLayoutService.qml
LemmyCook 7d2eaa46e6 qmlfmt: increase line-length to 360 to avoid hard-wrap.
+ cleaned up power menu/panel
2025-09-12 21:07:11 -04:00

233 lines
6.5 KiB
QML

pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import qs.Commons
import qs.Services
Singleton {
id: root
property string currentLayout: "Unknown"
property int updateInterval: 1000 // Update every second
// Timer to periodically update the layout
Timer {
id: updateTimer
interval: updateInterval
running: true
repeat: true
onTriggered: {
updateLayout()
}
}
// Process to get current keyboard layout using niri msg (Wayland native)
Process {
id: niriLayoutProcess
running: false
command: ["niri", "msg", "-j", "keyboard-layouts"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text)
const layoutName = data.names[data.current_idx]
root.currentLayout = extractLayoutCode(layoutName)
} catch (e) {
root.currentLayout = "Unknown"
}
}
}
}
// Process to get current keyboard layout using hyprctl (Hyprland)
Process {
id: hyprlandLayoutProcess
running: false
command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text)
// Find the main keyboard and get its active keymap
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
if (mainKeyboard && mainKeyboard.active_keymap) {
root.currentLayout = extractLayoutCode(mainKeyboard.active_keymap)
} else {
root.currentLayout = "Unknown"
}
} catch (e) {
root.currentLayout = "Unknown"
}
}
}
}
// Process for X11 systems using setxkbmap
Process {
id: x11LayoutProcess
running: false
command: ["setxkbmap", "-query"]
stdout: StdioCollector {
onStreamFinished: {
try {
const lines = text.split('\n')
for (const line of lines) {
if (line.startsWith('layout:')) {
const layout = line.split(':')[1].trim()
root.currentLayout = layout
return
}
}
root.currentLayout = "Unknown"
} catch (e) {
root.currentLayout = "Unknown"
}
}
}
}
// Process for general Wayland using localectl (systemd)
Process {
id: localectlProcess
running: false
command: ["localectl", "status"]
stdout: StdioCollector {
onStreamFinished: {
try {
const lines = text.split('\n')
for (const line of lines) {
if (line.includes('X11 Layout:')) {
const layout = line.split(':')[1].trim()
if (layout && layout !== "n/a") {
root.currentLayout = layout
return
}
}
if (line.includes('VC Keymap:')) {
const keymap = line.split(':')[1].trim()
if (keymap && keymap !== "n/a") {
root.currentLayout = extractLayoutCode(keymap)
return
}
}
}
root.currentLayout = "Unknown"
} catch (e) {
root.currentLayout = "Unknown"
}
}
}
}
// Process for generic keyboard layout detection using gsettings (GNOME-based)
Process {
id: gsettingsProcess
running: false
command: ["gsettings", "get", "org.gnome.desktop.input-sources", "current"]
stdout: StdioCollector {
onStreamFinished: {
try {
const currentIndex = parseInt(text.trim())
gsettingsSourcesProcess.running = true
} catch (e) {
fallbackToLocalectl()
}
}
}
}
Process {
id: gsettingsSourcesProcess
running: false
command: ["gsettings", "get", "org.gnome.desktop.input-sources", "sources"]
stdout: StdioCollector {
onStreamFinished: {
try {
// Parse the sources array and extract layout codes
const sourcesText = text.trim()
const matches = sourcesText.match(/\('xkb', '([^']+)'\)/g)
if (matches && matches.length > 0) {
// Get the first layout as default
const layoutMatch = matches[0].match(/\('xkb', '([^']+)'\)/)
if (layoutMatch) {
root.currentLayout = layoutMatch[1].split('+')[0] // Take first part before any variants
}
} else {
fallbackToLocalectl()
}
} catch (e) {
fallbackToLocalectl()
}
}
}
}
function fallbackToLocalectl() {
localectlProcess.running = true
}
// Extract layout code from various format strings using Commons data
function extractLayoutCode(layoutString) {
if (!layoutString)
return "Unknown"
const str = layoutString.toLowerCase()
// If it's already a short code (2-3 chars), return as-is
if (/^[a-z]{2,3}(\+.*)?$/.test(str)) {
return str.split('+')[0]
}
// Extract from parentheses like "English (US)"
const parenMatch = str.match(/\(([a-z]{2,3})\)/)
if (parenMatch) {
return parenMatch[1]
}
// Check for exact matches or partial matches in language map from Commons
const entries = Object.entries(KeyboardLayout.languageMap)
for (var i = 0; i < entries.length; i++) {
const lang = entries[i][0]
const code = entries[i][1]
if (str.includes(lang)) {
return code
}
}
// If nothing matches, try first 2-3 characters if they look like a code
const codeMatch = str.match(/^([a-z]{2,3})/)
return codeMatch ? codeMatch[1] : "unknown"
}
Component.onCompleted: {
Logger.log("KeyboardLayout", "Service started")
updateLayout()
}
function updateLayout() {
// Try compositor-specific methods first
if (CompositorService.isHyprland) {
hyprlandLayoutProcess.running = true
} else if (CompositorService.isNiri) {
niriLayoutProcess.running = true
} else {
// Try detection methods in order of preference
if (Qt.platform.os === "linux") {
// Check if we're in X11 or Wayland
const sessionType = Qt.application.arguments.find(arg => arg.includes("QT_QPA_PLATFORM")) || process.env.XDG_SESSION_TYPE
if (sessionType && sessionType.includes("xcb") || process.env.DISPLAY) {
// X11 system
x11LayoutProcess.running = true
} else {
// Wayland or unknown - try gsettings first, then localectl
gsettingsProcess.running = true
}
} else {
currentLayout = "Unknown"
}
}
}
}