pragma Singleton import QtQuick import Quickshell import Quickshell.Io import qs.Commons import qs.Services.Compositor import qs.Services.UI Singleton { id: root property string currentLayout: I18n.tr("common.unknown") property string fullLayoutName: I18n.tr("common.unknown") property string previousLayout: "" property bool isInitialized: false // Updates current layout from various format strings. Called by compositors function setCurrentLayout(layoutString) { root.fullLayoutName = layoutString || I18n.tr("common.unknown"); root.currentLayout = extractLayoutCode(layoutString); } // Extract layout code from various format strings // Priority: variant > country code > language lookup > fallback function extractLayoutCode(layoutString) { if (!layoutString) return I18n.tr("common.unknown"); const str = layoutString.toLowerCase(); // If it's already a short code (2-3 chars), return uppercase if (/^[a-z]{2,3}(\+.*)?$/.test(str)) { return str.split('+')[0].toUpperCase(); } // Check for layout variants first - these are more meaningful than country codes // when distinguishing between multiple layouts of the same language for (const [pattern, display] of Object.entries(variantMap)) { if (str.includes(pattern)) { return display; } } // Extract short code from parentheses like "English (US)" const shortCodeMatch = str.match(/\(([a-z]{2,3})\)/i); if (shortCodeMatch) { return shortCodeMatch[1].toUpperCase(); } // Check for language/country names in the language map // First, try to match at the start of the string (the primary language) for (const [lang, code] of Object.entries(languageMap)) { if (str.startsWith(lang)) { return code.toUpperCase(); } } // Then try word boundary matching anywhere in the string for (const [lang, code] of Object.entries(languageMap)) { const regex = new RegExp(`\\b${lang}\\b`); if (regex.test(str)) { return code.toUpperCase(); } } // 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].toUpperCase() : I18n.tr("common.unknown"); } // Watch for layout changes and show toast onCurrentLayoutChanged: { // Update previousLayout after checking for changes const layoutChanged = isInitialized && currentLayout !== previousLayout && currentLayout !== I18n.tr("common.unknown") && previousLayout !== "" && previousLayout !== I18n.tr("common.unknown"); if (layoutChanged) { if (Settings.data.notifications.enableKeyboardLayoutToast) { const message = I18n.tr("toast.keyboard-layout.changed", { "layout": fullLayoutName }); ToastService.showNotice(I18n.tr("toast.keyboard-layout.title"), message, "", 2000); } Logger.d("KeyboardLayout", "Layout changed from", previousLayout, "to", currentLayout); } // Update previousLayout for next comparison previousLayout = currentLayout; } Component.onCompleted: { Logger.i("KeyboardLayout", "Service started"); // Mark as initialized after a delay to allow first layout update to complete // This prevents showing a toast on the initial load initializationTimer.start(); } Timer { id: initializationTimer interval: 2000 // Wait 2 seconds for first layout update to complete onTriggered: { isInitialized = true; // Set previousLayout to current value after initialization previousLayout = currentLayout; Logger.d("KeyboardLayout", "Service initialized, current layout:", currentLayout); } } // Layout variants - checked BEFORE country codes // These display the variant name when it's more meaningful than the country property var variantMap: { // Alternative keyboard layouts "colemak": "Colemak", "dvorak": "Dvorak", "workman": "Workman", "programmer dvorak": "Dvk-P", "norman": "Norman", // International variants "intl": "Intl", "international": "Intl", "altgr-intl": "Intl", "with dead keys": "Dead", // Common variants "phonetic": "Phon", "extended": "Ext", "ergonomic": "Ergo", "legacy": "Legacy", // Input methods "pinyin": "Pinyin", "cangjie": "Cangjie", "romaji": "Romaji", "kana": "Kana" } // Language/country name to ISO code mapping property var languageMap: { // English variants "english": "us", "american": "us", "united states": "us", "us english": "us", "british": "gb", "united kingdom": "gb", "english (uk)": "gb", "canadian": "ca", "canada": "ca", "canadian english": "ca", "australian": "au", "australia": "au", // Nordic countries "swedish": "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", // Western/Central European Germanic "german": "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", // Romance languages (Western/Southern Europe) "french": "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", // Eastern European Romance "romanian": "ro", "română": "ro", "romania": "ro", // Slavic languages (Eastern Europe) "russian": "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 "ukraine": "ua", "ukrainian": "ua", "українська": "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", // Celtic languages (Western Europe) "irish": "ie", "gaeilge": "ie", "ireland": "ie", "welsh": "gb", "cymraeg": "gb", "wales": "gb", "scottish": "gb", "gàidhlig": "gb", "scotland": "gb", // Baltic languages (Northern Europe) "estonian": "ee", "eesti": "ee", "estonia": "ee", "latvian": "lv", "latviešu": "lv", "latvia": "lv", "lithuanian": "lt", "lietuvių": "lt", "lithuania": "lt", // Other European languages "hungarian": "hu", "magyar": "hu", "hungary": "hu", "greek": "gr", "ελληνικά": "gr", "greece": "gr", "albanian": "al", "shqip": "al", "albania": "al", "maltese": "mt", "malti": "mt", "malta": "mt", // West/Southwest Asian languages "turkish": "tr", "türkçe": "tr", "turkey": "tr", "arabic": "ar", "العربية": "ar", "arab": "ar", "hebrew": "il", "עברית": "il", "israel": "il", // South American languages "brazilian": "br", "brazilian portuguese": "br", "brasil": "br", "brazil": "br", // East Asian languages "japanese": "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", // Southeast Asian languages "thai": "th", "ไทย": "th", "thailand": "th", "vietnamese": "vn", "tiếng việt": "vn", "vietnam": "vn", // South Asian languages "hindi": "in", "हिन्दी": "in", "india": "in", // African languages "afrikaans": "za", "south africa": "za", "south african": "za" } }