KeyboardLayout: add tons of layouts, add Commons/KeyboardLayout.qml for ease of adding new ones

This commit is contained in:
Ly-sec 2025-09-11 01:40:25 +02:00
parent be0b568f1f
commit 7247a26586
2 changed files with 360 additions and 30 deletions

202
Commons/KeyboardLayout.qml Normal file
View file

@ -0,0 +1,202 @@
pragma Singleton
import QtQuick
QtObject {
id: root
// Comprehensive language name to ISO code mapping
property var languageMap: {
"english"// English variants
: "us",
"american": "us",
"united states": "us",
"us english": "us",
"british": "gb",
"uk": "gb",
"united kingdom": "gb",
"english (uk)": "gb",
"canadian": "ca",
"canada": "ca",
"canadian english": "ca",
"australian": "au",
"australia": "au",
"swedish"// Nordic countries
: "se",
"svenska": "se",
"sweden": "se",
"norwegian": "no",
"norsk": "no",
"norway": "no",
"danish": "dk",
"dansk": "dk",
"denmark": "dk",
"finnish": "fi",
"suomi": "fi",
"finland": "fi",
"icelandic": "is",
"íslenska": "is",
"iceland": "is",
"german"// Western/Central European Germanic
: "de",
"deutsch": "de",
"germany": "de",
"austrian": "at",
"austria": "at",
"österreich": "at",
"swiss": "ch",
"switzerland": "ch",
"schweiz": "ch",
"suisse": "ch",
"dutch": "nl",
"nederlands": "nl",
"netherlands": "nl",
"holland": "nl",
"belgian": "be",
"belgium": "be",
"belgië": "be",
"belgique": "be",
"french"// Romance languages (Western/Southern Europe)
: "fr",
"français": "fr",
"france": "fr",
"canadian french": "ca",
"spanish": "es",
"español": "es",
"spain": "es",
"castilian": "es",
"italian": "it",
"italiano": "it",
"italy": "it",
"portuguese": "pt",
"português": "pt",
"portugal": "pt",
"catalan": "ad",
"català": "ad",
"andorra": "ad",
"romanian"// Eastern European Romance
: "ro",
"română": "ro",
"romania": "ro",
"russian"// Slavic languages (Eastern Europe)
: "ru",
"русский": "ru",
"russia": "ru",
"polish": "pl",
"polski": "pl",
"poland": "pl",
"czech": "cz",
"čeština": "cz",
"czech republic": "cz",
"slovak": "sk",
"slovenčina": "sk",
"slovakia": "sk",
"ukrainian": "ua",
"українська": "ua",
"ukraine": "ua",
"bulgarian": "bg",
"български": "bg",
"bulgaria": "bg",
"serbian": "rs",
"srpski": "rs",
"serbia": "rs",
"croatian": "hr",
"hrvatski": "hr",
"croatia": "hr",
"slovenian": "si",
"slovenščina": "si",
"slovenia": "si",
"bosnian": "ba",
"bosanski": "ba",
"bosnia": "ba",
"macedonian": "mk",
"македонски": "mk",
"macedonia": "mk",
"irish"// Celtic languages (Western Europe)
: "ie",
"gaeilge": "ie",
"ireland": "ie",
"welsh": "gb",
"cymraeg": "gb",
"wales": "gb",
"scottish": "gb",
"gàidhlig": "gb",
"scotland": "gb",
"estonian"// Baltic languages (Northern Europe)
: "ee",
"eesti": "ee",
"estonia": "ee",
"latvian": "lv",
"latviešu": "lv",
"latvia": "lv",
"lithuanian": "lt",
"lietuvių": "lt",
"lithuania": "lt",
"hungarian"// Other European languages
: "hu",
"magyar": "hu",
"hungary": "hu",
"greek": "gr",
"ελληνικά": "gr",
"greece": "gr",
"albanian": "al",
"shqip": "al",
"albania": "al",
"maltese": "mt",
"malti": "mt",
"malta": "mt",
"turkish"// West/Southwest Asian languages
: "tr",
"türkçe": "tr",
"turkey": "tr",
"arabic": "ar",
"العربية": "ar",
"arab": "ar",
"hebrew": "il",
"עברית": "il",
"israel": "il",
"brazilian"// South American languages
: "br",
"brazilian portuguese": "br",
"brasil": "br",
"brazil": "br",
"japanese"// East Asian languages
: "jp",
"日本語": "jp",
"japan": "jp",
"korean": "kr",
"한국어": "kr",
"korea": "kr",
"south korea": "kr",
"chinese": "cn",
"中文": "cn",
"china": "cn",
"simplified chinese": "cn",
"traditional chinese": "tw",
"taiwan": "tw",
"繁體中文": "tw",
"thai"// Southeast Asian languages
: "th",
"ไทย": "th",
"thailand": "th",
"vietnamese": "vn",
"tiếng việt": "vn",
"vietnam": "vn",
"hindi"// South Asian languages
: "in",
"हिन्दी": "in",
"india": "in",
"afrikaans"// African languages
: "za",
"south africa": "za",
"south african": "za",
"qwerty"// Layout variants
: "us",
"dvorak": "us",
"colemak": "us",
"workman": "us",
"azerty": "fr",
"norman": "fr",
"qwertz": "de"
}
}

View file

@ -9,7 +9,6 @@ import qs.Services
Singleton {
id: root
property string currentLayout: "Unknown"
property int updateInterval: 1000 // Update every second
@ -34,8 +33,9 @@ Singleton {
try {
const data = JSON.parse(text)
const layoutName = data.names[data.current_idx]
root.currentLayout = mapLayoutNameToCode(layoutName)
root.currentLayout = extractLayoutCode(layoutName)
} catch (e) {
console.log("Niri layout error:", e)
root.currentLayout = "Unknown"
}
}
@ -54,45 +54,157 @@ Singleton {
// 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 = mapLayoutNameToCode(mainKeyboard.active_keymap)
root.currentLayout = extractLayoutCode(mainKeyboard.active_keymap)
} else {
root.currentLayout = "Unknown"
}
} catch (e) {
console.log("Hyprland layout error:", e)
root.currentLayout = "Unknown"
}
}
}
}
// Layout name to ISO code mapping
property var layoutMap: {
"German": "de",
"English (US)": "us",
"English (UK)": "gb",
"French": "fr",
"Spanish": "es",
"Italian": "it",
"Portuguese (Brazil)": "br",
"Portuguese": "pt",
"Russian": "ru",
"Polish": "pl",
"Swedish": "se",
"Norwegian": "no",
"Danish": "dk",
"Finnish": "fi",
"Hungarian": "hu",
"Turkish": "tr",
"Czech": "cz",
"Slovak": "sk",
"Japanese": "jp",
"Korean": "kr",
"Chinese": "cn"
// 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) {
console.log("X11 layout error:", e)
root.currentLayout = "Unknown"
}
}
}
}
// Map layout names to ISO codes
function mapLayoutNameToCode(layoutName) {
return layoutMap[layoutName] || layoutName // fallback to raw name if not found
// 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) {
console.log("Localectl error:", 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) {
console.log("Gsettings current error:", 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) {
console.log("Gsettings sources error:", 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: {
@ -101,12 +213,28 @@ Singleton {
}
function updateLayout() {
// Try compositor-specific methods first
if (CompositorService.isHyprland) {
hyprlandLayoutProcess.running = true
} else if (CompositorService.isNiri) {
niriLayoutProcess.running = true
} else {
currentLayout = "Unknown"
// 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"
}
}
}
}