-- SETTINGS: Customize your engines and display names here local engines = { -- primary = { name = "Google", url = "https://google.ca/search?q=" }, primary = { name = "Google/StartPage", url = "https://www.startpage.com/do/search?query=" }, secondary = { name = "Wikipedia", url = "https://wikipedia.org/wiki/Special:Search?search="}, tertiary = { name = "YouTube", url = "https://youtube.com/results?search_query=" }, chatgpt = { name = "ChatGPT", url = "https://chatgpt.com/?q=" }, maps = { name = "Maps", url = "https://www.google.com/maps/search/" }, spotify = { name = "Spotify", url = "https://open.spotify.com/search/" } } local closeTimeout = 1.0 local gracePeriod = 3.0 -- Global variables searchView = nil searchTimer = nil escWatcher = nil clickOutsideWatcher = nil local timeSinceLeft = 0 local hasEnteredWindow = false local autoKillTime = 0 ------------------------------------------------------------ -- CLOSE SEARCH ------------------------------------------------------------ local function closeSearch() if searchView then searchView:delete() searchView = nil end if searchTimer then searchTimer:stop() searchTimer = nil end if escWatcher then escWatcher:stop() escWatcher = nil end if clickOutsideWatcher then clickOutsideWatcher:stop() clickOutsideWatcher = nil end timeSinceLeft = 0 hasEnteredWindow = false autoKillTime = 0 end ------------------------------------------------------------ -- PERFORM SEARCH ------------------------------------------------------------ local function performSearch(engine) hs.alert.show("Searching " .. engine.name .. "...", 1) hs.eventtap.keyStroke({"cmd"}, "c") hs.timer.doAfter(0.2, function() local query = hs.pasteboard.getContents() if not query or query == "" then return end closeSearch() -- RESET TRACKERS FOR NEW SEARCH timeSinceLeft = 0 hasEnteredWindow = false autoKillTime = 0 local url = engine.url .. hs.http.encodeForQuery(query) local screen = hs.mouse.getCurrentScreen() local screenFrame = screen:frame() local width, height = 500, 600 local x = screenFrame.x + (screenFrame.w - width) / 2 local y = screenFrame.y + (screenFrame.h - height) / 2 searchView = hs.webview.new({ x = x, y = y, w = width, h = height }) searchView:windowStyle({"titled", "utility", "closable", "resizable"}) searchView:level(hs.drawing.windowLevels.floating) searchView:userAgent( "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)" ) searchView:url(url) searchView:show() searchView:bringToFront() -------------------------------------------------------- -- ESC CLOSE -------------------------------------------------------- escWatcher = hs.eventtap.new( {hs.eventtap.event.types.keyDown}, function(event) if event:getKeyCode() == 53 then closeSearch() return true end return false end ):start() -------------------------------------------------------- -- CLICK OUTSIDE CLOSE -------------------------------------------------------- clickOutsideWatcher = hs.eventtap.new( {hs.eventtap.event.types.leftMouseDown}, function() if not searchView then return false end local mousePos = hs.mouse.absolutePosition() local winFrame = searchView:frame() local isOutside = mousePos.x < winFrame.x or mousePos.x > (winFrame.x + winFrame.w) or mousePos.y < winFrame.y or mousePos.y > (winFrame.y + winFrame.h) if isOutside then closeSearch() end return false end ):start() -------------------------------------------------------- -- AUTO CLOSE TIMER -------------------------------------------------------- searchTimer = hs.timer.doEvery(0.1, function() if not searchView then return end local mousePos = hs.mouse.absolutePosition() local winFrame = searchView:frame() local isOutside = mousePos.x < winFrame.x or mousePos.x > (winFrame.x + winFrame.w) or mousePos.y < winFrame.y or mousePos.y > (winFrame.y + winFrame.h) if not isOutside then hasEnteredWindow = true timeSinceLeft = 0 else if hasEnteredWindow then timeSinceLeft = timeSinceLeft + 0.1 if timeSinceLeft >= closeTimeout then closeSearch() end else autoKillTime = autoKillTime + 0.1 if autoKillTime >= gracePeriod then closeSearch() end end end end) end) end ------------------------------------------------------------ -- RIGHT CLICK TRIGGERS ------------------------------------------------------------ if clickWatcher then clickWatcher:stop() end clickWatcher = hs.eventtap.new( {hs.eventtap.event.types.rightMouseDown}, function(event) local flags = event:getFlags() if flags.cmd and flags.shift then performSearch(engines.maps) return true elseif flags.alt and flags.shift then performSearch(engines.spotify) return true elseif flags.cmd then performSearch(engines.primary) return true elseif flags.alt then performSearch(engines.secondary) return true elseif flags.ctrl then performSearch(engines.tertiary) return true elseif flags.shift then performSearch(engines.chatgpt) return true end return false end ):start() ------------------------------------------------------------ -- CMD + ENTER → OPEN CURRENT PAGE ------------------------------------------------------------ hs.hotkey.bind({"cmd"}, "return", function() if searchView then local currentPage = searchView:url() if currentPage then hs.urlevent.openURL(currentPage) else hs.alert.show("No page loaded") end else hs.alert.show("No search window open") end end)