From 005e2a10012f537b22e2d03298f2ef873f3b9f10 Mon Sep 17 00:00:00 2001 From: kang Date: Fri, 9 Jan 2026 21:14:51 +0800 Subject: [PATCH] InputSourceSwitch: add application watcher support for input source switching Some apps like kitty-quick-access cannot be captured by window filters. This adds an alternative watcher mode using hs.application.watcher that triggers on app activation events instead. Config format now supports both string (uses window filter) and table with source and watcher type specification. Fixes #351 --- Source/InputSourceSwitch.spoon/init.lua | 75 ++++++++++++++++++------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/Source/InputSourceSwitch.spoon/init.lua b/Source/InputSourceSwitch.spoon/init.lua index d3814be4..9d44fa4a 100644 --- a/Source/InputSourceSwitch.spoon/init.lua +++ b/Source/InputSourceSwitch.spoon/init.lua @@ -8,7 +8,9 @@ --- --- spoon.InputSourceSwitch:setApplications({ --- ["WeChat"] = "Pinyin - Simplified", ---- ["Mail"] = "ABC" +--- ["Mail"] = "ABC", +--- -- Use application watcher for apps that window filter can't capture +--- ["kitty-quick-access"] = { source = "ABC", watcher = "application" } --- }) --- --- spoon.InputSourceSwitch:start() @@ -21,7 +23,7 @@ obj.__index = obj -- Metadata obj.name = "InputSourceSwitch" -obj.version = "1.0" +obj.version = "1.1" obj.author = "eks5115 " obj.homepage = "https://github.com/Hammerspoon/Spoons" obj.license = "MIT - https://opensource.org/licenses/MIT" @@ -52,24 +54,38 @@ local function isMethod(methodName) return false end -local function setAppInputSource(appName, sourceName, event) +local function switchInputSource(sourceName, appName) + local r = true + + if (isLayout(sourceName)) then + r = hs.keycodes.setLayout(sourceName) + elseif isMethod(sourceName) then + r = hs.keycodes.setMethod(sourceName) + else + hs.alert.show(string.format('sourceName: %s is not layout or method', sourceName)) + end + + if (not r) then + hs.alert.show(string.format('set %s to %s failure', appName, sourceName)) + end +end + +local function setAppInputSourceWithWindowFilter(appName, sourceName, event) event = event or hs.window.filter.windowFocused hs.window.filter.new(appName):subscribe(event, function() - local r = true - - if (isLayout(sourceName)) then - r = hs.keycodes.setLayout(sourceName) - elseif isMethod(sourceName) then - r = hs.keycodes.setMethod(sourceName) - else - hs.alert.show(string.format('sourceName: %s is not layout or method', sourceName)) - end + switchInputSource(sourceName, appName) + end) +end - if (not r) then - hs.alert.show(string.format('set %s to %s failure', appName, sourceName)) +local function setAppInputSourceWithAppWatcher(appName, sourceName) + local watcher = hs.application.watcher.new(function(name, eventType, appObj) + if eventType == hs.application.watcher.activated and name == appName then + switchInputSource(sourceName, appName) end - end) + end) + watcher:start() + return watcher end --- InputSourceSwitch.applicationMap @@ -77,18 +93,25 @@ end --- Mapping the application name to the input source obj.applicationsMap = {} +-- Store application watchers to prevent garbage collection +obj.appWatchers = {} + --- InputSourceSwitch:setApplications() --- Method --- Set that mapping the application name to the input source --- --- Parameters: --- * applications - A table containing that mapping the application name to the input source ---- key is the application name and value is the input source name +--- key is the application name +--- value can be: +--- - a string: the input source name (uses window filter, default) +--- - a table: { source = "input source name", watcher = "window" | "application" } --- example: --- ``` --- { --- ["WeChat"] = "Pinyin - Simplified", ---- ["Mail"] = "ABC" +--- ["Mail"] = "ABC", +--- ["kitty-quick-access"] = { source = "ABC", watcher = "application" } --- } --- ``` function obj:setApplications(applications) @@ -104,8 +127,22 @@ end --- Parameters: --- * None function obj:start() - for k,v in pairs(self.applicationsMap) do - setAppInputSource(k, v) + for appName, config in pairs(self.applicationsMap) do + local sourceName, watcherType + + if type(config) == "string" then + sourceName = config + watcherType = "window" + else + sourceName = config.source + watcherType = config.watcher or "window" + end + + if watcherType == "application" then + self.appWatchers[appName] = setAppInputSourceWithAppWatcher(appName, sourceName) + else + setAppInputSourceWithWindowFilter(appName, sourceName) + end end return self end