commit 8a9f5c37ff8ccd12a0593bf22a206ad5af3d8667 Author: Franco Pellicciotti Date: Thu May 14 18:59:23 2026 -0400 Initial commit of Hammerspoon config diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..73cbd07 Binary files /dev/null and b/.DS_Store differ diff --git a/AppBorders.lua b/AppBorders.lua new file mode 100644 index 0000000..a3faa5a --- /dev/null +++ b/AppBorders.lua @@ -0,0 +1,82 @@ +--- AppBorders.lua + +local logger = hs.logger.new("AppBorders") +logger.i("Init") + +-- 1. Variables must be global (no 'local') so they don't get garbage collected +global_border = nil +isBorderEnabled = false -- This tracks if the mode is ON or OFF + +function initBorder() + local win = hs.window.focusedWindow() + local frame + if win ~= nil then + frame = win:frame() + else + frame = hs.geometry.new(0, 0, 0, 0) + end + + global_border = hs.drawing.rectangle(frame) + global_border:setStrokeColor({ ["red"] = 1, ["blue"] = 0, ["green"] = 0, ["alpha"] = 0.8 }) + global_border:setFill(false) + global_border:setStrokeWidth(8) + + -- Only show if enabled + if isBorderEnabled then + global_border:show() + end +end + +function redrawBorder(window, name, event) + -- If the toggle is OFF, make sure border is hidden and stop + if not isBorderEnabled then + if global_border then global_border:hide() end + return + end + + -- Skip specific apps + if name == 'Kontrollzentrum' then return end + + local win = hs.window.focusedWindow() + if win ~= nil then + -- Create the border object if it doesn't exist yet + if not global_border then initBorder() end + + local newFrame = win:frame() + local currentFrame = global_border:frame() + + if not newFrame:equals(currentFrame) then + global_border:setFrame(newFrame) + end + global_border:show() + else + if global_border then global_border:hide() end + end +end + +-- 2. Setup the window filter (but it won't do anything until isBorderEnabled is true) +allwindows = hs.window.filter.new(nil) +allwindows:subscribe(hs.window.filter.windowCreated, redrawBorder) +allwindows:subscribe(hs.window.filter.windowDestroyed, redrawBorder) +allwindows:subscribe(hs.window.filter.windowFocused, redrawBorder) +allwindows:subscribe(hs.window.filter.windowMoved, redrawBorder) +allwindows:subscribe(hs.window.filter.windowUnfocused, redrawBorder) + +-- 3. THE TOGGLE FUNCTION +function toggleAppBorders() + isBorderEnabled = not isBorderEnabled + + if isBorderEnabled then + hs.alert.show("Window Borders: ON") + if not global_border then initBorder() end + redrawBorder() -- Trigger immediate draw + else + hs.alert.show("Window Borders: OFF") + if global_border then global_border:hide() end + end +end + +-- 4. THE HOTKEY (Change "B" or the modifiers to your liking) +hs.hotkey.bind({"cmd", "alt", "ctrl"}, "B", function() + toggleAppBorders() +end) diff --git a/AppConfig.lua b/AppConfig.lua new file mode 100644 index 0000000..3f461f6 --- /dev/null +++ b/AppConfig.lua @@ -0,0 +1,27 @@ +local AppConfig = {} + +-- Master list of apps that require AppleScript/Force-moves +AppConfig.stubbornApps = { + ["Gemini"] = true, + ["AFFiNE"] = true, + ["Terminal"] = true, + ["System Settings"] = true, + ["Hammerspoon"] = true +} + +-- Master list of system components/utilities to ignore +AppConfig.ignoreList = { + ["TheBoringNotch"] = true, + ["theboringteam.boringnotch"] = true, + ["Control Center"] = true, + ["Notification Center"] = true, + ["Dock"] = true, + ["com.surteesstudios.Bartender"] = true, + ["pro.betterdisplay.BetterDisplay"] = true, + ["stats"] = true, + ["eu.exelban.Stats"] = true, + ["com.ethanbills.DockDoor"] = true, + ["DockDoor"] = true +} + +return AppConfig \ No newline at end of file diff --git a/ArqMonitor.lua b/ArqMonitor.lua new file mode 100644 index 0000000..81037f5 --- /dev/null +++ b/ArqMonitor.lua @@ -0,0 +1,73 @@ +-- ~/.hammerspoon/ArqMonitor.lua +local arqMenu = hs.menubar.new() +local logPath = "/Library/Application Support/ArqAgent/logs/" + +local targets = { + { label = "Arq Storage", search = "Arq Cloud Storage" }, + { label = "Google Drive", search = "Google Drive" } +} + +local function getDestinationStatus() + local menuTable = {} + + for _, target in ipairs(targets) do + -- Find the latest log file that contains the target name + local findLog = "grep -l '" .. target.search .. "' '" .. logPath .. "'*.log 2>/dev/null | xargs ls -t | head -n 1" + local logFile = hs.execute(findLog) + + local status = "⏳ Waiting" + + if logFile and logFile ~= "" then + logFile = logFile:gsub("%s+$", "") + + -- Check for the 'ended' string in that specific file + local checkEnded = "grep 'Backup activity ended' '" .. logFile .. "'" + local finished = hs.execute(checkEnded) + + if finished and finished ~= "" then + status = "✅ Completed" + else + -- If not ended, check if it at least started or is snapshotting + local checkStarted = "grep -E 'Backup activity started|snapshot' '" .. logFile .. "'" + local started = hs.execute(checkStarted) + if started and started ~= "" then + status = "⏳ In Progress" + end + end + end + + table.insert(menuTable, { title = target.label .. ": " .. status, disabled = true }) + end + + table.insert(menuTable, { title = "-" }) + table.insert(menuTable, { title = "Force Refresh", fn = function() updateArqStatus() end }) + + return menuTable +end + +function updateArqStatus() + -- Global status: check the absolute newest log file in the folder + local lastLogCmd = "ls -t '" .. logPath .. "'*.log 2>/dev/null | head -n 1" + local lastLog = hs.execute(lastLogCmd) + + if lastLog and lastLog ~= "" then + lastLog = lastLog:gsub("%s+$", "") + local finished = hs.execute("grep 'Backup activity ended' '" .. lastLog .. "'") + + if finished and finished ~= "" then + arqMenu:setTitle("Arq: ✅") + else + arqMenu:setTitle("Arq: ⏳") + end + else + arqMenu:setTitle("Arq: ❓") + end + + arqMenu:setMenu(getDestinationStatus) +end + +-- Refresh every 5 minutes +local arqTimer = hs.timer.doEvery(300, updateArqStatus) +updateArqStatus() + +return arqMenu \ No newline at end of file diff --git a/Caffeine.lua b/Caffeine.lua new file mode 100644 index 0000000..b873bff --- /dev/null +++ b/Caffeine.lua @@ -0,0 +1,57 @@ +--- Caffeine.lua + +local obj = {} + +-- 1. POPUP ON LOAD +hs.alert.show("Caffeine Loaded", 2) + +-- 2. Create the menubar item +-- We use a unique ID just to keep things stable +obj.menu = hs.menubar.new(true, "CaffeineApp") + +local on_message = 'Caffeine: ON' +local off_message = 'Caffeine: OFF' +local on_icon = "☕️" +local off_icon = "😴" +-- Removed since HyperKey is global +-- local hyper = {"cmd", "alt", "ctrl"} + +function obj:init(mod, key, description) + + local function setCaffeineDisplay(state) + if state then + hs.alert.show(on_message) + if obj.menu then + obj.menu:setTitle(on_icon) + -- This ensures the hover text says "Caffeine: ON" + obj.menu:setTooltip(on_message) + end + else + hs.alert.show(off_message) + if obj.menu then + obj.menu:setTitle(off_icon) + -- This ensures the hover text says "Caffeine: OFF" + obj.menu:setTooltip(off_message) + end + end + end + + local function caffeineClicked() + -- Toggles the system sleep prevention and updates the UI + setCaffeineDisplay(hs.caffeinate.toggle("displayIdle")) + end + + if obj.menu then + obj.menu:setClickCallback(caffeineClicked) + -- Set initial state based on current system status + setCaffeineDisplay(hs.caffeinate.get("displayIdle")) + end + + -- Bind the hotkey (Cmd + Alt + Ctrl + C) + hs.hotkey.bind(mod, key, caffeineClicked) +end + +-- Initialize the object +obj:init(hyper, 'u', "Toggle Caffeine") + +return obj diff --git a/Config.lua b/Config.lua new file mode 100644 index 0000000..e193c83 --- /dev/null +++ b/Config.lua @@ -0,0 +1,10 @@ +-- ~/.hammerspoon/Config.lua +local _M = {} + +_M.homeSSID = "Morpheus5G" +_M.vpnProfileID = "1774034144090" +_M.openvpnPath = "/Applications/OpenVPN Connect.app/Contents/MacOS/OpenVPN Connect" +_M.nasIP = "192.168.1.135" +_M.shares = {"Arq_Backup", "Documents", "Home", "Media"} + +return _M \ No newline at end of file diff --git a/Focus.lua b/Focus.lua new file mode 100644 index 0000000..0938926 --- /dev/null +++ b/Focus.lua @@ -0,0 +1,51 @@ +-- ~/.hammerspoon/Focus.lua +local Focus = {} +local hiddenAppBundleIDs = {} + +function Focus.toggle() + local frontApp = hs.application.frontmostApplication() + + -- UNFOCUS: Bring back only what WE hid + if #hiddenAppBundleIDs > 0 then + local count = 0 + for _, bid in ipairs(hiddenAppBundleIDs) do + local app = hs.application.get(bid) + if app then + app:unhide() + count = count + 1 + end + end + hiddenAppBundleIDs = {} + hs.alert.show("Focus Off: " .. count .. " apps restored") + return + end + + -- FOCUS: Hide background apps instantly + local allApps = hs.application.runningApplications() + local count = 0 + + for _, app in ipairs(allApps) do + local bid = app:bundleID() + local name = app:name() + + -- Hide if: Not current app, has a window, isn't already hidden, and isn't Hammerspoon + if app ~= frontApp and bid and app:mainWindow() and not app:isHidden() then + if bid ~= "org.hammerspoon.Hammerspoon" and name ~= "Hammerspoon" then + table.insert(hiddenAppBundleIDs, bid) + app:hide() + count = count + 1 + end + end + end + + if count > 0 then + hs.alert.show("Focus On: " .. count .. " apps hidden") + else + hs.alert.show("Already Focused") + end +end + +hs.hotkey.bind(hyper, "F", Focus.toggle) + +print("Focus Module: Stable App-Only Version Loaded") +return Focus \ No newline at end of file diff --git a/GOLD SCRIPTS/.DS_Store b/GOLD SCRIPTS/.DS_Store new file mode 100644 index 0000000..2d4fd30 Binary files /dev/null and b/GOLD SCRIPTS/.DS_Store differ diff --git a/GOLD SCRIPTS/AppBorders.lua b/GOLD SCRIPTS/AppBorders.lua new file mode 100644 index 0000000..a3faa5a --- /dev/null +++ b/GOLD SCRIPTS/AppBorders.lua @@ -0,0 +1,82 @@ +--- AppBorders.lua + +local logger = hs.logger.new("AppBorders") +logger.i("Init") + +-- 1. Variables must be global (no 'local') so they don't get garbage collected +global_border = nil +isBorderEnabled = false -- This tracks if the mode is ON or OFF + +function initBorder() + local win = hs.window.focusedWindow() + local frame + if win ~= nil then + frame = win:frame() + else + frame = hs.geometry.new(0, 0, 0, 0) + end + + global_border = hs.drawing.rectangle(frame) + global_border:setStrokeColor({ ["red"] = 1, ["blue"] = 0, ["green"] = 0, ["alpha"] = 0.8 }) + global_border:setFill(false) + global_border:setStrokeWidth(8) + + -- Only show if enabled + if isBorderEnabled then + global_border:show() + end +end + +function redrawBorder(window, name, event) + -- If the toggle is OFF, make sure border is hidden and stop + if not isBorderEnabled then + if global_border then global_border:hide() end + return + end + + -- Skip specific apps + if name == 'Kontrollzentrum' then return end + + local win = hs.window.focusedWindow() + if win ~= nil then + -- Create the border object if it doesn't exist yet + if not global_border then initBorder() end + + local newFrame = win:frame() + local currentFrame = global_border:frame() + + if not newFrame:equals(currentFrame) then + global_border:setFrame(newFrame) + end + global_border:show() + else + if global_border then global_border:hide() end + end +end + +-- 2. Setup the window filter (but it won't do anything until isBorderEnabled is true) +allwindows = hs.window.filter.new(nil) +allwindows:subscribe(hs.window.filter.windowCreated, redrawBorder) +allwindows:subscribe(hs.window.filter.windowDestroyed, redrawBorder) +allwindows:subscribe(hs.window.filter.windowFocused, redrawBorder) +allwindows:subscribe(hs.window.filter.windowMoved, redrawBorder) +allwindows:subscribe(hs.window.filter.windowUnfocused, redrawBorder) + +-- 3. THE TOGGLE FUNCTION +function toggleAppBorders() + isBorderEnabled = not isBorderEnabled + + if isBorderEnabled then + hs.alert.show("Window Borders: ON") + if not global_border then initBorder() end + redrawBorder() -- Trigger immediate draw + else + hs.alert.show("Window Borders: OFF") + if global_border then global_border:hide() end + end +end + +-- 4. THE HOTKEY (Change "B" or the modifiers to your liking) +hs.hotkey.bind({"cmd", "alt", "ctrl"}, "B", function() + toggleAppBorders() +end) diff --git a/GOLD SCRIPTS/Caffeine.lua b/GOLD SCRIPTS/Caffeine.lua new file mode 100644 index 0000000..c3af88c --- /dev/null +++ b/GOLD SCRIPTS/Caffeine.lua @@ -0,0 +1,56 @@ +--- Caffeine.lua + +local obj = {} + +-- 1. POPUP ON LOAD +hs.alert.show("Caffeine Loaded", 2) + +-- 2. Create the menubar item +-- We use a unique ID just to keep things stable +obj.menu = hs.menubar.new(true, "CaffeineApp") + +local on_message = 'Caffeine: ON' +local off_message = 'Caffeine: OFF' +local on_icon = "☕️" +local off_icon = "😴" +local hyper = {"cmd", "alt", "ctrl"} + +function obj:init(mod, key, description) + + local function setCaffeineDisplay(state) + if state then + hs.alert.show(on_message) + if obj.menu then + obj.menu:setTitle(on_icon) + -- This ensures the hover text says "Caffeine: ON" + obj.menu:setTooltip(on_message) + end + else + hs.alert.show(off_message) + if obj.menu then + obj.menu:setTitle(off_icon) + -- This ensures the hover text says "Caffeine: OFF" + obj.menu:setTooltip(off_message) + end + end + end + + local function caffeineClicked() + -- Toggles the system sleep prevention and updates the UI + setCaffeineDisplay(hs.caffeinate.toggle("displayIdle")) + end + + if obj.menu then + obj.menu:setClickCallback(caffeineClicked) + -- Set initial state based on current system status + setCaffeineDisplay(hs.caffeinate.get("displayIdle")) + end + + -- Bind the hotkey (Cmd + Alt + Ctrl + C) + hs.hotkey.bind(mod, key, caffeineClicked) +end + +-- Initialize the object +obj:init(hyper, 'c', "Toggle Caffeine") + +return obj diff --git a/GOLD SCRIPTS/LayoutSelector.lua b/GOLD SCRIPTS/LayoutSelector.lua new file mode 100644 index 0000000..eb80701 --- /dev/null +++ b/GOLD SCRIPTS/LayoutSelector.lua @@ -0,0 +1,219 @@ +local selector = {} +local json = require("hs.json") +local styledtext = require("hs.styledtext") + +-- ========================================== +-- CONFIGURATION +-- ========================================== +selector.storageDir = os.getenv("HOME") .. "/.hammerspoon/layouts/" +selector.hotkeys = {} + +if not hs.fs.attributes(selector.storageDir) then hs.fs.mkdir(selector.storageDir) end + +local stubbornAppsList = { + ["Gemini"] = true, + ["AFFiNE"] = true, + ["Terminal"] = true, + ["System Settings"] = true +} + +local ignoreListItems = { + ["TheBoringNotch"] = true, + ["theboringteam.boringnotch"] = true, + ["Control Center"] = true, + ["Notification Center"] = true, + ["Dock"] = true, + ["com.surteesstudios.Bartender"] = true, + ["pro.betterdisplay.BetterDisplay"] = true +} + +-- ========================================== +-- INTERNAL LOGIC +-- ========================================== + +local function wrapText(text, limit) + local lines = {} + local currentLine = "" + for word in text:gmatch("%S+") do + local cleanWord = word:gsub(",$", "") + if #currentLine + #word >= limit then + table.insert(lines, currentLine .. (currentLine ~= "" and "," or "")) + currentLine = " " .. word + else + currentLine = currentLine == "" and " ↳ " .. word or currentLine .. " " .. word + end + end + table.insert(lines, currentLine) + return lines +end + +local function captureCurrentLayout() + local layout = { saveTime = os.date("%Y-%m-%d %H:%M:%S"), windows = {} } + for _, win in ipairs(hs.window.allWindows()) do + local app = win:application() + local screen = win:screen() + local frame = win:frame() + if app and screen and win:isVisible() and frame.w > 0 and frame.h > 0 then + local appName = app:name() or "" + local bid = app:bundleID() or "" + if not (ignoreListItems[appName] or ignoreListItems[bid] or appName:find("TheBoringNotch")) then + table.insert(layout.windows, { + appName = appName, bundleID = bid, winTitle = win:title(), + screenName = screen:name(), x = math.floor(frame.x), y = math.floor(frame.y), + w = math.floor(frame.w), h = math.floor(frame.h) + }) + end + end + end + return layout +end + +local function executeRestore(filePath, layoutName) + local data = hs.json.read(filePath) + if not data then return end + local windowList = data.windows or data + local launchedAny = false + + for _, winData in ipairs(windowList) do + local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName) + if not app then + if winData.bundleID and winData.bundleID ~= "" then + hs.application.launchOrFocusByBundleID(winData.bundleID) + else + hs.application.launchOrFocus(winData.appName) + end + launchedAny = true + end + end + + local function moveWindows() + for _, winData in ipairs(windowList) do + local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName) + if app then + local pid = app:pid() + local appPath = app:path() or "" + local isElectron = appPath:find("Electron") or appPath:find("Frameworks") + + for _, win in ipairs(app:allWindows()) do + -- Check if window is minimized and restore it if so + if win:isMinimized() then win:unminimize() end + + local isStubborn = stubbornAppsList[app:name()] or isElectron + local x, y, w, h = math.floor(winData.x), math.floor(winData.y), math.floor(winData.w), math.floor(winData.h) + + if isStubborn then + win:setFrame({x=x, y=y, w=w, h=h}, 0) + local script = string.format([[ + tell application "System Events" to tell (first process whose unix id is %d) + set position of window 1 to {%d, %d} + set size of window 1 to {%d, %d} + end tell + ]], pid, x, y, w, h) + hs.applescript.applescript(script) + hs.timer.doAfter(0.5, function() hs.applescript.applescript(script) end) + else + win:setFrame({x=x, y=y, w=w, h=h}, 0) + end + end + end + end + hs.alert.show("Restored: " .. layoutName, 1.5) + end + + if launchedAny then + hs.alert.show("Launching apps...", 3.5) + hs.timer.doAfter(4.5, moveWindows) + else + moveWindows() + end +end + +local function minimizeAll() + local allWindows = hs.window.filter.new():getWindows() + for _, win in ipairs(allWindows) do + local app = win:application() + if app and not ignoreListItems[app:name()] then + win:minimize() + end + end + hs.alert.show("All Windows Minimized", 1) +end + +-- ========================================== +-- MENU BAR & HOTKEYS +-- ========================================== +selector.barItem = hs.menubar.new() + +function selector.refreshMenu() + for _, hk in pairs(selector.hotkeys) do hk:delete() end + selector.hotkeys = {} + + -- Global Hotkeys + selector.hotkeys["save"] = hs.hotkey.bind({"shift", "ctrl", "alt", "cmd"}, "S", function() + local _, name = hs.dialog.textPrompt("New Layout", "Enter name:", "", "Save", "Cancel") + if name and name ~= "" then + hs.json.write(captureCurrentLayout(), selector.storageDir .. name .. ".json", true, true) + selector.refreshMenu() + end + end) + selector.hotkeys["minimize"] = hs.hotkey.bind({"shift", "ctrl", "alt", "cmd"}, "M", minimizeAll) + + local menuTable = { + { title = "📸 Save New Layout...", fn = function() hs.eventtap.keyStroke({"shift", "ctrl", "alt", "cmd"}, "S") end }, + { title = "📉 Minimize All Windows", fn = minimizeAll }, + { title = "-" } + } + + local files = {} + local iter, dir_obj = hs.fs.dir(selector.storageDir) + if iter then for f in iter, dir_obj do if f:find("%.json$") then table.insert(files, f) end end end + table.sort(files) + + local boldStyle = { font = { name = ".AppleSystemUIFontBold", size = 13 } } + + for i, file in ipairs(files) do + local path, label = selector.storageDir .. file, file:gsub("%.json$", "") + local data = hs.json.read(path) + + if i <= 9 then selector.hotkeys[i] = hs.hotkey.bind({"ctrl", "alt", "cmd"}, tostring(i), function() executeRestore(path, label) end) end + + table.insert(menuTable, { + title = hs.styledtext.new("Layout: " .. label .. (i<=9 and " (⌃⌥⌘"..i..")" or ""), boldStyle), + fn = function() executeRestore(path, label) end + }) + + if data and data.saveTime then table.insert(menuTable, { title = " 📅 Saved: " .. data.saveTime, disabled = true }) end + if data and data.windows then + local seen, appNames = {}, {} + for _, w in ipairs(data.windows) do if not seen[w.appName] then table.insert(appNames, w.appName .. ","); seen[w.appName] = true end end + for _, line in ipairs(wrapText(table.concat(appNames, " "), 50)) do table.insert(menuTable, { title = line:gsub(",$", ""), disabled = true }) end + end + + table.insert(menuTable, { + title = hs.styledtext.new(" 🔄 Update " .. label, boldStyle), + fn = function() + hs.json.write(captureCurrentLayout(), path, true, true) + selector.refreshMenu() + hs.alert.show("Updated: " .. label) + end + }) + + table.insert(menuTable, { title = " 🧹 Restore & Cleanup", fn = function() executeRestore(path, label) end }) + table.insert(menuTable, { title = " 📝 Rename", fn = function() + local _, n = hs.dialog.textPrompt("Rename", "New name:", label, "Rename", "Cancel") + if n and n ~= "" then os.rename(path, selector.storageDir .. n .. ".json"); selector.refreshMenu() end + end }) + table.insert(menuTable, { title = " 🗑️ Delete", fn = function() + if hs.dialog.blockAlert("Delete", "Delete "..label.."?", "Delete", "Cancel", "critical") == "Delete" then + os.remove(path); selector.refreshMenu() + end + end }) + table.insert(menuTable, { title = "-" }) + end + selector.barItem:setMenu(menuTable) +end + +selector.barItem:setTitle("🪟") +selector.refreshMenu() +hs.alert.show("Layout Selector Loaded", 1.5) +return selector \ No newline at end of file diff --git a/GOLD SCRIPTS/SearchWindows.lua b/GOLD SCRIPTS/SearchWindows.lua new file mode 100644 index 0000000..657b9e3 --- /dev/null +++ b/GOLD SCRIPTS/SearchWindows.lua @@ -0,0 +1,229 @@ +-- 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) \ No newline at end of file diff --git a/GOLD SCRIPTS/System_Tweaks.lua b/GOLD SCRIPTS/System_Tweaks.lua new file mode 100644 index 0000000..4a415dd --- /dev/null +++ b/GOLD SCRIPTS/System_Tweaks.lua @@ -0,0 +1,16 @@ +-- System_Tweaks.lua +-- Disable Time Machine throttling silently +local function disableTMThrottling() + -- Use hs.execute to run the command directly via sudo + -- Since we added the NOPASSWD rule, this will be silent and instant + local output, status, type, rc = hs.execute("sudo /usr/sbin/sysctl debug.lowpri_throttle_enabled=0", true) + + if status then + hs.notify.new({title="Hammerspoon", informativeText="Time Machine unthrottled (Silent)."}):send() + else + print("Sudo Error: " .. output) + end +end + +-- Run on Hammerspoon load/reload +disableTMThrottling() \ No newline at end of file diff --git a/GOLD SCRIPTS/Version 2 Working Backup/LayoutSelector.lua b/GOLD SCRIPTS/Version 2 Working Backup/LayoutSelector.lua new file mode 100644 index 0000000..fa13125 --- /dev/null +++ b/GOLD SCRIPTS/Version 2 Working Backup/LayoutSelector.lua @@ -0,0 +1,263 @@ +local selector = {} +local json = require("hs.json") +local styledtext = require("hs.styledtext") + +-- ========================================== +-- CONFIGURATION +-- ========================================== +selector.storageDir = os.getenv("HOME") .. "/.hammerspoon/layouts/" +selector.hotkeys = {} + +if not hs.fs.attributes(selector.storageDir) then hs.fs.mkdir(selector.storageDir) end + +local stubbornAppsList = { + ["Gemini"] = true, + ["AFFiNE"] = true, + ["Terminal"] = true, + ["System Settings"] = true +} + +local ignoreListItems = { + ["TheBoringNotch"] = true, + ["theboringteam.boringnotch"] = true, + ["Control Center"] = true, + ["Notification Center"] = true, + ["Dock"] = true, + ["com.surteesstudios.Bartender"] = true, + ["pro.betterdisplay.BetterDisplay"] = true, + ["stats"] = true, + ["eu.exelban.Stats"] = true, + ["com.ethanbills.DockDoor"] = true, + ["DockDoor"] = true +} + +-- ========================================== +-- INTERNAL LOGIC +-- ========================================== + +local function wrapText(text, limit) + local lines = {} + local currentLine = "" + for word in text:gmatch("%S+") do + local cleanWord = word:gsub(",$", "") + if #currentLine + #word >= limit then + table.insert(lines, currentLine .. (currentLine ~= "" and "," or "")) + currentLine = " " .. word + else + currentLine = currentLine == "" and " ↳ " .. word or currentLine .. " " .. word + end + end + table.insert(lines, currentLine) + return lines +end + +local function captureCurrentLayout() + local screens = hs.screen.allScreens() + local layout = { + saveTime = os.date("%Y-%m-%d %H:%M:%S"), + screenCount = #screens, + mode = (#screens > 1) and "Docked" or "Laptop", + windows = {} + } + + for _, win in ipairs(hs.window.allWindows()) do + local app = win:application() + local screen = win:screen() + local frame = win:frame() + if app and screen and win:isVisible() and frame.w > 0 and frame.h > 0 then + local appName = app:name() or "" + local bid = app:bundleID() or "" + if not (ignoreListItems[appName] or ignoreListItems[bid] or appName:find("TheBoringNotch")) then + table.insert(layout.windows, { + appName = appName, + bundleID = bid, + winTitle = win:title(), + winID = win:id(), + screenName = screen:name(), + x = math.floor(frame.x), + y = math.floor(frame.y), + w = math.floor(frame.w), + h = math.floor(frame.h) + }) + end + end + end + return layout +end + +local function executeRestore(filePath, layoutName) + local data = hs.json.read(filePath) + if not data then return end + local windowList = data.windows or data + local launchedAny = false + + for _, winData in ipairs(windowList) do + local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName) + if not app then + if winData.bundleID and winData.bundleID ~= "" then + hs.application.launchOrFocusByBundleID(winData.bundleID) + else + hs.application.launchOrFocus(winData.appName) + end + launchedAny = true + end + end + + local function moveWindows() + for _, winData in ipairs(windowList) do + local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName) + if app then + local pid = app:pid() + local appPath = app:path() or "" + local isElectron = appPath:find("Electron") or appPath:find("Frameworks") + + for _, win in ipairs(app:allWindows()) do + if win:isMinimized() then win:unminimize() end + + local isExactMatch = (win:id() == winData.winID) + local isTitleMatch = (win:title() == winData.winTitle) + local isStubborn = stubbornAppsList[app:name()] or isElectron + + local isFuzzy = (winData.bundleID == "com.apple.Terminal" or winData.appName == "Gemini") + local shouldMove = (isExactMatch or (not winData.winID and isTitleMatch) or (#app:allWindows() == 1 and (isFuzzy or isStubborn))) + + if shouldMove then + local x, y, w, h = math.floor(winData.x), math.floor(winData.y), math.floor(winData.w), math.floor(winData.h) + + if isStubborn then + -- FIX: Target window by title to prevent stacking + local safeTitle = win:title():gsub('"', '\\"') + local script = string.format([[ + tell application "System Events" to tell (first process whose unix id is %d) + try + set targetWin to (first window whose name is "%s") + set position of targetWin to {%d, %d} + set size of targetWin to {%d, %d} + end try + end tell + ]], pid, safeTitle, x, y, w, h) + + hs.applescript.applescript(script) + hs.timer.doAfter(0.5, function() hs.applescript.applescript(script) end) + else + win:setFrame({x=x, y=y, w=w, h=h}, 0) + end + end + end + end + end + hs.alert.show("Restored: " .. layoutName, 1.5) + end + + if launchedAny then + hs.alert.show("Launching apps...", 3.5) + hs.timer.doAfter(4.5, moveWindows) + else + moveWindows() + end +end + +local function minimizeAll() + local allWindows = hs.window.filter.new():getWindows() + for _, win in ipairs(allWindows) do + local app = win:application() + if app and not ignoreListItems[app:name()] then + win:minimize() + end + end + hs.alert.show("All Windows Minimized", 1) +end + +-- ========================================== +-- MENU BAR & HOTKEYS +-- ========================================== +selector.barItem = hs.menubar.new() + +function selector.refreshMenu() + for _, hk in pairs(selector.hotkeys) do hk:delete() end + selector.hotkeys = {} + + local screens = hs.screen.allScreens() + local isDocked = #screens > 1 + + selector.barItem:setTitle(isDocked and "🖥️" or "💻") + + selector.hotkeys["save"] = hs.hotkey.bind({"shift", "ctrl", "alt", "cmd"}, "S", function() + local _, name = hs.dialog.textPrompt("New Layout", "Enter name:", "", "Save", "Cancel") + if name and name ~= "" then + hs.json.write(captureCurrentLayout(), selector.storageDir .. name .. ".json", true, true) + selector.refreshMenu() + end + end) + selector.hotkeys["minimize"] = hs.hotkey.bind({"shift", "ctrl", "alt", "cmd"}, "M", minimizeAll) + + local menuTable = { + { title = "📸 Save New Layout... (⇧⌃⌥⌘S)", fn = function() hs.eventtap.keyStroke({"shift", "ctrl", "alt", "cmd"}, "S") end }, + { title = "📉 Minimize All Windows (⇧⌃⌥⌘M)", fn = minimizeAll }, + { title = "-" } + } + + local files = {} + local iter, dir_obj = hs.fs.dir(selector.storageDir) + if iter then for f in iter, dir_obj do if f:find("%.json$") then table.insert(files, f) end end end + table.sort(files) + + local boldStyle = { font = { name = ".AppleSystemUIFontBold", size = 13 } } + + for i, file in ipairs(files) do + local path, label = selector.storageDir .. file, file:gsub("%.json$", "") + local data = hs.json.read(path) + + if i <= 9 then selector.hotkeys[i] = hs.hotkey.bind({"ctrl", "alt", "cmd"}, tostring(i), function() executeRestore(path, label) end) end + + table.insert(menuTable, { + title = hs.styledtext.new("Layout: " .. label .. (i<=9 and " (⌃⌥⌘"..i..")" or ""), boldStyle), + fn = function() executeRestore(path, label) end + }) + + if data and data.mode then + local contextStr = (data.mode == "Docked") + and string.format(" 🖥️ Docked (%d Screens)", data.screenCount or 0) + or " 💻 Laptop Mode" + table.insert(menuTable, { title = contextStr, disabled = true }) + end + + if data and data.saveTime then table.insert(menuTable, { title = " 📅 Saved: " .. data.saveTime, disabled = true }) end + + if data and data.windows then + local seen, appNames = {}, {} + for _, w in ipairs(data.windows) do if not seen[w.appName] then table.insert(appNames, w.appName .. ","); seen[w.appName] = true end end + for _, line in ipairs(wrapText(table.concat(appNames, " "), 50)) do table.insert(menuTable, { title = line:gsub(",$", ""), disabled = true }) end + end + + table.insert(menuTable, { + title = hs.styledtext.new(" 🔄 Update " .. label, boldStyle), + fn = function() + hs.json.write(captureCurrentLayout(), path, true, true) + selector.refreshMenu() + hs.alert.show("Updated: " .. label) + end + }) + + table.insert(menuTable, { title = " 🧹 Restore & Cleanup", fn = function() executeRestore(path, label) end }) + table.insert(menuTable, { title = " 📝 Rename", fn = function() + local _, n = hs.dialog.textPrompt("Rename", "New name:", label, "Rename", "Cancel") + if n and n ~= "" then os.rename(path, selector.storageDir .. n .. ".json"); selector.refreshMenu() end + end }) + table.insert(menuTable, { title = " 🗑️ Delete", fn = function() + if hs.dialog.blockAlert("Delete", "Delete "..label.."?", "Delete", "Cancel", "critical") == "Delete" then + os.remove(path); selector.refreshMenu() + end + end }) + table.insert(menuTable, { title = "-" }) + end + selector.barItem:setMenu(menuTable) +end + +selector.screenWatcher = hs.screen.watcher.new(function() + selector.refreshMenu() +end):start() + +selector.refreshMenu() +hs.alert.show("Layout Selector Loaded", 1.5) +return selector \ No newline at end of file diff --git a/GOLD SCRIPTS/Version 2 Working Backup/WindowManager.lua b/GOLD SCRIPTS/Version 2 Working Backup/WindowManager.lua new file mode 100644 index 0000000..40d11bf --- /dev/null +++ b/GOLD SCRIPTS/Version 2 Working Backup/WindowManager.lua @@ -0,0 +1,309 @@ +local obj = {} +local json = require("hs.json") +local styledtext = require("hs.styledtext") + +-- ========================================== +-- CONFIGURATION +-- ========================================== +obj.layoutFile = os.getenv("HOME") .. "/.hammerspoon/saved_layout.json" +obj.saveInterval = 300 +obj.isRescued = false +obj.isTransitioning = false +obj.lastScreenCount = #hs.screen.allScreens() + +local stubbornAppsList = { + ["Gemini"] = true, + ["AFFiNE"] = true, + ["Terminal"] = true, + ["System Settings"] = true, + ["Hammerspoon"] = true +} + +local ignoreListItems = { + ["TheBoringNotch"] = true, + ["theboringteam.boringnotch"] = true, + ["Control Center"] = true, + ["Notification Center"] = true, + ["Dock"] = true, + ["com.surteesstudios.Bartender"] = true, + ["pro.betterdisplay.BetterDisplay"] = true, + ["stats"] = true, + ["eu.exelban.Stats"] = true, + ["com.ethanbills.DockDoor"] = true, + ["DockDoor"] = true +} + +-- ========================================== +-- POWER & SLEEP WATCHER +-- ========================================== + +obj.powerWatcher = hs.caffeinate.watcher.new(function(event) + if event == hs.caffeinate.watcher.systemWillSleep or + event == hs.caffeinate.watcher.systemWillPowerOff or + event == hs.caffeinate.watcher.screensDidSleep then + + -- Lock immediately to prevent saving the "scrambled" sleep layout + obj.isTransitioning = true + + elseif event == hs.caffeinate.watcher.systemDidWake or + event == hs.caffeinate.watcher.screensDidWake then + + -- Wait for monitors to handshake, then auto-restore + hs.timer.doAfter(5, function() + obj.isTransitioning = false + obj.restoreLayout() + end) + end +end):start() + +-- ========================================== +-- INTERNAL UTILITIES +-- ========================================== + +local function wrapText(text, limit) + local lines = {} + local currentLine = "" + for word in text:gmatch("%S+") do + if #currentLine + #word >= limit then + table.insert(lines, currentLine) + currentLine = " " .. word + else + currentLine = currentLine == "" and " ↳ " .. word or currentLine .. " " .. word + end + end + table.insert(lines, currentLine) + return lines +end + +-- ========================================== +-- CORE LOGIC +-- ========================================== + +function obj.saveLayout(silent) + if obj.isTransitioning or hs.caffeinate.get("displayIdle") or hs.caffeinate.get("systemIdle") then return end + + local screens = hs.screen.allScreens() + local layout = { + saveTime = os.date("%H:%M:%S"), + screenCount = #screens, + mode = (#screens > 1) and "Docked" or "Laptop", + windows = {} + } + + local allWindows = hs.window.allWindows() + local consoleWin = hs.console.hswindow() + if consoleWin then table.insert(allWindows, consoleWin) end + + for _, win in ipairs(allWindows) do + local app = win:application() + local screen = win:screen() + local frame = win:frame() + + if app and screen and (win:isVisible() or win:title() == "Hammerspoon Console") and frame.w > 0 and frame.h > 0 then + local appName = app:name() or "" + local bid = app:bundleID() or "" + if not (ignoreListItems[appName] or ignoreListItems[bid] or appName:find("TheBoringNotch")) then + table.insert(layout.windows, { + appName = appName, + bundleID = bid, + winTitle = win:title(), + winID = win:id(), + x = math.floor(frame.x), + y = math.floor(frame.y), + w = math.floor(frame.w), + h = math.floor(frame.h) + }) + end + end + end + hs.json.write(layout, obj.layoutFile, true, true) + obj.lastSavedTime = layout.saveTime + if not silent then hs.alert.show("Layout Saved", 1.5) end +end + +function obj.restoreLayout() + local data = hs.json.read(obj.layoutFile) + if not data then return end + obj.isRescued = false + local windowList = data.windows or data + + for _, winData in ipairs(windowList) do + if winData.winTitle == "Hammerspoon Console" then + local cWin = hs.console.hswindow() + if cWin then cWin:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0) end + else + local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName) + if app then + local pid = app:pid() + local appPath = app:path() or "" + local isElectron = appPath:find("Electron") or appPath:find("Frameworks") + + for _, win in ipairs(app:allWindows()) do + if win:isMinimized() then win:unminimize() end + + local isExactMatch = (win:id() == winData.winID) + local isTitleMatch = (win:title() == winData.winTitle) + local isStubborn = stubbornAppsList[app:name()] or isElectron + + local isFuzzy = (winData.bundleID == "com.apple.Terminal" or winData.appName == "Gemini") + local shouldMove = (isExactMatch or (not winData.winID and isTitleMatch) or (#app:allWindows() == 1 and (isFuzzy or isStubborn))) + + if shouldMove then + local x, y, w, h = math.floor(winData.x), math.floor(winData.y), math.floor(winData.w), math.floor(winData.h) + + if isStubborn then + local safeTitle = win:title():gsub('"', '\\"') + local winTarget = (app:name() == "Gemini") and "window 1" or string.format('first window whose name is "%s"', safeTitle) + local script = string.format([[ + tell application "System Events" to tell (first process whose unix id is %d) + try + set targetWin to %s + set position of targetWin to {%d, %d} + set size of targetWin to {%d, %d} + end try + end tell + ]], pid, winTarget, x, y, w, h) + + hs.applescript.applescript(script) + hs.timer.doAfter(0.5, function() hs.applescript.applescript(script) end) + else + win:setFrame({x=x, y=y, w=w, h=h}, 0) + end + end + end + end + end + end + hs.alert.show("Layout Restored", 1.5) +end + +-- ========================================== +-- RESCUE LOGIC +-- ========================================== + +function obj.rescueWindowsToLaptop() + local screens = hs.screen.allScreens() + if #screens == 1 and not obj.isRescued then + local primary = hs.screen.primaryScreen() + local primaryFrame = primary:frame() + for _, win in ipairs(hs.window.allWindows()) do + local app = win:application() + if app then + local name = app:name() or "" + local bid = app:bundleID() or "" + if not (ignoreListItems[name] or ignoreListItems[bid] or name:find("TheBoringNotch")) then + win:unminimize() + local appPath = app:path() or "" + local isElectron = appPath:find("Electron") or appPath:find("Frameworks") + if stubbornAppsList[name] or isElectron then + local pid = app:pid() + local safeTitle = win:title():gsub('"', '\\"') + local winTarget = (name == "Gemini") and "window 1" or string.format('first window whose name is "%s"', safeTitle) + local script = string.format([[ + tell application "System Events" to tell (first process whose unix id is %d) + try + set position of %s to {%d, %d} + end try + end tell + ]], pid, winTarget, math.floor(primaryFrame.x + 20), math.floor(primaryFrame.y + 40)) + hs.applescript.applescript(script) + else + win:moveToScreen(primary, false, true) + end + end + end + end + obj.isRescued = true + hs.alert.show("Forced to Laptop Screen", 1.5) + end +end + +-- ========================================== +-- MENUBAR UI +-- ========================================== +local saveCountdown = obj.saveInterval +local timerMenu = hs.menubar.new() + +function updateMenu() + if timerMenu then + local minutes = math.floor(saveCountdown / 60) + local seconds = saveCountdown % 60 + timerMenu:setTitle(string.format("💠 %d:%02d", minutes, seconds)) + + local screens = hs.screen.allScreens() + local currentCount = #screens + local modeIcon = (currentCount > 1) and "🖥️" or "💻" + local modeName = (currentCount > 1) and "Docked" or "Laptop" + + local data = hs.json.read(obj.layoutFile) + local lastTime = data and data.saveTime or "Never" + local boldStyle = { font = { name = ".AppleSystemUIFontBold", size = 13 } } + + local menuTable = { + { title = "📅 Last Saved: " .. lastTime, disabled = true }, + { title = "-" }, + { title = modeIcon .. " Status: " .. modeName .. " Mode", disabled = true }, + { title = " ↳ 📺 Screens Detected: " .. currentCount, disabled = true }, + { title = "-" }, + { title = hs.styledtext.new("📸 Save Layout (⇧⌘W)", boldStyle), fn = function() obj.saveLayout(false); obj.resetCountdown() end }, + { title = hs.styledtext.new("🔄 Restore Layout (⇧⌘R)", boldStyle), fn = function() obj.restoreLayout() end }, + { title = hs.styledtext.new("🚀 Force Rescue: All Windows to Laptop (⇧⌘⌃L)", boldStyle), fn = function() obj.isRescued = false; obj.rescueWindowsToLaptop() end }, + { title = "-" }, + { title = "📦 Windows in Saved File:", disabled = true } + } + + if data and data.windows then + local seen, appNames = {}, {} + for _, w in ipairs(data.windows) do + if not seen[w.appName] then + table.insert(appNames, w.appName .. ", ") + seen[w.appName] = true + end + end + local appString = table.concat(appNames):gsub(", $", "") + for _, line in ipairs(wrapText(appString, 45)) do + table.insert(menuTable, { title = line, disabled = true }) + end + end + + timerMenu:setMenu(menuTable) + end +end + +function obj.resetCountdown() + saveCountdown = obj.saveInterval + updateMenu() +end + +-- ========================================== +-- WATCHERS & HOTKEYS +-- ========================================== + +hs.hotkey.bind({"shift", "cmd"}, "W", function() obj.saveLayout(false); obj.resetCountdown() end) +hs.hotkey.bind({"shift", "cmd"}, "R", function() obj.restoreLayout() end) +hs.hotkey.bind({"shift", "cmd", "ctrl"}, "L", function() obj.isRescued = false; obj.rescueWindowsToLaptop() end) + +obj.screenWatcher = hs.screen.watcher.new(function() + local currentCount = #hs.screen.allScreens() + if currentCount ~= obj.lastScreenCount then + obj.isTransitioning = true + if currentCount > 1 then + obj.isRescued = false + hs.timer.doAfter(4, function() obj.restoreLayout(); obj.isTransitioning = false end) + else + hs.timer.doAfter(2, function() obj.rescueWindowsToLaptop(); obj.isTransitioning = false end) + end + obj.lastScreenCount = currentCount + updateMenu() + end +end):start() + +obj.autoSaveTimer = hs.timer.doEvery(obj.saveInterval, function() obj.saveLayout(true); obj.resetCountdown() end) +obj.clockTimer = hs.timer.doEvery(1, function() + saveCountdown = saveCountdown - 1 + if saveCountdown < 0 then obj.resetCountdown() end + updateMenu() +end) + +updateMenu() +return obj \ No newline at end of file diff --git a/GOLD SCRIPTS/WindowManager.lua b/GOLD SCRIPTS/WindowManager.lua new file mode 100644 index 0000000..06ef395 --- /dev/null +++ b/GOLD SCRIPTS/WindowManager.lua @@ -0,0 +1,157 @@ +local obj = {} +local json = require("hs.json") + +-- ========================================== +-- CONFIGURATION +-- ========================================== +obj.layoutFile = os.getenv("HOME") .. "/.hammerspoon/saved_layout.json" +obj.saveInterval = 300 + +-- Apps that require AppleScript authority +local stubbornApps = { + ["Gemini"] = true, + ["AFFiNE"] = true, + ["Hammerspoon"] = true, + ["System Settings"] = true +} + +-- Apps or Bundle IDs that the window manager should NEVER touch +local ignoreList = { + ["TheBoringNotch"] = true, + ["theboringteam.boringnotch"] = true, + ["boring.notch"] = true, + ["The Boring Notch"] = true, + ["Control Center"] = true, + ["Notification Center"] = true, + ["Dock"] = true, + ["com.surteesstudios.Bartender"] = true, + ["pro.betterdisplay.BetterDisplay"] = true +} + +-- ========================================== +-- LOGIC +-- ========================================== + +-- Check if window actually needs to be moved +local function needsMoving(win, savedFrame) + local curr = win:frame() + local threshold = 10 + return math.abs(curr.x - savedFrame.x) > threshold or + math.abs(curr.y - savedFrame.y) > threshold +end + +function obj.saveLayout(silent) + -- Safety: Don't save if the system is currently sleeping or screens are locked + if hs.caffeinate.get("displayIdle") or hs.caffeinate.get("systemIdle") then return end + + local allWindows = hs.window.allWindows() + local consoleWin = hs.console.hswindow() + if consoleWin then table.insert(allWindows, consoleWin) end + + local currentLayout = { + saveTime = os.date("%Y-%m-%d %H:%M:%S"), + windows = {} + } + + for _, win in ipairs(allWindows) do + local app = win:application() + local screen = win:screen() + local frame = win:frame() + + -- GHOST WINDOW FIX: Only save windows that have actual size + if app and screen and win:isVisible() and frame.w > 0 and frame.h > 0 then + local name = app:name() or "" + local bid = app:bundleID() or "" + + -- IGNORE LOGIC: Match by name, Bundle ID, or fuzzy notch match + local isIgnored = ignoreList[name] or ignoreList[bid] or name:find("TheBoringNotch") + + if not isIgnored then + table.insert(currentLayout.windows, { + appName = name, + bundleID = bid, + winTitle = win:title(), + screenName = screen:name(), + x = frame.x, y = frame.y, w = frame.w, h = frame.h + }) + end + end + end + + hs.json.write(currentLayout, obj.layoutFile, true, true) + if not silent then hs.alert.show("Layout Saved: " .. currentLayout.saveTime, 1.5) end +end + +function obj.restoreLayout() + local data = hs.json.read(obj.layoutFile) + if not data then return end + local windowList = data.windows or data + + local appleScriptParts = {"tell application \"System Events\""} + local needsAppleScript = false + + for _, winData in ipairs(windowList) do + -- Handle Hammerspoon Console specifically + if winData.appName == "Hammerspoon" and winData.winTitle == "Hammerspoon Console" then + local cWin = hs.console.hswindow() + if cWin and needsMoving(cWin, winData) then + cWin:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0) + end + else + local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName) + if app then + for _, win in ipairs(app:allWindows()) do + local targetScreen = hs.screen.find(winData.screenName) or hs.screen.primaryScreen() + + local isFuzzy = (winData.bundleID == "com.apple.Terminal" or winData.appName == "Gemini") + local isStubborn = stubbornApps[winData.appName] + local shouldMove = (win:title() == winData.winTitle or #app:allWindows() == 1 or isFuzzy or isStubborn) + + if targetScreen and shouldMove and needsMoving(win, winData) then + if isStubborn then + needsAppleScript = true + table.insert(appleScriptParts, string.format([[ + try + tell process "%s" to set position of window 1 to {%d, %d} + tell process "%s" to set size of window 1 to {%d, %d} + end try + ]], winData.appName, winData.x, winData.y, winData.appName, winData.w, winData.h)) + else + win:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0) + end + end + end + end + end + end + + if needsAppleScript then + table.insert(appleScriptParts, "end tell") + hs.applescript.applescript(table.concat(appleScriptParts, "\n")) + end +end + +-- Hotkeys +hs.hotkey.bind({"shift", "cmd"}, "W", function() obj.saveLayout(false) end) +hs.hotkey.bind({"shift", "cmd"}, "R", function() obj.restoreLayout() end) + +-- Automation: Screen Watcher +obj.screenWatcher = hs.screen.watcher.new(function() + hs.timer.doAfter(2, function() obj.restoreLayout() end) +end):start() + +-- Automation: Sleep Watcher +obj.sleepWatcher = hs.caffeinate.watcher.new(function(event) + if event == hs.caffeinate.watcher.systemWillSleep or event == hs.caffeinate.watcher.screensDidLock then + if obj.autoSaveTimer then obj.autoSaveTimer:stop() end + elseif event == hs.caffeinate.watcher.systemDidWake or event == hs.caffeinate.watcher.screensDidUnlock then + if obj.autoSaveTimer then obj.autoSaveTimer:start() end + hs.timer.doAfter(3, function() obj.restoreLayout() end) + end +end):start() + +obj.autoSaveTimer = hs.timer.doEvery(obj.saveInterval, function() obj.saveLayout(true) end) + +hs.alert.show("Window Manager Loaded", 1.5) + +return obj \ No newline at end of file diff --git a/GOLD SCRIPTS/init.lua b/GOLD SCRIPTS/init.lua new file mode 100644 index 0000000..6827759 --- /dev/null +++ b/GOLD SCRIPTS/init.lua @@ -0,0 +1,78 @@ +-- ~/.hammerspoon/init.lua +require('SearchWindows') +require('Caffeine') +require('AppBorders') +-- require('System_Tweaks') + +-- Load the window management module +windowMgr = require("WindowManager") + + +-- Load Spoon Files +hs.loadSpoon('SpoonInstall') +hs.loadSpoon('SpeedMenu') +hs.loadSpoon('BrewInfo') +-- hs.loadSpoon('Seal') + +--- 3. Run/Configure Spoons + +---- SpeedMenu Config +if spoon.SpeedMenu then + -- 1. Define the Fix Function (includes MAC and IPv6) + local function applyFullMenuFix() + local interface = spoon.SpeedMenu.interface or "en0" + local ssid = hs.wifi.currentNetwork() or "Disconnected" + local details = hs.network.interfaceDetails(interface) + + local ipv4 = (details and details.IPv4) and details.IPv4.Addresses[1] or "N/A" + local ipv6 = (details and details.IPv6) and details.IPv6.Addresses[1] or "N/A" + + -- Get MAC Address via shell + local macaddr = hs.execute('ifconfig ' .. interface .. ' | grep ether | awk \'{print $2}\''):gsub("%s+", "") + + local menuitems = { + { title = "SSID: " .. ssid, fn = function() hs.pasteboard.setContents(ssid) end }, + { title = "IPv4: " .. ipv4, fn = function() hs.pasteboard.setContents(ipv4) end }, + { title = "IPv6: " .. ipv6, fn = function() hs.pasteboard.setContents(ipv6) end }, + { title = "MAC: " .. macaddr, fn = function() hs.pasteboard.setContents(macaddr) end }, + { title = "-" }, + { title = "Rescan Network Interfaces", fn = function() spoon.SpeedMenu:rescan() end } + } + spoon.SpeedMenu.menubar:setMenu(menuitems) + end + + -- 2. Hook the rescan method + local oldRescan = spoon.SpeedMenu.rescan + spoon.SpeedMenu.rescan = function(self) + oldRescan(self) + applyFullMenuFix() + end + + -- 3. Toggle Logic (Starts as OFF) + local speedMenuRunning = false + hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function() + if speedMenuRunning then + spoon.SpeedMenu:stop() + hs.alert.show("SpeedMenu Stopped") + else + spoon.SpeedMenu:start() -- This puts it in the menu bar + applyFullMenuFix() -- This populates the data + hs.alert.show("SpeedMenu Started") + end + speedMenuRunning = not speedMenuRunning + end) +end +---- SpeedMenu Config END + +---- BrewInfo +if spoon.BrewInfo then + hs.hotkey.bind({"cmd", "alt", "ctrl"}, "I", function() + spoon.BrewInfo:showBrewInfo() + end) + hs.hotkey.bind({"cmd", "alt", "ctrl"}, "J", function() + spoon.BrewInfo:showBrewInfoCurSel() + end) +end +---- BrewInfo END + +hs.alert.show("Hammerspoon Config Reloaded") \ No newline at end of file diff --git a/GeminiMonitor.lua b/GeminiMonitor.lua new file mode 100644 index 0000000..570200e --- /dev/null +++ b/GeminiMonitor.lua @@ -0,0 +1,109 @@ +-- 1. Identify it uniquely (Fresh ID forces macOS to update the hover title) +GeminiMonitorItem = hs.menubar.new(true, "GeminiMonitorV3") + +-- 2. Set the hover text (Tooltip) +if GeminiMonitorItem then + GeminiMonitorItem:setTooltip("Gemini Usage Monitor") +end + +local JSON_KEY_PATH = os.getenv("HOME") .. "/.hammerspoon/gcp-key.json" +local PROJECT_ID = "gen-lang-client-0842463959" +local UPDATE_INTERVAL = 300 + +-- 3. Setup the Icon +-- "NSActionTemplate" is the standard 'Gear' icon +local geminiStar = hs.image.imageFromName("NSActionTemplate") + +if geminiStar then + -- 'template' allows the icon to switch between black and white for Dark Mode + geminiStar:template(true) + GeminiMonitorItem:setIcon(geminiStar) +end + +-- 2026 Flash Preview Pricing +local PRICING_ESTIMATES = { + ["Gemini 3 Flash Preview"] = 0.0010, + ["Gemini 3.1 Pro"] = 0.0450, + ["Gemini 2.0 Ultra"] = 0.1500 +} +local selectedModel = "Gemini 3 Flash Preview" + +local stats = { + requests = 0, + lastUpdate = "Never" +} + +local authToken = nil + +-- POPUP ON LOAD +hs.alert.show("GeminiMonitor Loaded", 2) + +local function getAccessToken(callback) + local keyFile = io.open(JSON_KEY_PATH, "r") + if not keyFile then return end + local keyData = hs.json.decode(keyFile:read("*all")) + keyFile:close() + local now = os.time() + local header = hs.base64.encode(hs.json.encode({alg="RS256", typ="JWT"})) + local claim = hs.base64.encode(hs.json.encode({iss=keyData.client_email, scope="https://www.googleapis.com/auth/monitoring.read", aud="https://oauth2.googleapis.com/token", exp=now+3600, iat=now})) + local sign_cmd = string.format("echo -n '%s' | openssl dgst -sha256 -sign <(echo '%s') -binary | openssl base64", header.."."..claim, keyData.private_key) + hs.task.new("/bin/bash", function(c, out, e) + local sig = out:gsub("%s+", ""):gsub("/", "_"):gsub("+", "-"):gsub("=", "") + hs.http.asyncPost("https://oauth2.googleapis.com/token", "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion="..header.."."..claim.."."..sig, {["Content-Type"]="application/x-www-form-urlencoded"}, function(s, b) + local res = hs.json.decode(b) + if res and res.access_token then callback(res.access_token) end + end) + end, {"-c", sign_cmd}):start() +end + +local function updateUsage() + if not authToken then + getAccessToken(function(token) authToken = token; updateUsage() end) + return + end + + local endTime = os.date("!%Y-%m-%dT%H:%M:%SZ") + local startTime = os.date("!%Y-%m-%dT%H:%M:%SZ", os.time() - 86400) + + local filter = 'resource.type="consumed_api" AND metric.type="serviceruntime.googleapis.com/api/request_count" AND resource.labels.service="generativelanguage.googleapis.com"' + local url = string.format("https://monitoring.googleapis.com/v3/projects/%s/timeSeries?filter=%s&interval.startTime=%s&interval.endTime=%s", + PROJECT_ID, hs.http.encodeForQuery(filter), startTime, endTime) + + hs.http.asyncGet(url, {Authorization = "Bearer "..authToken}, function(status, body) + if status ~= 200 then return end + local data = hs.json.decode(body) + local r = 0 + if data.timeSeries then + for _, s in ipairs(data.timeSeries) do + for _, p in ipairs(s.points) do r = r + (tonumber(p.value.int64Value) or 0) end + end + end + + stats.requests = r + stats.lastUpdate = os.date("%H:%M") + + local estCost = r * PRICING_ESTIMATES[selectedModel] + GeminiMonitorItem:setTitle(string.format(" %d ($%0.3f)", r, estCost)) + end) +end + +GeminiMonitorItem:setMenu(function() + return { + { title = "GEMINI FLASH MONITOR", disabled = true }, + { title = "Project: " .. PROJECT_ID, disabled = true }, + { title = "-" }, + { title = "24H Requests: " .. stats.requests }, + { title = string.format("Est. Cost: $%0.4f", stats.requests * PRICING_ESTIMATES[selectedModel]) }, + { title = "-" }, + { title = "ACTIVE MODEL:", disabled = true }, + { title = "Gemini 3 Flash Preview", checked = (selectedModel == "Gemini 3 Flash Preview"), fn = function() selectedModel = "Gemini 3 Flash Preview"; updateUsage() end }, + { title = "Gemini 3.1 Pro", checked = (selectedModel == "Gemini 3.1 Pro"), fn = function() selectedModel = "Gemini 3.1 Pro"; updateUsage() end }, + { title = "-" }, + { title = "Refresh Now", fn = updateUsage }, + { title = "Last Sync: " .. stats.lastUpdate, disabled = true } + } +end) + +updateUsage() +if GeminiTimer then GeminiTimer:stop() end +GeminiTimer = hs.timer.doEvery(UPDATE_INTERVAL, updateUsage) diff --git a/HotKeyMapper.lua b/HotKeyMapper.lua new file mode 100644 index 0000000..57420eb --- /dev/null +++ b/HotKeyMapper.lua @@ -0,0 +1,32 @@ +--- HotkeyMapper.lua +local obj = {} +obj.registry = {} + +obj.chooser = hs.chooser.new(function(choice) + if not choice then return end +end) + +-- This is the function you'll use to log keys +function obj:register(mod, key, description, source) + table.insert(self.registry, { + text = string.format("%s + %s: %s", hs.inspect(mod):gsub('"', ''), key:upper(), description), + subText = "Source: " .. (source or "Unknown") + }) +end + +function obj:show() + if #self.registry == 0 then + hs.alert.show("No hotkeys registered in Mapper") + return + end + table.sort(self.registry, function(a, b) return a.text < b.text end) + obj.chooser:choices(self.registry) + obj.chooser:show() +end + +function obj:init(mod, key) + hs.hotkey.bind(mod, key, function() self:show() end) + return self +end + +return obj \ No newline at end of file diff --git a/HyperKey.lua b/HyperKey.lua new file mode 100644 index 0000000..fd8e3b9 --- /dev/null +++ b/HyperKey.lua @@ -0,0 +1,16 @@ +-- ~/.hammerspoon/HyperKey.lua + +-- Define the Hyper Key global (Cmd + Alt + Ctrl + Shift) +-- Map Caps Lock to this combo using Karabiner-Elements + +-- hyper = {"ralt"} +hyper = {"cmd", "alt", "ctrl", "shift"} + +-- Global Shortcuts +hs.hotkey.bind(hyper, "R", function() + hs.reload() +end) + +print("HyperKey Module: Set to CAPS Lock") + +return hyper \ No newline at end of file diff --git a/Last known/WindowManager.lua b/Last known/WindowManager.lua new file mode 100644 index 0000000..73a131a --- /dev/null +++ b/Last known/WindowManager.lua @@ -0,0 +1,218 @@ +local obj = {} +local json = require("hs.json") +local styledtext = require("hs.styledtext") + +-- ========================================== +-- CONFIGURATION +-- ========================================== +obj.layoutFile = os.getenv("HOME") .. "/.hammerspoon/saved_layout.json" +obj.saveInterval = 300 +obj.isRescued = false +obj.isTransitioning = false +obj.isRestoring = false +obj.wakeTimer = nil +obj.lastScreenCount = #hs.screen.allScreens() +obj.lastSavedTime = "Never" + +local ignoreListItems = { + ["TheBoringNotch"] = true, ["Control Center"] = true, ["Notification Center"] = true, + ["Dock"] = true, ["BetterDisplay"] = true, ["Stats"] = true, ["DockDoor"] = true +} + +-- ========================================== +-- INTERNAL UTILITIES +-- ========================================== + +local function wrapText(text, limit) + local lines = {} + local currentLine = "" + for word in text:gmatch("%S+") do + if #currentLine + #word >= limit then + table.insert(lines, currentLine) + currentLine = " " .. word + else + currentLine = currentLine == "" and " ↳ " .. word or currentLine .. " " .. word + end + end + table.insert(lines, currentLine) + return lines +end + +-- ========================================== +-- CORE LOGIC (PURE NATIVE) +-- ========================================== + +function obj.saveLayout(silent) + if obj.isTransitioning or obj.isRestoring or #hs.screen.allScreens() ~= obj.lastScreenCount then + return + end + + local layout = { + saveTime = os.date("%H:%M:%S"), + screenCount = #hs.screen.allScreens(), + windows = {} + } + + for _, win in ipairs(hs.window.allWindows()) do + local app = win:application() + if app and win:isVisible() and win:frame().w > 0 then + local appName = app:name() or "" + if not ignoreListItems[appName] then + table.insert(layout.windows, { + appName = appName, + bundleID = app:bundleID(), + winTitle = win:title(), + x = math.floor(win:frame().x), + y = math.floor(win:frame().y), + w = math.floor(win:frame().w), + h = math.floor(win:frame().h) + }) + end + end + end + hs.json.write(layout, obj.layoutFile, true, true) + obj.lastSavedTime = layout.saveTime + if not silent then hs.alert.show("Layout Saved", 1.5) end +end + +function obj.restoreLayout() + if obj.isRestoring then return end + + local data = hs.json.read(obj.layoutFile) + if not data or not data.windows then return end + + obj.isRestoring = true + obj.isRescued = false + + -- 1. Group saved windows by App Name + local savedByApp = {} + for _, winData in ipairs(data.windows) do + savedByApp[winData.appName] = savedByApp[winData.appName] or {} + table.insert(savedByApp[winData.appName], winData) + end + + -- 2. Sort saved windows by position + for appName, entries in pairs(savedByApp) do + table.sort(entries, function(a, b) + if a.y == b.y then return a.x < b.x end + return a.y < b.y + end) + end + + print("WindowManager: Restore Starting (Native Engine)") + + -- 3. Move windows using Pure Hammerspoon logic + for appName, savedEntries in pairs(savedByApp) do + local firstEntry = savedEntries[1] + local app = hs.application.get(firstEntry.bundleID) or hs.application.get(appName) + + if app then + local physicalWins = {} + for _, w in ipairs(app:allWindows()) do + if w:isVisible() and w:frame().w > 0 then table.insert(physicalWins, w) end + end + + -- Sort physical windows to match + table.sort(physicalWins, function(a, b) + local af, bf = a:frame(), b:frame() + if af.y == bf.y then return af.x < bf.x end + return af.y < bf.y + end) + + for i, winData in ipairs(savedEntries) do + local win = physicalWins[i] + if win then + if win:isMinimized() then win:unminimize() end + -- Move window to the exact saved frame + win:setFrame({ + x = winData.x, + y = winData.y, + w = winData.w, + h = winData.h + }, 0) -- 0 duration for instant move + end + end + end + end + + hs.alert.show("Layout Restored", 1.5) + + hs.timer.doAfter(2, function() + obj.isRestoring = false + print("WindowManager: Restore Lock Cleared.") + end) +end + +function obj.rescueWindowsToLaptop() + local primary = hs.screen.primaryScreen() + if not primary then return end + print("WindowManager: Rescue Triggered.") + for _, win in ipairs(hs.window.allWindows()) do + if win and win:isVisible() and win:frame().w > 0 then + win:setFrame(primary:fullFrame(), 0) + end + end + obj.isRescued = true + hs.alert.show("Rescued to Laptop", 1.5) +end + +-- ========================================== +-- MENUBAR & WATCHERS +-- ========================================== +local saveCountdown = obj.saveInterval +local timerMenu = hs.menubar.new() + +function updateMenu() + if timerMenu then + local screens = hs.screen.allScreens() + timerMenu:setTitle(string.format("💠 %d:%02d", math.floor(saveCountdown / 60), saveCountdown % 60)) + local menuTable = { + { title = "📸 Save Layout (⇧⌘W)", fn = function() obj.saveLayout(false); saveCountdown = obj.saveInterval end }, + { title = "🔄 Restore Layout (⇧⌘R)", fn = function() obj.isRestoring = false; obj.restoreLayout() end }, + { title = "🚀 Rescue Windows (Bring to Laptop) (⇧⌘⌃L)", fn = obj.rescueWindowsToLaptop } + } + timerMenu:setMenu(menuTable) + end +end + +obj.powerWatcher = hs.caffeinate.watcher.new(function(event) + if event == hs.caffeinate.watcher.systemWillSleep or event == hs.caffeinate.watcher.screensDidSleep or event == hs.caffeinate.watcher.screensDidLock then + print("WindowManager: SLEEP") + obj.isTransitioning = true + elseif event == hs.caffeinate.watcher.systemDidWake or event == hs.caffeinate.watcher.screensDidWake or event == hs.caffeinate.watcher.screensDidUnlock then + print("WindowManager: WAKE") + if obj.wakeTimer then obj.wakeTimer:stop() end + obj.wakeTimer = hs.timer.doAfter(10, function() + obj.isTransitioning = false + obj.isRestoring = false + obj.restoreLayout() + obj.wakeTimer = nil + end) + end +end):start() + +hs.hotkey.bind({"shift", "cmd"}, "W", function() obj.saveLayout(false); saveCountdown = obj.saveInterval end) +hs.hotkey.bind({"shift", "cmd"}, "R", function() obj.isRestoring = false; obj.restoreLayout() end) +hs.hotkey.bind({"shift", "cmd", "ctrl"}, "L", obj.rescueWindowsToLaptop) + +obj.screenWatcher = hs.screen.watcher.new(function() + if obj.isTransitioning or obj.isRestoring or obj.wakeTimer then return end + print("WindowManager: DOCK") + obj.isTransitioning = true + hs.timer.doAfter(5, function() + obj.lastScreenCount = #hs.screen.allScreens() + obj.isTransitioning = false + if obj.lastScreenCount > 1 then obj.restoreLayout() else obj.rescueWindowsToLaptop() end + updateMenu() + end) +end):start() + +obj.autoSaveTimer = hs.timer.doEvery(obj.saveInterval, function() obj.saveLayout(true); saveCountdown = obj.saveInterval end) +obj.clockTimer = hs.timer.doEvery(1, function() + saveCountdown = saveCountdown - 1 + if saveCountdown < 0 then saveCountdown = obj.saveInterval end + updateMenu() +end) + +updateMenu() +return obj \ No newline at end of file diff --git a/LayoutSelector copy.lua b/LayoutSelector copy.lua new file mode 100644 index 0000000..6909b44 --- /dev/null +++ b/LayoutSelector copy.lua @@ -0,0 +1,243 @@ +local selector = {} +local json = require("hs.json") +local styledtext = require("hs.styledtext") + +-- ========================================== +-- CONFIGURATION +-- ========================================== +selector.storageDir = os.getenv("HOME") .. "/.hammerspoon/layouts/" +selector.hotkeys = {} + +if not hs.fs.attributes(selector.storageDir) then hs.fs.mkdir(selector.storageDir) end + +local stubbornAppsList = { + ["Gemini"] = true, ["AFFiNE"] = true, ["Terminal"] = true, + ["System Settings"] = true, ["Hammerspoon"] = true +} + +local ignoreListItems = { + ["TheBoringNotch"] = true, ["Control Center"] = true, ["Notification Center"] = true, + ["Dock"] = true, ["BetterDisplay"] = true, ["Stats"] = true, ["DockDoor"] = true +} + +-- ========================================== +-- INTERNAL UTILITIES +-- ========================================== + +local function wrapText(text, limit) + local lines = {} + local currentLine = "" + for word in text:gmatch("%S+") do + if #currentLine + #word >= limit then + table.insert(lines, currentLine) + currentLine = " " .. word + else + currentLine = currentLine == "" and " ↳ " .. word or currentLine .. " " .. word + end + end + table.insert(lines, currentLine) + return lines +end + +-- ========================================== +-- CORE RESTORE LOGIC (SORTED MATCHING) +-- ========================================== + +local function executeRestore(filePath, layoutName) + local data = hs.json.read(filePath) + if not data or not data.windows then return end + + local launchedAny = false + for _, winData in ipairs(data.windows) do + local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName) + if not app then + hs.application.launchOrFocusByBundleID(winData.bundleID) + launchedAny = true + end + end + + local function moveWindows() + local savedByApp = {} + for _, winData in ipairs(data.windows) do + savedByApp[winData.appName] = savedByApp[winData.appName] or {} + table.insert(savedByApp[winData.appName], winData) + end + + for appName, entries in pairs(savedByApp) do + table.sort(entries, function(a, b) + if a.y == b.y then return a.x < b.x end + return a.y < b.y + end) + end + + for appName, savedEntries in pairs(savedByApp) do + local app = hs.application.get(savedEntries[1].bundleID) or hs.application.get(appName) + if app then + local physicalWins = {} + for _, w in ipairs(app:allWindows()) do + if w:isVisible() and w:frame().w > 0 then table.insert(physicalWins, w) end + end + + table.sort(physicalWins, function(a, b) + local af, bf = a:frame(), b:frame() + if af.y == bf.y then return af.x < bf.x end + return af.y < bf.y + end) + + for i, winData in ipairs(savedEntries) do + local win = physicalWins[i] + if win then + if win:isMinimized() then win:unminimize() end + local x, y, w, h = winData.x, winData.y, winData.w, winData.h + local isStubborn = stubbornAppsList[appName] or (app:path() or ""):find("Electron") + + local function moveAction() + if isStubborn then + local safeTitle = win:title():gsub('"', '\\"') + local winTarget = (appName == "Gemini") and "window 1" or string.format('first window whose name is "%s"', safeTitle) + local script = string.format([[ + tell application "System Events" to tell (first process whose unix id is %d) + try + set targetWin to %s + set position of targetWin to {%d, %d} + set size of targetWin to {%d, %d} + end try + end tell + ]], app:pid(), winTarget, x, y, w, h) + hs.applescript.applescript(script) + else + win:setFrame({x=x, y=y, w=w, h=h}, 0) + end + end + + moveAction() + hs.timer.doAfter(0.5, moveAction) + hs.timer.doAfter(1.5, moveAction) + end + end + end + end + hs.alert.show("Restored: " .. layoutName, 1.5) + end + + if launchedAny then + hs.alert.show("Syncing Apps...", 3) + hs.timer.doAfter(4.5, moveWindows) + else + moveWindows() + end +end + +-- ========================================== +-- MENU BAR & UI +-- ========================================== + +local function captureCurrentLayout() + local screens = hs.screen.allScreens() + local layout = { + saveTime = os.date("%Y-%m-%d %H:%M:%S"), + screenCount = #screens, + mode = (#screens > 1) and "Docked" or "Laptop", + windows = {} + } + for _, win in ipairs(hs.window.allWindows()) do + local app = win:application() + if app and win:isVisible() and win:frame().w > 0 then + local appName = app:name() or "" + if not (ignoreListItems[appName] or appName:find("TheBoringNotch")) then + table.insert(layout.windows, { + appName = appName, bundleID = app:bundleID(), winTitle = win:title(), + x = math.floor(win:frame().x), y = math.floor(win:frame().y), + w = math.floor(win:frame().w), h = math.floor(win:frame().h) + }) + end + end + end + return layout +end + +selector.barItem = hs.menubar.new() + +function selector.refreshMenu() + for _, hk in pairs(selector.hotkeys) do hk:delete() end + selector.hotkeys = {} + + local screens = hs.screen.allScreens() + selector.barItem:setTitle(#screens > 1 and "🖥️" or "💻") + + -- Define global save hotkey + selector.hotkeys["save"] = hs.hotkey.bind({"shift", "ctrl", "alt", "cmd"}, "S", function() + local _, name = hs.dialog.textPrompt("New Layout", "Enter name:", "", "Save", "Cancel") + if name and name ~= "" then + hs.json.write(captureCurrentLayout(), selector.storageDir .. name .. ".json", true, true) + selector.refreshMenu() + end + end) + + local menuTable = { + { title = "📸 Save New Layout... (⇧⌃⌥⌘S)", fn = function() hs.eventtap.keyStroke({"shift", "ctrl", "alt", "cmd"}, "S") end }, + { title = "-" } + } + + local files = {} + local iter, dir_obj = hs.fs.dir(selector.storageDir) + if iter then for f in iter, dir_obj do if f:find("%.json$") then table.insert(files, f) end end end + table.sort(files) + + local boldStyle = { font = { name = ".AppleSystemUIFontBold", size = 13 } } + + for i, file in ipairs(files) do + local path, label = selector.storageDir .. file, file:gsub("%.json$", "") + local data = hs.json.read(path) + + if i <= 9 then + selector.hotkeys[i] = hs.hotkey.bind({"ctrl", "alt", "cmd"}, tostring(i), function() executeRestore(path, label) end) + end + + -- Layout Header + table.insert(menuTable, { + title = hs.styledtext.new("Layout: " .. label .. (i<=9 and " (⌃⌥⌘"..i..")" or ""), boldStyle), + fn = function() executeRestore(path, label) end + }) + + if data then + -- Context Metadata + local modeStr = (data.mode == "Docked") and " 🖥️ Docked (".. (data.screenCount or "?") .." Screens)" or " 💻 Laptop Mode" + table.insert(menuTable, { title = modeStr, disabled = true }) + table.insert(menuTable, { title = " 📅 Saved: " .. (data.saveTime or "Unknown"), disabled = true }) + + -- App List (Wrapped) + if data.windows then + local seen, names = {}, {} + for _, w in ipairs(data.windows) do + if not seen[w.appName] then + table.insert(names, w.appName .. ", ") + seen[w.appName] = true + end + end + local appString = table.concat(names):gsub(", $", "") + for _, line in ipairs(wrapText(appString, 45)) do + table.insert(menuTable, { title = line, disabled = true }) + end + end + end + + -- Actions + table.insert(menuTable, { + title = hs.styledtext.new(" 🔄 Update " .. label, boldStyle), + fn = function() + hs.json.write(captureCurrentLayout(), path, true, true) + selector.refreshMenu() + hs.alert.show("Updated: " .. label) + end + }) + table.insert(menuTable, { title = " 🗑️ Delete", fn = function() if hs.dialog.blockAlert("Delete", "Delete "..label.."?", "Delete", "Cancel", "critical") == "Delete" then os.remove(path); selector.refreshMenu() end end }) + table.insert(menuTable, { title = "-" }) + end + selector.barItem:setMenu(menuTable) +end + +selector.screenWatcher = hs.screen.watcher.new(selector.refreshMenu):start() +selector.refreshMenu() + +return selector \ No newline at end of file diff --git a/LayoutSelector.lua b/LayoutSelector.lua new file mode 100644 index 0000000..a784782 --- /dev/null +++ b/LayoutSelector.lua @@ -0,0 +1,303 @@ +-- LayoutSelector.lua + +local selector = {} +local json = require("hs.json") +local styledtext = require("hs.styledtext") + +-- ========================================== +-- CONFIGURATION & CENTRAL CONFIG LOADING +-- ========================================== +selector.storageDir = os.getenv("HOME") .. "/.hammerspoon/layouts/" +local configFile = os.getenv("HOME") .. "/.hammerspoon/config.json" +selector.layoutHotkeys = {} +selector.staticHotkeys = {} + +if not hs.fs.attributes(selector.storageDir) then hs.fs.mkdir(selector.storageDir) end + +local stubbornAppsList = { + ["Gemini"] = true, ["AFFiNE"] = true, ["Terminal"] = true, + ["System Settings"] = true, ["Hammerspoon"] = true +} + +local ignoreListItems = { + ["TheBoringNotch"] = true, ["Control Center"] = true, ["Notification Center"] = true, + ["Dock"] = true, ["BetterDisplay"] = true, ["Stats"] = true, ["DockDoor"] = true, + ["Bartender 6"] = true, ["Bartender"] = true, ["Arq"] = true, ["Arq Agent"] = true, ["TypeWhisper"] = true, + ["com.typewhisper.mac"] = true +} + +-- Load Central Config if available +local cfgData = hs.json.read(configFile) +if cfgData then + if cfgData.stubbornAppsList then stubbornAppsList = cfgData.stubbornAppsList end + if cfgData.ignoreListItems then ignoreListItems = cfgData.ignoreListItems end +end + +-- ========================================== +-- INTERNAL UTILITIES +-- ========================================== + +local function wrapText(text, limit) + local lines = {} + local currentLine = "" + for word in text:gmatch("%S+") do + if #currentLine + #word >= limit then + table.insert(lines, currentLine) + currentLine = " " .. word + else + currentLine = currentLine == "" and " ↳ " .. word or currentLine .. " " .. word + end + end + table.insert(lines, currentLine) + return lines +end + +local function getLayoutFiles() + local files = {} + local iter, dir_obj = hs.fs.dir(selector.storageDir) + if iter then for f in iter, dir_obj do if f:find("%.json$") then table.insert(files, f) end end end + table.sort(files) + return files +end + +-- ========================================== +-- CORE LOGIC +-- ========================================== + +local function captureCurrentLayout() + local screens = hs.screen.allScreens() + local layout = { + saveTime = os.date("%Y-%m-%d %H:%M:%S"), + screenCount = #screens, + mode = (#screens > 1) and "Docked" or "Laptop", + windows = {} + } + for _, win in ipairs(hs.window.allWindows()) do + local app = win:application() + if app and win:isVisible() and win:frame().w > 0 then + local appName = app:name() or "" + local bundleID = app:bundleID() or "" + -- Hybrid check for Capture + if not ignoreListItems[appName] and not ignoreListItems[bundleID] then + table.insert(layout.windows, { + appName = appName, bundleID = bundleID, winTitle = win:title(), + x = math.floor(win:frame().x), y = math.floor(win:frame().y), + w = math.floor(win:frame().w), h = math.floor(win:frame().h) + }) + end + end + end + return layout +end + +local function executeRestore(filePath, layoutName) + local data = hs.json.read(filePath) + if not data or not data.windows then return end + + local launchedAny = false + for _, winData in ipairs(data.windows) do + local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName) + if not app then + hs.application.launchOrFocusByBundleID(winData.bundleID) + launchedAny = true + end + end + + local function moveWindows() + local savedByApp = {} + for _, winData in ipairs(data.windows) do + -- Hybrid check for Restore processing + if not ignoreListItems[winData.appName] and not ignoreListItems[winData.bundleID] then + savedByApp[winData.appName] = savedByApp[winData.appName] or {} + table.insert(savedByApp[winData.appName], winData) + end + end + + for appName, entries in pairs(savedByApp) do + table.sort(entries, function(a, b) + if a.y == b.y then return a.x < b.x end + return a.y < b.y + end) + end + + for appName, savedEntries in pairs(savedByApp) do + local app = hs.application.get(savedEntries[1].bundleID) or hs.application.get(appName) + if app then + local physicalWins = {} + for _, w in ipairs(app:allWindows()) do + if w:isVisible() and w:frame().w > 0 then table.insert(physicalWins, w) end + end + + table.sort(physicalWins, function(a, b) + local af, bf = a:frame(), b:frame() + if af.y == bf.y then return af.x < bf.x end + return af.y < bf.y + end) + + for i, winData in ipairs(savedEntries) do + local win = physicalWins[i] + if win then + if win:isMinimized() then win:unminimize() end + local x, y, w, h = winData.x, winData.y, winData.w, winData.h + local isStubborn = stubbornAppsList[appName] or (app:path() or ""):find("Electron") + + local function moveAction() + if isStubborn then + local safeTitle = win:title():gsub('"', '\\"') + local winTarget = (appName == "Gemini") and "window 1" or string.format('first window whose name is "%s"', safeTitle) + local script = string.format([[ + tell application "System Events" to tell (first process whose unix id is %d) + try + set targetWin to %s + set position of targetWin to {%d, %d} + set size of targetWin to {%d, %d} + end try + end tell + ]], app:pid(), winTarget, x, y, w, h) + hs.applescript.applescript(script) + else + win:setFrame({x=x, y=y, w=w, h=h}, 0) + end + end + moveAction() + hs.timer.doAfter(0.5, moveAction) + hs.timer.doAfter(1.5, moveAction) + end + end + end + end + hs.alert.show("Restored: " .. layoutName, 1.5) + end + + if launchedAny then + hs.alert.show("Syncing Apps...", 3) + hs.timer.doAfter(4.5, moveWindows) + else + moveWindows() + end +end + +-- ========================================== +-- HOTKEY & MENU MANAGEMENT +-- ========================================== + +function selector.rebindLayoutKeys() + -- Only rebind when called explicitly (on file changes) + for _, hk in pairs(selector.layoutHotkeys) do hk:delete() end + selector.layoutHotkeys = {} + + local files = getLayoutFiles() + for i, file in ipairs(files) do + if i > 9 then break end + local path = selector.storageDir .. file + local label = file:gsub("%.json$", "") + selector.layoutHotkeys[i] = hs.hotkey.bind({"ctrl", "alt", "cmd"}, tostring(i), function() executeRestore(path, label) end) + end + print("LayoutSelector: Dynamic Hotkeys Rebuilt") +end + +selector.barItem = hs.menubar.new() + +function selector.refreshMenu() + -- Guard against title-looping + local icon = #hs.screen.allScreens() > 1 and "🖥️" or "💻" + if selector.barItem:title() ~= icon then selector.barItem:setTitle(icon) end + + local menuTable = { + { title = "📸 Save New Layout... (⇧⌃⌥⌘S)", fn = function() hs.eventtap.keyStroke({"shift", "ctrl", "alt", "cmd"}, "S") end }, + { title = "-" } + } + + local files = getLayoutFiles() + local boldStyle = { font = { name = ".AppleSystemUIFontBold", size = 13 } } + + for i, file in ipairs(files) do + local path, label = selector.storageDir .. file, file:gsub("%.json$", "") + local data = hs.json.read(path) + local layoutSubmenu = {} + + -- Logic to determine visual cue icon + local mainIcon = "" + if data and data.mode == "Docked" then mainIcon = "🖥️ " elseif data and data.mode == "Laptop" then mainIcon = "💻 " end + + table.insert(layoutSubmenu, { + title = hs.styledtext.new("🚀 Restore Layout", boldStyle), + fn = function() executeRestore(path, label) end + }) + table.insert(layoutSubmenu, { title = "-" }) + + if data then + local modeStr = (data.mode == "Docked") and "🖥️ Docked (".. (data.screenCount or "?") .." Screens)" or "💻 Laptop Mode" + table.insert(layoutSubmenu, { title = modeStr, disabled = true }) + table.insert(layoutSubmenu, { title = "📅 Saved: " .. (data.saveTime or "Unknown"), disabled = true }) + + if data.windows then + local seen, names = {}, {} + for _, w in ipairs(data.windows) do + if not seen[w.appName] then + table.insert(names, w.appName .. ", ") + seen[w.appName] = true + end + end + local appString = table.concat(names):gsub(", $", "") + for _, line in ipairs(wrapText(appString, 45)) do table.insert(layoutSubmenu, { title = line, disabled = true }) end + end + end + + table.insert(layoutSubmenu, { title = "-" }) + table.insert(layoutSubmenu, { + title = "🔄 Update " .. label, + fn = function() + hs.json.write(captureCurrentLayout(), path, true, true) + selector.refreshMenu() + hs.alert.show("Updated: " .. label) + end + }) + table.insert(layoutSubmenu, { title = "✏️ Rename", fn = function() + local _, newName = hs.dialog.textPrompt("Rename Layout", "Enter new name for " .. label .. ":", label, "Rename", "Cancel") + if newName and newName ~= "" and newName ~= label then + local newPath = selector.storageDir .. newName .. ".json" + os.rename(path, newPath) + selector.rebindLayoutKeys() + selector.refreshMenu() + end + end }) + table.insert(layoutSubmenu, { title = "🗑️ Delete", fn = function() + if hs.dialog.blockAlert("Delete", "Delete "..label.."?", "Delete", "Cancel", "critical") == "Delete" then + os.remove(path) + selector.rebindLayoutKeys() + selector.refreshMenu() + end + end }) + + table.insert(menuTable, { + title = hs.styledtext.new(mainIcon .. "Layout: " .. label .. (i<=9 and " (⌃⌥⌘"..i..")" or ""), boldStyle), + menu = layoutSubmenu + }) + end + selector.barItem:setMenu(menuTable) +end + +-- ========================================== +-- INITIALIZATION +-- ========================================== + +-- Static Save Hotkey (Defined once) +selector.staticHotkeys["save"] = hs.hotkey.bind({"shift", "ctrl", "alt", "cmd"}, "S", function() + local _, name = hs.dialog.textPrompt("New Layout", "Enter name:", "", "Save", "Cancel") + if name and name ~= "" then + hs.json.write(captureCurrentLayout(), selector.storageDir .. name .. ".json", true, true) + selector.rebindLayoutKeys() -- Rebuild keys only when file added + selector.refreshMenu() + end +end) + +-- Screen watcher only refreshes the VISUAL menu, not the keys +selector.screenWatcher = hs.screen.watcher.new(function() + hs.timer.doAfter(2, selector.refreshMenu) +end):start() + +selector.rebindLayoutKeys() -- Run once at load +selector.refreshMenu() -- Run once at load + +return selector \ No newline at end of file diff --git a/NetworkCenter.lua b/NetworkCenter.lua new file mode 100644 index 0000000..af9532f --- /dev/null +++ b/NetworkCenter.lua @@ -0,0 +1,326 @@ +-- ~/.hammerspoon/NetworkCenter.lua +local NetworkCenter = {} +local Config = require("Config") +local menuBar = hs.menubar.new() + +-- State Management +local nasOnline = false +local vpnActive = false +local ovpnIP = "Offline" +local tailscaleIP = "Offline" +local tailnetDomain = "Offline" -- NEW: Added state for domain +local speedTestResult = "Idle" +local lastExternalIP = "Offline" +local ispName = "Fetching..." +local wifiInfo = { + ssid = "N/A", speed = 0, gen = "N/A", band = "N/A", + channel = "N/A", protocol = "N/A", rssi = 0, noise = 0 +} +local gatewayLatency = "---" + +-- Service Monitoring State +local services = { + { name = "Plex", host = "192.168.1.105", port = 32400 }, + { name = "Komga", host = "192.168.1.101", port = 25600 }, + { name = "Sonarr", host = "192.168.1.101", port = 8989 }, + { name = "Radarr", host = "192.168.1.101", port = 7878 }, + { name = "qBitorrent", host = "192.168.1.101", port = 8080 } +} +local serviceStatus = {} + +-- ========================================== +-- 1. DATA RETRIEVAL +-- ========================================== +local function getWifiDetails() + local details = hs.wifi.interfaceDetails() + if details then + wifiInfo.ssid = details.ssid or "Disconnected" + wifiInfo.speed = details.transmitRate or 0 + wifiInfo.rssi = details.rssi or 0 + wifiInfo.noise = details.noise or 0 + + local phy = details.activePHYMode + local phyStr = tostring(phy) + + if phy == 6 or phyStr:find("6") or phyStr:find("ax") then + wifiInfo.gen, wifiInfo.protocol = "6", "AX" + elseif phy == 5 or phyStr:find("5") or phyStr:find("ac") then + wifiInfo.gen, wifiInfo.protocol = "5", "AC" + elseif phy == 4 or phyStr:find("4") or phyStr:find("n") then + wifiInfo.gen, wifiInfo.protocol = "4", "N" + else + wifiInfo.gen, wifiInfo.protocol = "?", "?" + end + + if details.wlanChannel and type(details.wlanChannel) == "table" then + wifiInfo.band = tostring(details.wlanChannel.band):gsub("GHz", "") or "N/A" + wifiInfo.channel = tostring(details.wlanChannel.number) or "N/A" + end + else + wifiInfo.ssid = hs.wifi.currentNetwork() or "N/A" + end +end + +function NetworkCenter.updateStatus() + getWifiDetails() + + -- Async NAS Ping + hs.task.new("/sbin/ping", function(code) + nasOnline = (code == 0) + NetworkCenter.refreshUI() + end, {"-c", "1", "-t", "1", Config.nasIP}):start() + + -- Gateway Latency + hs.task.new("/sbin/ping", function(code, stdOut) + if code == 0 and stdOut then + local ms = stdOut:match("time=(%d+%.?%d*) ms") + gatewayLatency = ms and (math.floor(tonumber(ms)) .. "ms") or "Error" + else + gatewayLatency = "Timeout" + end + NetworkCenter.refreshUI() + end, {"-c", "1", "-t", "1", "192.168.1.1"}):start() + + -- Service Port Checks + for _, svc in ipairs(services) do + hs.task.new("/usr/bin/nc", function(code) + serviceStatus[svc.name] = (code == 0 and "🟢" or "🔴") + NetworkCenter.refreshUI() + end, {"-z", "-w", "2", svc.host, tostring(svc.port)}):start() + end + + -- Tailscale Detection & Domain Retrieval + local tsPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale" + local tsStatus = hs.execute(tsPath .. " status --json 2>/dev/null") + local foundTSIP = "Offline" + local foundTSDomain = "Offline" + + if tsStatus and tsStatus ~= "" then + local tsData = hs.json.decode(tsStatus) + if tsData and tsData.BackendState == "Running" then + -- Get the Tailnet name/domain + foundTSDomain = tsData.MagicDNSSuffix or "N/A" + + if tsData.Self and tsData.Self.TailscaleIPs then + for _, ip in ipairs(tsData.Self.TailscaleIPs) do + if ip:match("^100%.") then foundTSIP = ip break end + end + end + end + end + tailscaleIP = foundTSIP + tailnetDomain = foundTSDomain + + -- OpenVPN Detection + local ovpnCheck = hs.execute("pgrep -x 'OpenVPN Connect'"):gsub("%s+", "") + local ovpnIPQuery = hs.execute("ifconfig | grep -A 1 'utun' | grep 'inet 10.' | awk '{print $2}'"):gsub("%s+", "") + + if ovpnCheck ~= "" and ovpnIPQuery ~= "" then + vpnActive = true + ovpnIP = ovpnIPQuery + else + vpnActive = false + ovpnIP = "Offline" + end + + -- External IP & ISP + hs.task.new("/usr/bin/curl", function(exitCode, stdOut) + if exitCode == 0 and stdOut then + local data = hs.json.decode(stdOut) + if data then + lastExternalIP = data.ip or "Offline" + ispName = data.asn_org or data.company_name or "Unknown ISP" + end + else + lastExternalIP = "Offline" + ispName = "Offline" + end + NetworkCenter.refreshUI() + end, {"-s", "-m", "5", "https://ifconfig.co/json"}):start() +end + +local function runSpeedTest() + speedTestResult = "Testing..." + NetworkCenter.refreshUI() + hs.task.new("/usr/bin/networkQuality", function(exitCode, stdOut) + if exitCode == 0 and stdOut then + local data = hs.json.decode(stdOut) + if data then + local downMbps = math.floor((data.dl_throughput / 1048576) + 0.5) + local upMbps = math.floor((data.ul_throughput / 1048576) + 0.5) + speedTestResult = string.format("D:%d / U:%d Mbps", downMbps, upMbps) + else + speedTestResult = "Parse Error" + end + else + speedTestResult = "Failed" + end + NetworkCenter.refreshUI() + end, {"-c"}):start() +end + +-- ========================================== +-- 2. ACTIONS & STEALTH LOGIC +-- ========================================== + +local function fastHideOpenVPN() + local count = 0 + local hideTimer + hideTimer = hs.timer.doEvery(0.02, function() + local app = hs.application.get("OpenVPN Connect") + if app then + app:hide() + hs.applescript.applescript('tell application "System Events" to set visible of process "OpenVPN Connect" to false') + local win = app:mainWindow() + if win then win:setFrame(hs.geometry.rect(5000, 5000, 0, 0), 0) end + end + count = count + 1 + if count > 75 then hideTimer:stop() end + end) +end + +local function watchForVPN() + local attempts = 0 + local watchTimer + watchTimer = hs.timer.doEvery(1, function() + attempts = attempts + 1 + local checkIP = hs.execute("ifconfig | grep -A 1 'utun' | grep 'inet 10.'"):gsub("%s+", "") + if checkIP ~= "" or attempts > 10 then + NetworkCenter.updateStatus() + watchTimer:stop() + end + end) +end + +local function vpnAction(mode) + if mode == "connect" then + hs.alert.show("Connecting VPN...") + fastHideOpenVPN() + hs.execute(string.format('"%s" --connect-shortcut=%s', Config.openvpnPath, Config.vpnProfileID)) + watchForVPN() + else + hs.alert.show("Disconnecting VPN...") + hs.execute(string.format('"%s" --disconnect-shortcut', Config.openvpnPath)) + hs.timer.doAfter(0.5, function() + hs.execute("pkill -9 'OpenVPN Connect'") + NetworkCenter.updateStatus() + end) + end +end + +local function tailscaleAction(mode) + local tsPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale" + if mode == "up" then + hs.alert.show("Starting Tailscale...") + hs.execute(tsPath .. " up") + else + hs.alert.show("Stopping Tailscale...") + hs.execute(tsPath .. " down") + end + hs.timer.doAfter(1, NetworkCenter.updateStatus) + hs.timer.doAfter(3, NetworkCenter.updateStatus) +end + +-- ========================================== +-- 3. THE MENU UI +-- ========================================== + +function NetworkCenter.refreshUI() + if not menuBar then return end + + local isHome = (wifiInfo.ssid == Config.homeSSID) + local locationStr = isHome and "HOME" or "REMOTE" + local topIcon = isHome and "🏠" or "🌐" + + local tsActive = (tailscaleIP ~= "Offline") + if vpnActive and tsActive then + topIcon = topIcon .. "🛡️" + elseif vpnActive then + topIcon = topIcon .. "🔒" + elseif tsActive then + topIcon = topIcon .. "📡" + end + + menuBar:setTitle(topIcon .. " " .. math.floor(wifiInfo.speed) .. "Mb") + + local shareSubMenu = {} + for i, share in ipairs(Config.shares) do + local isMounted = hs.fs.attributes("/Volumes/" .. share) ~= nil + local sIcon = isMounted and "✅" or "⚪️" + local titleText = sIcon .. " " .. share + if isMounted then + local capCmd = "df -h '/Volumes/" .. share .. "' | tail -1 | awk '{gsub(\"i\", \"b\", $2); print \"(\" $2 \")\"}'" + titleText = titleText .. " " .. hs.execute(capCmd):gsub("\n", "") + end + table.insert(shareSubMenu, { + title = titleText, + fn = function() + if isMounted then hs.execute("diskutil unmount /Volumes/" .. share) + else hs.applescript.applescript(string.format('mount volume "smb://%s/%s"', Config.nasIP, share)) end + hs.timer.doAfter(3, NetworkCenter.updateStatus) + end + }) + if isMounted then + local barCmd = "df -h '/Volumes/" .. share .. "' | tail -1 | awk '{p=int($5); bar=\"\"; for(i=1;i<=10;i++){if(i<=p/10) bar=bar \"■\"; else bar=bar \"□\"} gsub(\"i\", \"b\", $3); print \" [\" bar \"] \" p \"% (\" $3 \" Used)\"}'" + table.insert(shareSubMenu, { title = hs.execute(barCmd):gsub("\n", ""), disabled = true }) + table.insert(shareSubMenu, { title = " 📂 Browse Folder", fn = function() hs.execute("open /Volumes/" .. share) end }) + table.insert(shareSubMenu, { title = "-" }) + end + end + + local labSubMenu = {} + local labIcons = "" + for i, svc in ipairs(services) do + local status = serviceStatus[svc.name] or "⚪️" + labIcons = labIcons .. status + table.insert(labSubMenu, { + title = status .. " " .. svc.name, + fn = function() hs.execute(string.format("open http://%s:%s", svc.host, svc.port)) end + }) + end + + local ovpnHeader = "🔐 OPENVPN CONTROLS" .. (vpnActive and " (🟢 ACTIVE)" or "") + local tsHeader = "🚀 TAILSCALE MESH" .. (tsActive and " (🟢 ACTIVE)" or "") + + local menu = { + { title = "📍 LOCATION STATUS" }, + { title = " ├─ Profile: " .. locationStr, disabled = true }, + { title = " ├─ Gateway: " .. gatewayLatency, disabled = true }, + { title = " ├─ Channel: " .. wifiInfo.channel .. " (" .. wifiInfo.band .. " GHz)", disabled = true }, + { title = " ├─ Signal: " .. wifiInfo.rssi .. " dBm / Noise: " .. wifiInfo.noise .. " dBm", disabled = true }, + { title = " └─ WiFi " .. wifiInfo.gen .. " (" .. wifiInfo.protocol .. ") @ " .. wifiInfo.speed .. " Mbps", disabled = true }, + { title = "-" }, + { title = "🚀 LAB SERVICES " .. labIcons, menu = labSubMenu }, + { title = "-" }, + { title = "🛡️ VPN & MESH NET" }, + { title = " ├─ OpenVPN: " .. ovpnIP, disabled = true }, + { title = " ├─ Tailscale IP: " .. tailscaleIP, disabled = true }, + { title = " ├─ Tailnet: " .. tailnetDomain, disabled = true }, -- NEW: Display Domain + { title = " ├─ ISP: " .. ispName, disabled = true }, + { title = " └─ Public IP: " .. lastExternalIP, disabled = true }, + { title = "-" }, + { title = "📦 STORAGE & SHARES", menu = shareSubMenu }, + { title = " ├─ NAS IP: " .. Config.nasIP, disabled = true }, + { title = " └─ NAS Status: " .. (nasOnline and "🟢 Online" or "🔴 Offline"), disabled = true }, + { title = "-" }, + { title = "⚡️ SPEED TEST: " .. speedTestResult, fn = runSpeedTest }, + { title = "-" }, + { title = ovpnHeader }, + { title = " ├─ 🔒 Connect tunnel", fn = function() vpnAction("connect") end }, + { title = " └─ 🔓 Disconnect & Quit", fn = function() vpnAction("disconnect") end }, + { title = "-" }, + { title = tsHeader }, + { title = " ├─ 🟢 Start Service", fn = function() tailscaleAction("up") end }, + { title = " └─ 🔴 Stop Service", fn = function() tailscaleAction("down") end }, + { title = "-" }, + { title = "🔄 Refresh Status", fn = NetworkCenter.updateStatus } + } + + menuBar:setMenu(menu) +end + +NetworkCenter.updateStatus() +hs.timer.doEvery(30, NetworkCenter.updateStatus) +hs.alert.show("Network Center Platinum Loaded") + +return NetworkCenter \ No newline at end of file diff --git a/NetworkMenu.lua b/NetworkMenu.lua new file mode 100644 index 0000000..c2c0f85 --- /dev/null +++ b/NetworkMenu.lua @@ -0,0 +1,163 @@ +-- ~/.hammerspoon/NetworkMenu.lua +NetworkMenu = {} +NetworkMenu.menuBarItem = hs.menubar.new() + +-- Global state management +automationEnabled = (automationEnabled == nil) and true or automationEnabled +local nasOnline = false +local lastInternalIP = "Offline" +local lastExternalIP = "Offline" +local vpnActive = false + +-- 1. ASYNC STATUS UPDATE (Prevents hangs and crashes) +function updateStatus() + if not config then + print("NetworkMenu Error: Config not found") + return + end + + -- A. Async NAS Ping + hs.task.new("/sbin/ping", function(code) + nasOnline = (code == 0) + end, {"-c", "1", "-t", "1", config.nasIP}):start() + + -- B. Internal VPN IP (Fast local command) + local internalCmd = "ifconfig | grep -A 1 'utun' | grep 'inet ' | awk '{print $2}' | head -n 1" + local internalIP = hs.execute(internalCmd):gsub("%s+", "") + lastInternalIP = (internalIP == "" and "Offline" or internalIP) + vpnActive = (internalIP ~= "") + + -- C. Async External IP + hs.task.new("/usr/bin/curl", function(exitCode, stdOut) + if exitCode == 0 and stdOut then + lastExternalIP = stdOut:gsub("%s+", "") + else + lastExternalIP = "Offline" + end + end, {"-s", "-m", "2", "icanhazip.com"}):start() + + -- D. Update Menu Icon + local currentSSID = hs.wifi.currentNetwork() + if NetworkMenu.menuBarItem then + local automationIcon = automationEnabled and "" or "🔘" + local icon = (currentSSID == config.homeSSID) and "🏠" or "🌐" + if vpnActive then icon = icon .. "🛡️" end + if nasOnline then icon = icon .. "🟢" end + NetworkMenu.menuBarItem:setTitle(automationIcon .. icon) + end +end + +-- 2. Aggressive Backgrounding for OpenVPN +function NetworkMenu.deepHideOpenVPN() + local count = 0 + local hideTimer + hideTimer = hs.timer.doEvery(0.1, function() + local app = hs.application.get("OpenVPN Connect") + if app then + app:hide() + hs.applescript.applescript('tell application "System Events" to set visible of process "OpenVPN Connect" to false') + end + count = count + 1 + if count > 30 then hideTimer:stop() end + end) +end + +-- 3. Check Share Status (Local Filesystem Check) +local function getShareStatus() + local statusLines = {} + if not config or not config.shares then return statusLines end + for _, share in ipairs(config.shares) do + local path = "/Volumes/" .. share + local isMounted = hs.fs.attributes(path) ~= nil + local icon = isMounted and "🟢" or "⚪️" + table.insert(statusLines, { title = " " .. icon .. " " .. share, disabled = true }) + end + return statusLines +end + +-- 4. THE MENU UI +function NetworkMenu.buildMenu() + local currentSSID = hs.wifi.currentNetwork() or "Disconnected" + local isHome = config and (currentSSID == config.homeSSID) + + local menu = { + { title = (automationEnabled and "🟢" or "🔴") .. " Automation: " .. (automationEnabled and "Enabled" or "Disabled"), + fn = function() + automationEnabled = not automationEnabled + updateStatus() + hs.alert.show("Network Automation: " .. (automationEnabled and "ON" or "OFF")) + end + }, + { title = "-" }, + { title = "📍 Profile: " .. (isHome and "Home" or "Remote"), disabled = true }, + { title = " " .. (isHome and "🏠" or "🌐") .. " Location: " .. currentSSID, disabled = true }, + { title = "-" }, + { title = (vpnActive and "🔒" or "🔓") .. " VPN: " .. (vpnActive and "Connected" or "Disconnected"), disabled = true }, + { title = " 🆔 Int IP: " .. lastInternalIP, disabled = true }, + { title = " 🌍 Ext IP: " .. lastExternalIP, disabled = true }, + { title = (nasOnline and "✅" or "❌") .. " NAS Status: " .. (nasOnline and "Online" or "Offline"), disabled = true }, + { title = " 📂 Mounted Shares:", disabled = true }, + } + + local shares = getShareStatus() + for _, s in ipairs(shares) do table.insert(menu, s) end + + table.insert(menu, { title = "-" }) + table.insert(menu, { title = "⚡️ VPN Controls:", disabled = true }) + table.insert(menu, { title = " 🔒 Connect VPN", fn = function() + if config then + hs.execute(string.format('"%s" --connect-shortcut=%s', config.openvpnPath, config.vpnProfileID)) + NetworkMenu.deepHideOpenVPN() + hs.timer.doAfter(6, updateStatus) + end + end }) + + table.insert(menu, { title = " 🔓 Disconnect & Quit VPN", fn = function() + if config then + hs.execute(string.format('"%s" --disconnect-shortcut', config.openvpnPath)) + hs.timer.doAfter(1, function() + local app = hs.application.get("OpenVPN Connect") + if app then app:kill() end + updateStatus() + end) + end + end }) + + table.insert(menu, { title = "-" }) + table.insert(menu, { title = "📂 NAS Actions:", disabled = true }) + + table.insert(menu, { title = " ⬆️ Mount All Shares", fn = function() + if config then + for _, share in ipairs(config.shares) do + hs.applescript.applescript(string.format('mount volume "smb://%s/%s"', config.nasIP, share)) + end + end + end }) + + table.insert(menu, { title = " ⬇️ Unmount All Shares", fn = function() + if config then + for _, share in ipairs(config.shares) do + hs.execute(string.format("diskutil unmount /Volumes/%s", share)) + end + end + end }) + + table.insert(menu, { title = "-" }) + table.insert(menu, { title = "🔄 Refresh Status", fn = updateStatus }) + + return menu +end + +-- 5. INITIALIZATION +if NetworkMenu.menuBarItem then + NetworkMenu.menuBarItem:setMenu(NetworkMenu.buildMenu) + -- Small delay before first status update to ensure config is ready + hs.timer.doAfter(1, updateStatus) +end + +-- Watchers +NetworkMenu.watcher = hs.wifi.watcher.new(updateStatus):start() +NetworkMenu.timer = hs.timer.doEvery(30, updateStatus) + +print("NetworkMenu Module Loaded Successfully") +return NetworkMenu \ No newline at end of file diff --git a/SearchWindows.lua b/SearchWindows.lua new file mode 100644 index 0000000..657b9e3 --- /dev/null +++ b/SearchWindows.lua @@ -0,0 +1,229 @@ +-- 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) \ No newline at end of file diff --git a/Spoons/.DS_Store b/Spoons/.DS_Store new file mode 100644 index 0000000..aa10e5c Binary files /dev/null and b/Spoons/.DS_Store differ diff --git a/Spoons/BrewInfo.spoon/docs.json b/Spoons/BrewInfo.spoon/docs.json new file mode 100644 index 0000000..bd9ed36 --- /dev/null +++ b/Spoons/BrewInfo.spoon/docs.json @@ -0,0 +1,322 @@ +[ + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [ + { + "def": "BrewInfo:bindHotkeys(mapping)", + "desc": "Binds hotkeys for BrewInfo", + "doc": "Binds hotkeys for BrewInfo\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following items:\n * show_brew_info - Show output of `brew info` using the selected text as package name\n * open_brew_url - Open the homepage of the formula whose name is currently selected\n * show_brew_cask_info - Show output of `brew cask info` using the selected text as package name\n * open_brew_cask_url - Open the homepage of the Cask whose name is currently selected", + "examples": [], + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "166", + "name": "bindHotkeys", + "notes": [], + "parameters": [ + " * mapping - A table containing hotkey modifier/key details for the following items:\n * show_brew_info - Show output of `brew info` using the selected text as package name\n * open_brew_url - Open the homepage of the formula whose name is currently selected\n * show_brew_cask_info - Show output of `brew cask info` using the selected text as package name\n * open_brew_cask_url - Open the homepage of the Cask whose name is currently selected" + ], + "returns": [], + "signature": "BrewInfo:bindHotkeys(mapping)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "BrewInfo:openBrewURL(pkg, subcommand)", + "desc": "Opens the homepage for package `pkg`, as obtained from the `homepage` field in `brew cat `", + "doc": "Opens the homepage for package `pkg`, as obtained from the `homepage` field in `brew cat `\n\nParameters:\n * pkg - name of the package to query\n * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat \" being run. For example, if `subcommand` is \"cask\", the `brew cask cat ` command will be used.\n\nReturns:\n * The Spoon object", + "examples": [], + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "126", + "name": "openBrewURL", + "notes": [], + "parameters": [ + " * pkg - name of the package to query", + " * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat \" being run. For example, if `subcommand` is \"cask\", the `brew cask cat ` command will be used." + ], + "returns": [ + " * The Spoon object" + ], + "signature": "BrewInfo:openBrewURL(pkg, subcommand)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "BrewInfo:openBrewURLCurSel(subcommand)", + "desc": "Opens the homepage for the currently-selected package, as obtained from the `homepage` field in `brew cat `", + "doc": "Opens the homepage for the currently-selected package, as obtained from the `homepage` field in `brew cat `\n\nParameters:\n * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat \" being run. For example, if `subcommand` is \"cask\", the `brew cask cat ` command will be used.\n\nReturns:\n * The Spoon object", + "examples": [], + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "153", + "name": "openBrewURLCurSel", + "notes": [], + "parameters": [ + " * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat \" being run. For example, if `subcommand` is \"cask\", the `brew cask cat ` command will be used." + ], + "returns": [ + " * The Spoon object" + ], + "signature": "BrewInfo:openBrewURLCurSel(subcommand)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "BrewInfo:showBrewInfo(pkg, subcommand)", + "desc": "Displays an alert with the output of `brew info `", + "doc": "Displays an alert with the output of `brew info `\n\nParameters:\n * pkg - name of the package to query\n * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info \" being run. For example, if `subcommand` is \"cask\", the `brew cask info ` command will be used.\n\nReturns:\n * The Spoon object", + "examples": [], + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "89", + "name": "showBrewInfo", + "notes": [], + "parameters": [ + " * pkg - name of the package to query", + " * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info \" being run. For example, if `subcommand` is \"cask\", the `brew cask info ` command will be used." + ], + "returns": [ + " * The Spoon object" + ], + "signature": "BrewInfo:showBrewInfo(pkg, subcommand)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "BrewInfo:showBrewInfoCurSel(subcommand)", + "desc": "Display `brew info` using the selected text as the package name", + "doc": "Display `brew info` using the selected text as the package name\n\nParameters:\n * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info\" being run. For example, if `subcommand` is \"cask\", the `brew cask info` command will be used.\n\nReturns:\n * The Spoon object", + "examples": [], + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "113", + "name": "showBrewInfoCurSel", + "notes": [], + "parameters": [ + " * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info\" being run. For example, if `subcommand` is \"cask\", the `brew cask info` command will be used." + ], + "returns": [ + " * The Spoon object" + ], + "signature": "BrewInfo:showBrewInfoCurSel(subcommand)", + "stripped_doc": "", + "type": "Method" + } + ], + "Variable": [ + { + "def": "BrewInfo.brew_info_delay_sec", + "desc": "An integer specifying how long the alerts generated by BrewInfo will stay onscreen", + "doc": "An integer specifying how long the alerts generated by BrewInfo will stay onscreen", + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "30", + "name": "brew_info_delay_sec", + "signature": "BrewInfo.brew_info_delay_sec", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "BrewInfo.brew_info_style", + "desc": "A table in conformance with the [hs.alert.defaultStyle](http://www.hammerspoon.org/docs/hs.alert.html#defaultStyle[]) format that specifies the style used by the alerts. Default value: `{ textFont = \"Courier New\", textSize = 14, radius = 10 }`", + "doc": "A table in conformance with the [hs.alert.defaultStyle](http://www.hammerspoon.org/docs/hs.alert.html#defaultStyle[]) format that specifies the style used by the alerts. Default value: `{ textFont = \"Courier New\", textSize = 14, radius = 10 }`", + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "35", + "name": "brew_info_style", + "signature": "BrewInfo.brew_info_style", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "BrewInfo.brew_path", + "desc": "A string specifying the path to the `brew` executable. Defaults to `/usr/local/bin/brew`", + "doc": "A string specifying the path to the `brew` executable. Defaults to `/usr/local/bin/brew`", + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "25", + "name": "brew_path", + "signature": "BrewInfo.brew_path", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "BrewInfo.select_text_if_needed", + "desc": "If `true`, and no text is currently selected in the terminal, issue a double-click to select the text below the cursor, and use that as the input to `brew info`. See also `BrewInfo.select_text_modifiers`. Defaults to `true`.", + "doc": "If `true`, and no text is currently selected in the terminal, issue a double-click to select the text below the cursor, and use that as the input to `brew info`. See also `BrewInfo.select_text_modifiers`. Defaults to `true`.", + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "44", + "name": "select_text_if_needed", + "signature": "BrewInfo.select_text_if_needed", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "BrewInfo.select_text_modifiers", + "desc": "Table containing the modifiers to be used together with a double-click when `BrewInfo.select_text_if_needed` is true. Defaults to `{cmd = true, shift = true}` to issue a Cmd-Shift-double-click, which will select a continuous non-space string in Terminal and iTerm2.", + "doc": "Table containing the modifiers to be used together with a double-click when `BrewInfo.select_text_if_needed` is true. Defaults to `{cmd = true, shift = true}` to issue a Cmd-Shift-double-click, which will select a continuous non-space string in Terminal and iTerm2.", + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "49", + "name": "select_text_modifiers", + "signature": "BrewInfo.select_text_modifiers", + "stripped_doc": "", + "type": "Variable" + } + ], + "desc": "Display pop-up with Homebrew Formula info, or open their URL", + "doc": "Display pop-up with Homebrew Formula info, or open their URL\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip)\n\nYou can bind keys to automatically display the output of `brew\ninfo` of the currently-selected package name, or to open its\nhomepage. I use it to quickly explore new packages from the output\nof `brew update`.", + "items": [ + { + "def": "BrewInfo:bindHotkeys(mapping)", + "desc": "Binds hotkeys for BrewInfo", + "doc": "Binds hotkeys for BrewInfo\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following items:\n * show_brew_info - Show output of `brew info` using the selected text as package name\n * open_brew_url - Open the homepage of the formula whose name is currently selected\n * show_brew_cask_info - Show output of `brew cask info` using the selected text as package name\n * open_brew_cask_url - Open the homepage of the Cask whose name is currently selected", + "examples": [], + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "166", + "name": "bindHotkeys", + "notes": [], + "parameters": [ + " * mapping - A table containing hotkey modifier/key details for the following items:\n * show_brew_info - Show output of `brew info` using the selected text as package name\n * open_brew_url - Open the homepage of the formula whose name is currently selected\n * show_brew_cask_info - Show output of `brew cask info` using the selected text as package name\n * open_brew_cask_url - Open the homepage of the Cask whose name is currently selected" + ], + "returns": [], + "signature": "BrewInfo:bindHotkeys(mapping)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "BrewInfo.brew_info_delay_sec", + "desc": "An integer specifying how long the alerts generated by BrewInfo will stay onscreen", + "doc": "An integer specifying how long the alerts generated by BrewInfo will stay onscreen", + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "30", + "name": "brew_info_delay_sec", + "signature": "BrewInfo.brew_info_delay_sec", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "BrewInfo.brew_info_style", + "desc": "A table in conformance with the [hs.alert.defaultStyle](http://www.hammerspoon.org/docs/hs.alert.html#defaultStyle[]) format that specifies the style used by the alerts. Default value: `{ textFont = \"Courier New\", textSize = 14, radius = 10 }`", + "doc": "A table in conformance with the [hs.alert.defaultStyle](http://www.hammerspoon.org/docs/hs.alert.html#defaultStyle[]) format that specifies the style used by the alerts. Default value: `{ textFont = \"Courier New\", textSize = 14, radius = 10 }`", + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "35", + "name": "brew_info_style", + "signature": "BrewInfo.brew_info_style", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "BrewInfo.brew_path", + "desc": "A string specifying the path to the `brew` executable. Defaults to `/usr/local/bin/brew`", + "doc": "A string specifying the path to the `brew` executable. Defaults to `/usr/local/bin/brew`", + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "25", + "name": "brew_path", + "signature": "BrewInfo.brew_path", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "BrewInfo:openBrewURL(pkg, subcommand)", + "desc": "Opens the homepage for package `pkg`, as obtained from the `homepage` field in `brew cat `", + "doc": "Opens the homepage for package `pkg`, as obtained from the `homepage` field in `brew cat `\n\nParameters:\n * pkg - name of the package to query\n * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat \" being run. For example, if `subcommand` is \"cask\", the `brew cask cat ` command will be used.\n\nReturns:\n * The Spoon object", + "examples": [], + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "126", + "name": "openBrewURL", + "notes": [], + "parameters": [ + " * pkg - name of the package to query", + " * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat \" being run. For example, if `subcommand` is \"cask\", the `brew cask cat ` command will be used." + ], + "returns": [ + " * The Spoon object" + ], + "signature": "BrewInfo:openBrewURL(pkg, subcommand)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "BrewInfo:openBrewURLCurSel(subcommand)", + "desc": "Opens the homepage for the currently-selected package, as obtained from the `homepage` field in `brew cat `", + "doc": "Opens the homepage for the currently-selected package, as obtained from the `homepage` field in `brew cat `\n\nParameters:\n * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat \" being run. For example, if `subcommand` is \"cask\", the `brew cask cat ` command will be used.\n\nReturns:\n * The Spoon object", + "examples": [], + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "153", + "name": "openBrewURLCurSel", + "notes": [], + "parameters": [ + " * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in \"brew cat \" being run. For example, if `subcommand` is \"cask\", the `brew cask cat ` command will be used." + ], + "returns": [ + " * The Spoon object" + ], + "signature": "BrewInfo:openBrewURLCurSel(subcommand)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "BrewInfo.select_text_if_needed", + "desc": "If `true`, and no text is currently selected in the terminal, issue a double-click to select the text below the cursor, and use that as the input to `brew info`. See also `BrewInfo.select_text_modifiers`. Defaults to `true`.", + "doc": "If `true`, and no text is currently selected in the terminal, issue a double-click to select the text below the cursor, and use that as the input to `brew info`. See also `BrewInfo.select_text_modifiers`. Defaults to `true`.", + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "44", + "name": "select_text_if_needed", + "signature": "BrewInfo.select_text_if_needed", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "BrewInfo.select_text_modifiers", + "desc": "Table containing the modifiers to be used together with a double-click when `BrewInfo.select_text_if_needed` is true. Defaults to `{cmd = true, shift = true}` to issue a Cmd-Shift-double-click, which will select a continuous non-space string in Terminal and iTerm2.", + "doc": "Table containing the modifiers to be used together with a double-click when `BrewInfo.select_text_if_needed` is true. Defaults to `{cmd = true, shift = true}` to issue a Cmd-Shift-double-click, which will select a continuous non-space string in Terminal and iTerm2.", + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "49", + "name": "select_text_modifiers", + "signature": "BrewInfo.select_text_modifiers", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "BrewInfo:showBrewInfo(pkg, subcommand)", + "desc": "Displays an alert with the output of `brew info `", + "doc": "Displays an alert with the output of `brew info `\n\nParameters:\n * pkg - name of the package to query\n * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info \" being run. For example, if `subcommand` is \"cask\", the `brew cask info ` command will be used.\n\nReturns:\n * The Spoon object", + "examples": [], + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "89", + "name": "showBrewInfo", + "notes": [], + "parameters": [ + " * pkg - name of the package to query", + " * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info \" being run. For example, if `subcommand` is \"cask\", the `brew cask info ` command will be used." + ], + "returns": [ + " * The Spoon object" + ], + "signature": "BrewInfo:showBrewInfo(pkg, subcommand)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "BrewInfo:showBrewInfoCurSel(subcommand)", + "desc": "Display `brew info` using the selected text as the package name", + "doc": "Display `brew info` using the selected text as the package name\n\nParameters:\n * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info\" being run. For example, if `subcommand` is \"cask\", the `brew cask info` command will be used.\n\nReturns:\n * The Spoon object", + "examples": [], + "file": "Source/BrewInfo.spoon//init.lua", + "lineno": "113", + "name": "showBrewInfoCurSel", + "notes": [], + "parameters": [ + " * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in \"brew info\" being run. For example, if `subcommand` is \"cask\", the `brew cask info` command will be used." + ], + "returns": [ + " * The Spoon object" + ], + "signature": "BrewInfo:showBrewInfoCurSel(subcommand)", + "stripped_doc": "", + "type": "Method" + } + ], + "name": "BrewInfo", + "stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip)\n\nYou can bind keys to automatically display the output of `brew\ninfo` of the currently-selected package name, or to open its\nhomepage. I use it to quickly explore new packages from the output\nof `brew update`.", + "submodules": [], + "type": "Module" + } +] \ No newline at end of file diff --git a/Spoons/BrewInfo.spoon/init.lua b/Spoons/BrewInfo.spoon/init.lua new file mode 100644 index 0000000..04308be --- /dev/null +++ b/Spoons/BrewInfo.spoon/init.lua @@ -0,0 +1,195 @@ +--- === BrewInfo === +--- +--- Display pop-up with Homebrew Formula info, or open their URL +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/BrewInfo.spoon.zip) +--- +--- You can bind keys to automatically display the output of `brew +--- info` of the currently-selected package name, or to open its +--- homepage. I use it to quickly explore new packages from the output +--- of `brew update`. + +local mod={} +mod.__index = mod + +-- Conformance hack, our Travis linter expects the object to be called "obj" +local obj=mod + +-- Metadata +mod.name = "BrewInfo" +obj.version = "1.1" +mod.author = "Diego Zamboni " +mod.homepage = "https://github.com/Hammerspoon/Spoons" +mod.license = "MIT - https://opensource.org/licenses/MIT" + +--- BrewInfo.brew_path +--- Variable +--- A string specifying the path to the `brew` executable. Defaults to `/usr/local/bin/brew` +mod.brew_path = "/usr/local/bin/brew" + +--- BrewInfo.brew_info_delay_sec +--- Variable +--- An integer specifying how long the alerts generated by BrewInfo will stay onscreen +mod.brew_info_delay_sec = 3 + +--- BrewInfo.brew_info_style +--- Variable +--- A table in conformance with the [hs.alert.defaultStyle](http://www.hammerspoon.org/docs/hs.alert.html#defaultStyle[]) format that specifies the style used by the alerts. Default value: `{ textFont = "Courier New", textSize = 14, radius = 10 }` +mod.brew_info_style = { + textFont = "Courier New", + textSize = 14, + radius = 10 +} + +--- BrewInfo.select_text_if_needed +--- Variable +--- If `true`, and no text is currently selected in the terminal, issue a double-click to select the text below the cursor, and use that as the input to `brew info`. See also `BrewInfo.select_text_modifiers`. Defaults to `true`. +mod.select_text_if_needed = true + +--- BrewInfo.select_text_modifiers +--- Variable +--- Table containing the modifiers to be used together with a double-click when `BrewInfo.select_text_if_needed` is true. Defaults to `{cmd = true, shift = true}` to issue a Cmd-Shift-double-click, which will select a continuous non-space string in Terminal and iTerm2. +mod.select_text_modifiers = {cmd = true, shift = true} + +-- Internal function to issue a double click with given modifiers +function leftDoubleClick(modifiers) + local pos=hs.mouse.absolutePosition() + hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseDown, pos, modifiers) + :setProperty(hs.eventtap.event.properties.mouseEventClickState, 2) + :post() + hs.eventtap.event.newMouseEvent(hs.eventtap.event.types.leftMouseUp, pos, modifiers) + :post() +end + +-- Internal method to get the currently selected text +-- If `select_text_if_needed` is true and no text is selected, issue +-- a double-click to select, then use that +function mod:current_selection() + local elem=hs.uielement.focusedElement() + if elem then + local sel = elem:selectedText() + if (sel == nil or sel == "") and self.select_text_if_needed then + -- Simulate a double click to select the text under the cursor + leftDoubleClick(self.select_text_modifiers) + hs.timer.usleep(20000) + sel = elem:selectedText() + end + return sel + else + return nil + end +end + +-- Internal method to show an alert in the configured style +function mod:show(text) + hs.alert.show(text, self.brew_info_style, self.brew_info_delay_sec) + return self +end + +--- BrewInfo:showBrewInfo(pkg, subcommand) +--- Method +--- Displays an alert with the output of `brew info ` +--- +--- Parameters: +--- * pkg - name of the package to query +--- * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in "brew info " being run. For example, if `subcommand` is "cask", the `brew cask info ` command will be used. +--- +--- Returns: +--- * The Spoon object +function mod:showBrewInfo(pkg, subcommand) + local info = "No package selected" + local st = nil + if pkg and pkg ~= "" then + local cmd=string.format("%s %s info %s", self.brew_path, subcommand or "", pkg) + info, st=hs.execute(cmd) + if st == nil then + info = "No information found about formula '" .. pkg .. "'!" + end + end + self:show(info) + return self +end + +--- BrewInfo:showBrewInfoCurSel(subcommand) +--- Method +--- Display `brew info` using the selected text as the package name +--- +--- Parameters: +--- * subcommand - brew subcommand to use for the `info` command. Defaults to an empty string, which results in "brew info" being run. For example, if `subcommand` is "cask", the `brew cask info` command will be used. +--- +--- Returns: +--- * The Spoon object +function mod:showBrewInfoCurSel(subcommand) + return self:showBrewInfo(self:current_selection(), subcommand) +end + +--- BrewInfo:openBrewURL(pkg, subcommand) +--- Method +--- Opens the homepage for package `pkg`, as obtained from the `homepage` field in `brew cat ` +--- +--- Parameters: +--- * pkg - name of the package to query +--- * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in "brew cat " being run. For example, if `subcommand` is "cask", the `brew cask cat ` command will be used. +--- +--- Returns: +--- * The Spoon object +function mod:openBrewURL(pkg, subcommand) + local msg = "No package selected" + if pkg and pkg ~= "" then + local j, st, t, rc=hs.execute(string.format("%s %s cat %s", self.brew_path, (subcommand or ""), pkg )) + if st ~= nil then + local url=string.match(j, "\n%s*homepage%s+['\"](.-)['\"]%s*\n") + if url and url ~= "" then + hs.urlevent.openURLWithBundle(url, hs.urlevent.getDefaultHandler("http")) + return self + end + end + msg = "An error occurred obtaining information about '" .. pkg .. "'" + end + self:show(msg) + return self +end + +--- BrewInfo:openBrewURLCurSel(subcommand) +--- Method +--- Opens the homepage for the currently-selected package, as obtained from the `homepage` field in `brew cat ` +--- +--- Parameters: +--- * subcommand - brew subcommand to use for the `cat` command. Defaults to an empty string, which results in "brew cat " being run. For example, if `subcommand` is "cask", the `brew cask cat ` command will be used. +--- +--- Returns: +--- * The Spoon object +function mod:openBrewURLCurSel(subcommand) + return self:openBrewURL(self:current_selection(), subcommand) +end + +--- BrewInfo:bindHotkeys(mapping) +--- Method +--- Binds hotkeys for BrewInfo +--- +--- Parameters: +--- * mapping - A table containing hotkey modifier/key details for the following items: +--- * show_brew_info - Show output of `brew info` using the selected text as package name +--- * open_brew_url - Open the homepage of the formula whose name is currently selected +--- * show_brew_cask_info - Show output of `brew cask info` using the selected text as package name +--- * open_brew_cask_url - Open the homepage of the Cask whose name is currently selected +function mod:bindHotkeys(mapping) + local def = { + show_brew_info = function() self:showBrewInfoCurSel() end, + open_brew_url = function() self:openBrewURLCurSel() end, + } + for action, key in pairs(mapping) do + local subcommand_show = action:match("show_brew_(.*)_info") + if subcommand_show and subcommand_show ~= "" then + def[action] = function() self:showBrewInfoCurSel(subcommand_show) end + end + + local subcommand_open = action:match("open_brew_(.*)_url") + if subcommand_open and subcommand_open ~= "" then + def[action] = function() self:openBrewURLCurSel(subcommand_open) end + end + end + hs.spoons.bindHotkeysToSpec(def, mapping) +end + +return mod diff --git a/Spoons/KSheet.spoon/docs.json b/Spoons/KSheet.spoon/docs.json new file mode 100644 index 0000000..ab23f94 --- /dev/null +++ b/Spoons/KSheet.spoon/docs.json @@ -0,0 +1,123 @@ +[ + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [ + { + "def": "KSheet:bindHotkeys(mapping)", + "desc": "Binds hotkeys for KSheet", + "doc": "Binds hotkeys for KSheet\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following items:\n * show - Show the keybinding view\n * hide - Hide the keybinding view\n * toggle - Show if hidden, hide if shown", + "name": "bindHotkeys", + "parameters": [ + " * mapping - A table containing hotkey modifier/key details for the following items:", + " * show - Show the keybinding view", + " * hide - Hide the keybinding view", + " * toggle - Show if hidden, hide if shown" + ], + "signature": "KSheet:bindHotkeys(mapping)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "KSheet:hide()", + "desc": "Hide the cheatsheet view.", + "doc": "Hide the cheatsheet view.", + "name": "hide", + "signature": "KSheet:hide()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "KSheet:init()", + "desc": "Initialize the spoon", + "doc": "Initialize the spoon", + "name": "init", + "signature": "KSheet:init()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "KSheet:show()", + "desc": "Show current application's keybindings in a view.", + "doc": "Show current application's keybindings in a view.", + "name": "show", + "signature": "KSheet:show()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "KSheet:toggle()", + "desc": "Alternatively show/hide the cheatsheet view.", + "doc": "Alternatively show/hide the cheatsheet view.", + "name": "toggle", + "signature": "KSheet:toggle()", + "stripped_doc": "", + "type": "Method" + } + ], + "Variable": [], + "desc": "Keybindings cheatsheet for current application", + "doc": "Keybindings cheatsheet for current application\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip)", + "items": [ + { + "def": "KSheet:bindHotkeys(mapping)", + "desc": "Binds hotkeys for KSheet", + "doc": "Binds hotkeys for KSheet\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following items:\n * show - Show the keybinding view\n * hide - Hide the keybinding view\n * toggle - Show if hidden, hide if shown", + "name": "bindHotkeys", + "parameters": [ + " * mapping - A table containing hotkey modifier/key details for the following items:", + " * show - Show the keybinding view", + " * hide - Hide the keybinding view", + " * toggle - Show if hidden, hide if shown" + ], + "signature": "KSheet:bindHotkeys(mapping)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "KSheet:hide()", + "desc": "Hide the cheatsheet view.", + "doc": "Hide the cheatsheet view.", + "name": "hide", + "signature": "KSheet:hide()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "KSheet:init()", + "desc": "Initialize the spoon", + "doc": "Initialize the spoon", + "name": "init", + "signature": "KSheet:init()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "KSheet:show()", + "desc": "Show current application's keybindings in a view.", + "doc": "Show current application's keybindings in a view.", + "name": "show", + "signature": "KSheet:show()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "KSheet:toggle()", + "desc": "Alternatively show/hide the cheatsheet view.", + "doc": "Alternatively show/hide the cheatsheet view.", + "name": "toggle", + "signature": "KSheet:toggle()", + "stripped_doc": "", + "type": "Method" + } + ], + "name": "KSheet", + "stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip)", + "submodules": [], + "type": "Module" + } +] \ No newline at end of file diff --git a/Spoons/KSheet.spoon/init.lua b/Spoons/KSheet.spoon/init.lua new file mode 100644 index 0000000..065dc69 --- /dev/null +++ b/Spoons/KSheet.spoon/init.lua @@ -0,0 +1,242 @@ +--- === KSheet === +--- +--- Keybindings cheatsheet for current application +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/KSheet.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "KSheet" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +-- Workaround for "Dictation" menuitem +hs.application.menuGlyphs[148]="fn fn" + +obj.commandEnum = { + cmd = '⌘', + shift = '⇧', + alt = '⌥', + ctrl = '⌃', +} + +--- KSheet:init() +--- Method +--- Initialize the spoon +function obj:init() + self.sheetView = hs.webview.new({x=0, y=0, w=0, h=0}) + self.sheetView:windowTitle("CheatSheets") + self.sheetView:windowStyle("utility") + self.sheetView:allowGestures(true) + self.sheetView:allowNewWindows(false) + self.sheetView:level(hs.drawing.windowLevels.tornOffMenu) + local cscreen = hs.screen.mainScreen() + local cres = cscreen:fullFrame() + self.sheetView:frame({ + x = cres.x+cres.w*0.15/2, + y = cres.y+cres.h*0.25/2, + w = cres.w*0.85, + h = cres.h*0.75 + }) +end + +local function processMenuItems(menustru) + local menu = "" + for pos,val in pairs(menustru) do + if type(val) == "table" then + -- TODO: Remove menubar items with no shortcuts in them + if val.AXRole == "AXMenuBarItem" and type(val.AXChildren) == "table" then + menu = menu .. "
    " + menu = menu .. "
  • " .. val.AXTitle .. "
  • " + menu = menu .. processMenuItems(val.AXChildren[1]) + menu = menu .. "
" + elseif val.AXRole == "AXMenuItem" and not val.AXChildren then + if not (val.AXMenuItemCmdChar == '' and val.AXMenuItemCmdGlyph == '') then + local CmdModifiers = '' + for key, value in pairs(val.AXMenuItemCmdModifiers) do + CmdModifiers = CmdModifiers .. obj.commandEnum[value] + end + local CmdChar = val.AXMenuItemCmdChar + local CmdGlyph = hs.application.menuGlyphs[val.AXMenuItemCmdGlyph] or '' + local CmdKeys = CmdChar .. CmdGlyph + menu = menu .. "
  • " .. CmdModifiers .. " " .. CmdKeys .. "
    " .. " " .. val.AXTitle .. "
  • " + end + elseif val.AXRole == "AXMenuItem" and type(val.AXChildren) == "table" then + menu = menu .. processMenuItems(val.AXChildren[1]) + end + end + end + return menu +end + +local function generateHtml(application) + local app_title = application:title() + local menuitems_tree = application:getMenuItems() + local allmenuitems = processMenuItems(menuitems_tree) + + local html = [[ + + + + + + +
    +
    ]] .. app_title .. [[
    +
    +
    +
    ]] .. allmenuitems .. [[
    +
    + + + + + + + ]] + + return html +end + +--- KSheet:show() +--- Method +--- Show current application's keybindings in a view. +function obj:show() + local capp = hs.application.frontmostApplication() + local webcontent = generateHtml(capp) + self.sheetView:html(webcontent) + self.sheetView:show() +end + +--- KSheet:hide() +--- Method +--- Hide the cheatsheet view. +function obj:hide() + self.sheetView:hide() +end + +--- KSheet:toggle() +--- Method +--- Alternatively show/hide the cheatsheet view. +function obj:toggle() + if self.sheetView and self.sheetView:hswindow() and self.sheetView:hswindow():isVisible() then + self:hide() + else + self:show() + end +end + +--- KSheet:bindHotkeys(mapping) +--- Method +--- Binds hotkeys for KSheet +--- +--- Parameters: +--- * mapping - A table containing hotkey modifier/key details for the following items: +--- * show - Show the keybinding view +--- * hide - Hide the keybinding view +--- * toggle - Show if hidden, hide if shown +function obj:bindHotkeys(mapping) + local actions = { + toggle = hs.fnutils.partial(self.toggle, self), + show = hs.fnutils.partial(self.show, self), + hide = hs.fnutils.partial(self.hide, self) + } + hs.spoons.bindHotkeysToSpec(actions, mapping) +end + +return obj diff --git a/Spoons/Seal.spoon/docs.json b/Spoons/Seal.spoon/docs.json new file mode 100644 index 0000000..54570f5 --- /dev/null +++ b/Spoons/Seal.spoon/docs.json @@ -0,0 +1,1125 @@ +[ + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [ + { + "def": "Seal:bindHotkeys(mapping)", + "desc": "Binds hotkeys for Seal", + "doc": "Binds hotkeys for Seal\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following (optional) items:\n * show - This will cause Seal's UI to be shown\n * toggle - This will cause Seal's UI to be shown or hidden depending on its current state\n\nReturns:\n * The Seal object", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "157", + "name": "bindHotkeys", + "notes": [], + "parameters": [ + " * mapping - A table containing hotkey modifier/key details for the following (optional) items:\n * show - This will cause Seal's UI to be shown\n * toggle - This will cause Seal's UI to be shown or hidden depending on its current state" + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:bindHotkeys(mapping)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:loadPluginFromFile(plugin_name, file)", + "desc": "Loads a plugin from a given file", + "doc": "Loads a plugin from a given file\n\nParameters:\n * plugin_name - the name of the plugin, without \"seal_\" at the beginning or \".lua\" at the end\n * file - the file where the plugin code is stored.\n\nReturns:\n * The Seal object if the plugin was successfully loaded, `nil` otherwise\n\nNotes:\n * You should normally use `Seal:loadPlugins()`. This method allows you to load plugins\n from non-standard locations and is mostly a development interface.\n * Some plugins may immediately begin doing background work (e.g. Spotlight searches)", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "94", + "name": "loadPluginFromFile", + "notes": [ + " * You should normally use `Seal:loadPlugins()`. This method allows you to load plugins", + " from non-standard locations and is mostly a development interface.", + " * Some plugins may immediately begin doing background work (e.g. Spotlight searches)" + ], + "parameters": [ + " * plugin_name - the name of the plugin, without \"seal_\" at the beginning or \".lua\" at the end", + " * file - the file where the plugin code is stored." + ], + "returns": [ + " * The Seal object if the plugin was successfully loaded, `nil` otherwise" + ], + "signature": "Seal:loadPluginFromFile(plugin_name, file)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:loadPlugins(plugins)", + "desc": "Loads a list of Seal plugins", + "doc": "Loads a list of Seal plugins\n\nParameters:\n * plugins - A list containing the names of plugins to load\n\nReturns:\n * The Seal object\n\nNotes:\n * The plugins live inside the Seal.spoon directory\n * The plugin names in the list, should not have `seal_` at the start, or `.lua` at the end\n * Some plugins may immediately begin doing background work (e.g. Spotlight searches)", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "122", + "name": "loadPlugins", + "notes": [ + " * The plugins live inside the Seal.spoon directory", + " * The plugin names in the list, should not have `seal_` at the start, or `.lua` at the end", + " * Some plugins may immediately begin doing background work (e.g. Spotlight searches)" + ], + "parameters": [ + " * plugins - A list containing the names of plugins to load" + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:loadPlugins(plugins)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:refreshAllCommands()", + "desc": "Refresh the list of commands provided by all the currently loaded plugins.", + "doc": "Refresh the list of commands provided by all the currently loaded plugins.\n\nParameters:\n * None\n\nReturns:\n * The Seal object\n\nNotes:\n * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands.", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "75", + "name": "refreshAllCommands", + "notes": [ + " * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands." + ], + "parameters": [ + " * None" + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:refreshAllCommands()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:refreshCommandsForPlugin(plugin_name)", + "desc": "Refresh the list of commands provided by the given plugin.", + "doc": "Refresh the list of commands provided by the given plugin.\n\nParameters:\n * plugin_name - the name of the plugin. Should be the name as passed to `loadPlugins()` or `loadPluginFromFile`.\n\nReturns:\n * The Seal object\n\nNotes:\n * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands.", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "50", + "name": "refreshCommandsForPlugin", + "notes": [ + " * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands." + ], + "parameters": [ + " * plugin_name - the name of the plugin. Should be the name as passed to `loadPlugins()` or `loadPluginFromFile`." + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:refreshCommandsForPlugin(plugin_name)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:show(query)", + "desc": "Shows the Seal UI", + "doc": "Shows the Seal UI\n\nParameters:\n * query - An optional string to pre-populate the query box with\n\nReturns:\n * None\n\nNotes:\n * This may be useful if you wish to show Seal in response to something other than its hotkey", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "234", + "name": "show", + "notes": [ + " * This may be useful if you wish to show Seal in response to something other than its hotkey" + ], + "parameters": [ + " * query - An optional string to pre-populate the query box with" + ], + "returns": [ + " * None" + ], + "signature": "Seal:show(query)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:start()", + "desc": "Starts Seal", + "doc": "Starts Seal\n\nParameters:\n * None\n\nReturns:\n * The Seal object", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "190", + "name": "start", + "notes": [], + "parameters": [ + " * None" + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:start()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:stop()", + "desc": "Stops Seal", + "doc": "Stops Seal\n\nParameters:\n * None\n\nReturns:\n * The Seal object\n\nNotes:\n * Some Seal plugins will continue performing background work even after this call (e.g. Spotlight searches)", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "210", + "name": "stop", + "notes": [ + " * Some Seal plugins will continue performing background work even after this call (e.g. Spotlight searches)" + ], + "parameters": [ + " * None" + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:stop()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:toggle(query)", + "desc": "Shows or hides the Seal UI", + "doc": "Shows or hides the Seal UI\n\nParameters:\n * query - An optional string to pre-populate the query box with\n\nReturns:\n * None", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "252", + "name": "toggle", + "notes": [], + "parameters": [ + " * query - An optional string to pre-populate the query box with" + ], + "returns": [ + " * None" + ], + "signature": "Seal:toggle(query)", + "stripped_doc": "", + "type": "Method" + } + ], + "Variable": [ + { + "def": "Seal.plugin_search_paths", + "desc": "List of directories where Seal will look for plugins. Defaults to `~/.hammerspoon/seal_plugins/` and the Seal Spoon directory.", + "doc": "List of directories where Seal will look for plugins. Defaults to `~/.hammerspoon/seal_plugins/` and the Seal Spoon directory.", + "file": "Source/Seal.spoon//init.lua", + "lineno": "45", + "name": "plugin_search_paths", + "signature": "Seal.plugin_search_paths", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "Seal.queryChangedTimerDuration", + "desc": "Time between the last keystroke and the start of the recalculation of the choices to display, in seconds.", + "doc": "Time between the last keystroke and the start of the recalculation of the choices to display, in seconds.\n\nNotes:\n * Defaults to 0.02s (20ms).", + "file": "Source/Seal.spoon//init.lua", + "lineno": "37", + "name": "queryChangedTimerDuration", + "notes": [ + " * Defaults to 0.02s (20ms)." + ], + "signature": "Seal.queryChangedTimerDuration", + "stripped_doc": "", + "type": "Variable" + } + ], + "desc": "Pluggable launch bar", + "doc": "Pluggable launch bar\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Seal.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Seal.spoon.zip)\n\nSeal includes a number of plugins, which you can choose to load (see `:loadPlugins()` below):\n * apps : Launch applications by name\n * calc : Simple calculator\n * rot13 : Apply ROT13 substitution cipher\n * safari_bookmarks : Open Safari bookmarks (this is broken since at least High Sierra)\n * screencapture : Lets you take screenshots in various ways\n * urlformats : User defined URL formats to open\n * useractions : User defined custom actions\n * vpn : Connect and disconnect VPNs (currently supports Viscosity and macOS system preferences)A", + "items": [ + { + "def": "Seal:bindHotkeys(mapping)", + "desc": "Binds hotkeys for Seal", + "doc": "Binds hotkeys for Seal\n\nParameters:\n * mapping - A table containing hotkey modifier/key details for the following (optional) items:\n * show - This will cause Seal's UI to be shown\n * toggle - This will cause Seal's UI to be shown or hidden depending on its current state\n\nReturns:\n * The Seal object", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "157", + "name": "bindHotkeys", + "notes": [], + "parameters": [ + " * mapping - A table containing hotkey modifier/key details for the following (optional) items:\n * show - This will cause Seal's UI to be shown\n * toggle - This will cause Seal's UI to be shown or hidden depending on its current state" + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:bindHotkeys(mapping)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:loadPluginFromFile(plugin_name, file)", + "desc": "Loads a plugin from a given file", + "doc": "Loads a plugin from a given file\n\nParameters:\n * plugin_name - the name of the plugin, without \"seal_\" at the beginning or \".lua\" at the end\n * file - the file where the plugin code is stored.\n\nReturns:\n * The Seal object if the plugin was successfully loaded, `nil` otherwise\n\nNotes:\n * You should normally use `Seal:loadPlugins()`. This method allows you to load plugins\n from non-standard locations and is mostly a development interface.\n * Some plugins may immediately begin doing background work (e.g. Spotlight searches)", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "94", + "name": "loadPluginFromFile", + "notes": [ + " * You should normally use `Seal:loadPlugins()`. This method allows you to load plugins", + " from non-standard locations and is mostly a development interface.", + " * Some plugins may immediately begin doing background work (e.g. Spotlight searches)" + ], + "parameters": [ + " * plugin_name - the name of the plugin, without \"seal_\" at the beginning or \".lua\" at the end", + " * file - the file where the plugin code is stored." + ], + "returns": [ + " * The Seal object if the plugin was successfully loaded, `nil` otherwise" + ], + "signature": "Seal:loadPluginFromFile(plugin_name, file)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:loadPlugins(plugins)", + "desc": "Loads a list of Seal plugins", + "doc": "Loads a list of Seal plugins\n\nParameters:\n * plugins - A list containing the names of plugins to load\n\nReturns:\n * The Seal object\n\nNotes:\n * The plugins live inside the Seal.spoon directory\n * The plugin names in the list, should not have `seal_` at the start, or `.lua` at the end\n * Some plugins may immediately begin doing background work (e.g. Spotlight searches)", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "122", + "name": "loadPlugins", + "notes": [ + " * The plugins live inside the Seal.spoon directory", + " * The plugin names in the list, should not have `seal_` at the start, or `.lua` at the end", + " * Some plugins may immediately begin doing background work (e.g. Spotlight searches)" + ], + "parameters": [ + " * plugins - A list containing the names of plugins to load" + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:loadPlugins(plugins)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal.plugin_search_paths", + "desc": "List of directories where Seal will look for plugins. Defaults to `~/.hammerspoon/seal_plugins/` and the Seal Spoon directory.", + "doc": "List of directories where Seal will look for plugins. Defaults to `~/.hammerspoon/seal_plugins/` and the Seal Spoon directory.", + "file": "Source/Seal.spoon//init.lua", + "lineno": "45", + "name": "plugin_search_paths", + "signature": "Seal.plugin_search_paths", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "Seal.queryChangedTimerDuration", + "desc": "Time between the last keystroke and the start of the recalculation of the choices to display, in seconds.", + "doc": "Time between the last keystroke and the start of the recalculation of the choices to display, in seconds.\n\nNotes:\n * Defaults to 0.02s (20ms).", + "file": "Source/Seal.spoon//init.lua", + "lineno": "37", + "name": "queryChangedTimerDuration", + "notes": [ + " * Defaults to 0.02s (20ms)." + ], + "signature": "Seal.queryChangedTimerDuration", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "Seal:refreshAllCommands()", + "desc": "Refresh the list of commands provided by all the currently loaded plugins.", + "doc": "Refresh the list of commands provided by all the currently loaded plugins.\n\nParameters:\n * None\n\nReturns:\n * The Seal object\n\nNotes:\n * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands.", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "75", + "name": "refreshAllCommands", + "notes": [ + " * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands." + ], + "parameters": [ + " * None" + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:refreshAllCommands()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:refreshCommandsForPlugin(plugin_name)", + "desc": "Refresh the list of commands provided by the given plugin.", + "doc": "Refresh the list of commands provided by the given plugin.\n\nParameters:\n * plugin_name - the name of the plugin. Should be the name as passed to `loadPlugins()` or `loadPluginFromFile`.\n\nReturns:\n * The Seal object\n\nNotes:\n * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands.", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "50", + "name": "refreshCommandsForPlugin", + "notes": [ + " * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands." + ], + "parameters": [ + " * plugin_name - the name of the plugin. Should be the name as passed to `loadPlugins()` or `loadPluginFromFile`." + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:refreshCommandsForPlugin(plugin_name)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:show(query)", + "desc": "Shows the Seal UI", + "doc": "Shows the Seal UI\n\nParameters:\n * query - An optional string to pre-populate the query box with\n\nReturns:\n * None\n\nNotes:\n * This may be useful if you wish to show Seal in response to something other than its hotkey", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "234", + "name": "show", + "notes": [ + " * This may be useful if you wish to show Seal in response to something other than its hotkey" + ], + "parameters": [ + " * query - An optional string to pre-populate the query box with" + ], + "returns": [ + " * None" + ], + "signature": "Seal:show(query)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:start()", + "desc": "Starts Seal", + "doc": "Starts Seal\n\nParameters:\n * None\n\nReturns:\n * The Seal object", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "190", + "name": "start", + "notes": [], + "parameters": [ + " * None" + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:start()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:stop()", + "desc": "Stops Seal", + "doc": "Stops Seal\n\nParameters:\n * None\n\nReturns:\n * The Seal object\n\nNotes:\n * Some Seal plugins will continue performing background work even after this call (e.g. Spotlight searches)", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "210", + "name": "stop", + "notes": [ + " * Some Seal plugins will continue performing background work even after this call (e.g. Spotlight searches)" + ], + "parameters": [ + " * None" + ], + "returns": [ + " * The Seal object" + ], + "signature": "Seal:stop()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal:toggle(query)", + "desc": "Shows or hides the Seal UI", + "doc": "Shows or hides the Seal UI\n\nParameters:\n * query - An optional string to pre-populate the query box with\n\nReturns:\n * None", + "examples": [], + "file": "Source/Seal.spoon//init.lua", + "lineno": "252", + "name": "toggle", + "notes": [], + "parameters": [ + " * query - An optional string to pre-populate the query box with" + ], + "returns": [ + " * None" + ], + "signature": "Seal:toggle(query)", + "stripped_doc": "", + "type": "Method" + } + ], + "name": "Seal", + "stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Seal.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Seal.spoon.zip)\n\nSeal includes a number of plugins, which you can choose to load (see `:loadPlugins()` below):\n * apps : Launch applications by name\n * calc : Simple calculator\n * rot13 : Apply ROT13 substitution cipher\n * safari_bookmarks : Open Safari bookmarks (this is broken since at least High Sierra)\n * screencapture : Lets you take screenshots in various ways\n * urlformats : User defined URL formats to open\n * useractions : User defined custom actions\n * vpn : Connect and disconnect VPNs (currently supports Viscosity and macOS system preferences)A", + "submodules": [ + "plugins" + ], + "type": "Module" + }, + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [], + "Variable": [], + "desc": "Various APIs for Seal plugins", + "doc": "Various APIs for Seal plugins", + "items": [], + "name": "Seal.plugins", + "stripped_doc": "", + "submodules": [ + "apps", + "filesearch", + "pasteboard", + "safari_bookmarks", + "screencapture", + "urlformats", + "useractions" + ], + "type": "Module" + }, + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [ + { + "def": "Seal.plugins.apps:restart()", + "desc": "Restarts the Spotlight app searcher", + "doc": "Restarts the Spotlight app searcher\n\nParameters:\n * None\n\nReturns:\n * None", + "examples": [], + "file": "Source/Seal.spoon//seal_apps.lua", + "lineno": "105", + "name": "restart", + "notes": [], + "parameters": [ + " * None" + ], + "returns": [ + " * None" + ], + "signature": "Seal.plugins.apps:restart()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal.plugins.apps:start()", + "desc": "Starts the Spotlight app searcher", + "doc": "Starts the Spotlight app searcher\n\nParameters:\n * None\n\nReturns:\n * None\n\nNotes:\n * This is called automatically when the plugin is loaded", + "examples": [], + "file": "Source/Seal.spoon//seal_apps.lua", + "lineno": "70", + "name": "start", + "notes": [ + " * This is called automatically when the plugin is loaded" + ], + "parameters": [ + " * None" + ], + "returns": [ + " * None" + ], + "signature": "Seal.plugins.apps:start()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal.plugins.apps:stop()", + "desc": "Stops the Spotlight app searcher", + "doc": "Stops the Spotlight app searcher\n\nParameters:\n * None\n\nReturns:\n * None", + "examples": [], + "file": "Source/Seal.spoon//seal_apps.lua", + "lineno": "90", + "name": "stop", + "notes": [], + "parameters": [ + " * None" + ], + "returns": [ + " * None" + ], + "signature": "Seal.plugins.apps:stop()", + "stripped_doc": "", + "type": "Method" + } + ], + "Variable": [ + { + "def": "Seal.plugins.apps.appSearchPaths", + "desc": "Table containing the paths to search for launchable items", + "doc": "Table containing the paths to search for launchable items\n\nNotes:\n * If you change this, you will need to call `spoon.Seal.plugins.apps:restart()` to force Spotlight to search for new items.", + "file": "Source/Seal.spoon//seal_apps.lua", + "lineno": "9", + "name": "appSearchPaths", + "notes": [ + " * If you change this, you will need to call `spoon.Seal.plugins.apps:restart()` to force Spotlight to search for new items." + ], + "signature": "Seal.plugins.apps.appSearchPaths", + "stripped_doc": "", + "type": "Variable" + } + ], + "desc": "A plugin to add launchable apps/scripts, making Seal act as a launch bar", + "doc": "A plugin to add launchable apps/scripts, making Seal act as a launch bar", + "items": [ + { + "def": "Seal.plugins.apps.appSearchPaths", + "desc": "Table containing the paths to search for launchable items", + "doc": "Table containing the paths to search for launchable items\n\nNotes:\n * If you change this, you will need to call `spoon.Seal.plugins.apps:restart()` to force Spotlight to search for new items.", + "file": "Source/Seal.spoon//seal_apps.lua", + "lineno": "9", + "name": "appSearchPaths", + "notes": [ + " * If you change this, you will need to call `spoon.Seal.plugins.apps:restart()` to force Spotlight to search for new items." + ], + "signature": "Seal.plugins.apps.appSearchPaths", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "Seal.plugins.apps:restart()", + "desc": "Restarts the Spotlight app searcher", + "doc": "Restarts the Spotlight app searcher\n\nParameters:\n * None\n\nReturns:\n * None", + "examples": [], + "file": "Source/Seal.spoon//seal_apps.lua", + "lineno": "105", + "name": "restart", + "notes": [], + "parameters": [ + " * None" + ], + "returns": [ + " * None" + ], + "signature": "Seal.plugins.apps:restart()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal.plugins.apps:start()", + "desc": "Starts the Spotlight app searcher", + "doc": "Starts the Spotlight app searcher\n\nParameters:\n * None\n\nReturns:\n * None\n\nNotes:\n * This is called automatically when the plugin is loaded", + "examples": [], + "file": "Source/Seal.spoon//seal_apps.lua", + "lineno": "70", + "name": "start", + "notes": [ + " * This is called automatically when the plugin is loaded" + ], + "parameters": [ + " * None" + ], + "returns": [ + " * None" + ], + "signature": "Seal.plugins.apps:start()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "Seal.plugins.apps:stop()", + "desc": "Stops the Spotlight app searcher", + "doc": "Stops the Spotlight app searcher\n\nParameters:\n * None\n\nReturns:\n * None", + "examples": [], + "file": "Source/Seal.spoon//seal_apps.lua", + "lineno": "90", + "name": "stop", + "notes": [], + "parameters": [ + " * None" + ], + "returns": [ + " * None" + ], + "signature": "Seal.plugins.apps:stop()", + "stripped_doc": "", + "type": "Method" + } + ], + "name": "Seal.plugins.apps", + "stripped_doc": "", + "submodules": [], + "type": "Module" + }, + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [], + "Variable": [ + { + "def": "Seal.plugins.filesearch.displayResultsTimeout", + "desc": "Maximum time to wait before displaying the results", + "doc": "Maximum time to wait before displaying the results\nDefaults to 0.2s (200ms).\n\nNotes:\n * higher value might give you more results but will give a less snappy experience", + "file": "Source/Seal.spoon//seal_filesearch.lua", + "lineno": "21", + "name": "displayResultsTimeout", + "notes": [ + " * higher value might give you more results but will give a less snappy experience" + ], + "signature": "Seal.plugins.filesearch.displayResultsTimeout", + "stripped_doc": "Defaults to 0.2s (200ms).", + "type": "Variable" + }, + { + "def": "Seal.plugins.filesearch.fileSearchPaths", + "desc": "Table containing the paths to search for files", + "doc": "Table containing the paths to search for files\n\nNotes:\n * You will need to authorize hammerspoon to access the folders in this list in order for this to work.", + "file": "Source/Seal.spoon//seal_filesearch.lua", + "lineno": "8", + "name": "fileSearchPaths", + "notes": [ + " * You will need to authorize hammerspoon to access the folders in this list in order for this to work." + ], + "signature": "Seal.plugins.filesearch.fileSearchPaths", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "Seal.plugins.filesearch.maxResults", + "desc": "Maximum number of results to display", + "doc": "Maximum number of results to display", + "file": "Source/Seal.spoon//seal_filesearch.lua", + "lineno": "16", + "name": "maxResults", + "signature": "Seal.plugins.filesearch.maxResults", + "stripped_doc": "", + "type": "Variable" + } + ], + "desc": "A plugin to add file search capabilities, making Seal act as a spotlight file search", + "doc": "A plugin to add file search capabilities, making Seal act as a spotlight file search", + "items": [ + { + "def": "Seal.plugins.filesearch.displayResultsTimeout", + "desc": "Maximum time to wait before displaying the results", + "doc": "Maximum time to wait before displaying the results\nDefaults to 0.2s (200ms).\n\nNotes:\n * higher value might give you more results but will give a less snappy experience", + "file": "Source/Seal.spoon//seal_filesearch.lua", + "lineno": "21", + "name": "displayResultsTimeout", + "notes": [ + " * higher value might give you more results but will give a less snappy experience" + ], + "signature": "Seal.plugins.filesearch.displayResultsTimeout", + "stripped_doc": "Defaults to 0.2s (200ms).", + "type": "Variable" + }, + { + "def": "Seal.plugins.filesearch.fileSearchPaths", + "desc": "Table containing the paths to search for files", + "doc": "Table containing the paths to search for files\n\nNotes:\n * You will need to authorize hammerspoon to access the folders in this list in order for this to work.", + "file": "Source/Seal.spoon//seal_filesearch.lua", + "lineno": "8", + "name": "fileSearchPaths", + "notes": [ + " * You will need to authorize hammerspoon to access the folders in this list in order for this to work." + ], + "signature": "Seal.plugins.filesearch.fileSearchPaths", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "Seal.plugins.filesearch.maxResults", + "desc": "Maximum number of results to display", + "doc": "Maximum number of results to display", + "file": "Source/Seal.spoon//seal_filesearch.lua", + "lineno": "16", + "name": "maxResults", + "signature": "Seal.plugins.filesearch.maxResults", + "stripped_doc": "", + "type": "Variable" + } + ], + "name": "Seal.plugins.filesearch", + "stripped_doc": "", + "submodules": [], + "type": "Module" + }, + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [], + "Variable": [ + { + "def": "Seal.plugins.pasteboard.historySize", + "desc": "", + "doc": "\nThe number of history items to keep. Defaults to 50", + "file": "Source/Seal.spoon//seal_pasteboard.lua", + "lineno": "12", + "name": "historySize", + "signature": "Seal.plugins.pasteboard.historySize", + "stripped_doc": "The number of history items to keep. Defaults to 50", + "type": "Variable" + }, + { + "def": "Seal.plugins.pasteboard.saveHistory", + "desc": "", + "doc": "\nA boolean, true if Seal should automatically load/save clipboard history. Defaults to true", + "file": "Source/Seal.spoon//seal_pasteboard.lua", + "lineno": "18", + "name": "saveHistory", + "signature": "Seal.plugins.pasteboard.saveHistory", + "stripped_doc": "A boolean, true if Seal should automatically load/save clipboard history. Defaults to true", + "type": "Variable" + }, + { + "def": "Seal.plugins.pasteboard.skipUTIs", + "desc": "", + "doc": "\nAn array of UTIs to skip when saving to the history. Defaults to:\n```\n{\n \"de.petermaurer.TransientPasteboardType\",\n \"com.typeit4me.clipping\",\n \"Pasteboard generator type\",\n \"com.agilebits.onepassword\",\n \"org.nspasteboard.TransientType\",\n \"org.nspasteboard.ConcealedType\",\n \"org.nspasteboard.AutoGeneratedType\"\n}\n```", + "file": "Source/Seal.spoon//seal_pasteboard.lua", + "lineno": "24", + "name": "skipUTIs", + "signature": "Seal.plugins.pasteboard.skipUTIs", + "stripped_doc": "An array of UTIs to skip when saving to the history. Defaults to:\n```\n{\n \"de.petermaurer.TransientPasteboardType\",\n \"com.typeit4me.clipping\",\n \"Pasteboard generator type\",\n \"com.agilebits.onepassword\",\n \"org.nspasteboard.TransientType\",\n \"org.nspasteboard.ConcealedType\",\n \"org.nspasteboard.AutoGeneratedType\"\n}\n```", + "type": "Variable" + } + ], + "desc": "Visual, searchable pasteboard (ie clipboard) history", + "doc": "Visual, searchable pasteboard (ie clipboard) history", + "items": [ + { + "def": "Seal.plugins.pasteboard.historySize", + "desc": "", + "doc": "\nThe number of history items to keep. Defaults to 50", + "file": "Source/Seal.spoon//seal_pasteboard.lua", + "lineno": "12", + "name": "historySize", + "signature": "Seal.plugins.pasteboard.historySize", + "stripped_doc": "The number of history items to keep. Defaults to 50", + "type": "Variable" + }, + { + "def": "Seal.plugins.pasteboard.saveHistory", + "desc": "", + "doc": "\nA boolean, true if Seal should automatically load/save clipboard history. Defaults to true", + "file": "Source/Seal.spoon//seal_pasteboard.lua", + "lineno": "18", + "name": "saveHistory", + "signature": "Seal.plugins.pasteboard.saveHistory", + "stripped_doc": "A boolean, true if Seal should automatically load/save clipboard history. Defaults to true", + "type": "Variable" + }, + { + "def": "Seal.plugins.pasteboard.skipUTIs", + "desc": "", + "doc": "\nAn array of UTIs to skip when saving to the history. Defaults to:\n```\n{\n \"de.petermaurer.TransientPasteboardType\",\n \"com.typeit4me.clipping\",\n \"Pasteboard generator type\",\n \"com.agilebits.onepassword\",\n \"org.nspasteboard.TransientType\",\n \"org.nspasteboard.ConcealedType\",\n \"org.nspasteboard.AutoGeneratedType\"\n}\n```", + "file": "Source/Seal.spoon//seal_pasteboard.lua", + "lineno": "24", + "name": "skipUTIs", + "signature": "Seal.plugins.pasteboard.skipUTIs", + "stripped_doc": "An array of UTIs to skip when saving to the history. Defaults to:\n```\n{\n \"de.petermaurer.TransientPasteboardType\",\n \"com.typeit4me.clipping\",\n \"Pasteboard generator type\",\n \"com.agilebits.onepassword\",\n \"org.nspasteboard.TransientType\",\n \"org.nspasteboard.ConcealedType\",\n \"org.nspasteboard.AutoGeneratedType\"\n}\n```", + "type": "Variable" + } + ], + "name": "Seal.plugins.pasteboard", + "stripped_doc": "", + "submodules": [], + "type": "Module" + }, + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [], + "Variable": [ + { + "def": "Seal.plugins.safari_bookmarks.always_open_with_safari", + "desc": "If `true` (default), bookmarks are always opened with Safari, otherwise they are opened with the default application using the `/usr/bin/open` command.", + "doc": "If `true` (default), bookmarks are always opened with Safari, otherwise they are opened with the default application using the `/usr/bin/open` command.", + "file": "Source/Seal.spoon//seal_safari_bookmarks.lua", + "lineno": "11", + "name": "always_open_with_safari", + "signature": "Seal.plugins.safari_bookmarks.always_open_with_safari", + "stripped_doc": "", + "type": "Variable" + } + ], + "desc": "", + "doc": "\nNote: Apple has changed the way Safari stores bookmarks and this plugin no longer works on recent macOS releases.", + "items": [ + { + "def": "Seal.plugins.safari_bookmarks.always_open_with_safari", + "desc": "If `true` (default), bookmarks are always opened with Safari, otherwise they are opened with the default application using the `/usr/bin/open` command.", + "doc": "If `true` (default), bookmarks are always opened with Safari, otherwise they are opened with the default application using the `/usr/bin/open` command.", + "file": "Source/Seal.spoon//seal_safari_bookmarks.lua", + "lineno": "11", + "name": "always_open_with_safari", + "signature": "Seal.plugins.safari_bookmarks.always_open_with_safari", + "stripped_doc": "", + "type": "Variable" + } + ], + "name": "Seal.plugins.safari_bookmarks", + "stripped_doc": "Note: Apple has changed the way Safari stores bookmarks and this plugin no longer works on recent macOS releases.", + "submodules": [], + "type": "Module" + }, + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [], + "Variable": [ + { + "def": "Seal.plugins.screencapture.showPostUI", + "desc": "Whether or not to show the screen capture UI in macOS 10.14 or later", + "doc": "Whether or not to show the screen capture UI in macOS 10.14 or later", + "file": "Source/Seal.spoon//seal_screencapture.lua", + "lineno": "8", + "name": "showPostUI", + "signature": "Seal.plugins.screencapture.showPostUI", + "stripped_doc": "", + "type": "Variable" + } + ], + "desc": "A plugin to capture the screen in various ways", + "doc": "A plugin to capture the screen in various ways", + "items": [ + { + "def": "Seal.plugins.screencapture.showPostUI", + "desc": "Whether or not to show the screen capture UI in macOS 10.14 or later", + "doc": "Whether or not to show the screen capture UI in macOS 10.14 or later", + "file": "Source/Seal.spoon//seal_screencapture.lua", + "lineno": "8", + "name": "showPostUI", + "signature": "Seal.plugins.screencapture.showPostUI", + "stripped_doc": "", + "type": "Variable" + } + ], + "name": "Seal.plugins.screencapture", + "stripped_doc": "", + "submodules": [], + "type": "Module" + }, + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [ + { + "def": "Seal.plugins.urlformats:providersTable(aTable)", + "desc": "Gets or sets the current providers table", + "doc": "Gets or sets the current providers table\n\nParameters:\n * aTable - An optional table of providers, which must contain the following keys:\n * name - A string naming the provider, which will be shown in the Seal results\n * url - A string containing the URL to insert the user's query into. This should contain one and only one `%s`\n\nReturns:\n * Either a table of current providers, if no parameter was passed, or nothing if a parmameter was passed.\n\nNotes:\n * An example table might look like:\n```lua\n{\n rhbz = { name = \"Red Hat Bugzilla\", url = \"https://bugzilla.redhat.com/show_bug.cgi?id=%s\", },\n lp = { name = \"Launchpad Bug\", url = \"https://launchpad.net/bugs/%s\", },\n}\n```", + "examples": [], + "file": "Source/Seal.spoon//seal_urlformats.lua", + "lineno": "97", + "name": "providersTable", + "notes": [ + " * An example table might look like:", + "```lua", + "{", + " rhbz = { name = \"Red Hat Bugzilla\", url = \"https://bugzilla.redhat.com/show_bug.cgi?id=%s\", },", + " lp = { name = \"Launchpad Bug\", url = \"https://launchpad.net/bugs/%s\", },", + "}", + "```" + ], + "parameters": [ + " * aTable - An optional table of providers, which must contain the following keys:\n * name - A string naming the provider, which will be shown in the Seal results\n * url - A string containing the URL to insert the user's query into. This should contain one and only one `%s`" + ], + "returns": [ + " * Either a table of current providers, if no parameter was passed, or nothing if a parmameter was passed." + ], + "signature": "Seal.plugins.urlformats:providersTable(aTable)", + "stripped_doc": "", + "type": "Method" + } + ], + "Variable": [], + "desc": "A plugin to quickly open URLs containing a search/query term", + "doc": "A plugin to quickly open URLs containing a search/query term\nThis plugin is invoked with the `uf` keyword and requires some configuration, see `:providersTable()`\n\nThe way this works is by defining a set of providers, each of which contains a URL with a `%s` somewhere insert it.\nWhen the user types `uf` in Seal, followed by some more characters, those characters will be inserted into the string at the point where the `%s` is.\n\nBy way of an example, you could define a provider with a url like `http://bugs.mycorp.com/showBug?id=%s`, and just need to type `uf 123456` in Seal to get a quick shortcut to open the full URL.", + "items": [ + { + "def": "Seal.plugins.urlformats:providersTable(aTable)", + "desc": "Gets or sets the current providers table", + "doc": "Gets or sets the current providers table\n\nParameters:\n * aTable - An optional table of providers, which must contain the following keys:\n * name - A string naming the provider, which will be shown in the Seal results\n * url - A string containing the URL to insert the user's query into. This should contain one and only one `%s`\n\nReturns:\n * Either a table of current providers, if no parameter was passed, or nothing if a parmameter was passed.\n\nNotes:\n * An example table might look like:\n```lua\n{\n rhbz = { name = \"Red Hat Bugzilla\", url = \"https://bugzilla.redhat.com/show_bug.cgi?id=%s\", },\n lp = { name = \"Launchpad Bug\", url = \"https://launchpad.net/bugs/%s\", },\n}\n```", + "examples": [], + "file": "Source/Seal.spoon//seal_urlformats.lua", + "lineno": "97", + "name": "providersTable", + "notes": [ + " * An example table might look like:", + "```lua", + "{", + " rhbz = { name = \"Red Hat Bugzilla\", url = \"https://bugzilla.redhat.com/show_bug.cgi?id=%s\", },", + " lp = { name = \"Launchpad Bug\", url = \"https://launchpad.net/bugs/%s\", },", + "}", + "```" + ], + "parameters": [ + " * aTable - An optional table of providers, which must contain the following keys:\n * name - A string naming the provider, which will be shown in the Seal results\n * url - A string containing the URL to insert the user's query into. This should contain one and only one `%s`" + ], + "returns": [ + " * Either a table of current providers, if no parameter was passed, or nothing if a parmameter was passed." + ], + "signature": "Seal.plugins.urlformats:providersTable(aTable)", + "stripped_doc": "", + "type": "Method" + } + ], + "name": "Seal.plugins.urlformats", + "stripped_doc": "This plugin is invoked with the `uf` keyword and requires some configuration, see `:providersTable()`\n\nThe way this works is by defining a set of providers, each of which contains a URL with a `%s` somewhere insert it.\nWhen the user types `uf` in Seal, followed by some more characters, those characters will be inserted into the string at the point where the `%s` is.\n\nBy way of an example, you could define a provider with a url like `http://bugs.mycorp.com/showBug?id=%s`, and just need to type `uf 123456` in Seal to get a quick shortcut to open the full URL.", + "submodules": [], + "type": "Module" + }, + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [], + "Variable": [ + { + "def": "Seal.plugins.useractions.actions", + "desc": "", + "doc": "\nNotes:\n * A table containing the definitions of static user-defined actions. Each entry is indexed by the name of the entry as it will be shown in the chooser. Its value is a table which can have the following keys (one of `fn` or `url` is required. If both are provided, `url` is ignored):\n * fn - A function which will be called when the entry is selected. The function receives no arguments.\n * url - A URL which will be opened when the entry is selected. Can also be non-HTTP URLs, such as `mailto:` or other app-specific URLs.\n * description - (optional) A string or `hs.styledtext` object that will be shown underneath the main text of the choice.\n * icon - (optional) An `hs.image` object that will be shown next to the entry in the chooser. If not provided, `Seal.plugins.useractions.default_icon` is used. For `url` bookmarks, it can be set to `\"favicon\"` to fetch and use the website's favicon.\n * keyword - (optional) A command by which this action will be invoked, effectively turning it into a Seal command. Any arguments passed to the command will be handled as follows:\n * For `fn` actions, passed as an argument to the function\n * For `url` actions, substituted into the URL, taking the place of any occurrences of `${query}`.\n * hotkey - (optional) A hotkey specification in the form `{ modifiers, key }` by which this action can be invoked.\n * Example configuration:\n```\nspoon.Seal:loadPlugins({\"useractions\"})\nspoon.Seal.plugins.useractions.actions =\n {\n [\"Hammerspoon docs webpage\"] = {\n url = \"http://hammerspoon.org/docs/\",\n icon = hs.image.imageFromName(hs.image.systemImageNames.ApplicationIcon),\n description = \"Open Hammerspoon documentation\",\n hotkey = { hyper, \"h\" },\n },\n [\"Leave corpnet\"] = {\n fn = function()\n spoon.WiFiTransitions:processTransition('foo', 'corpnet01')\n end,\n },\n [\"Arrive in corpnet\"] = {\n fn = function()\n spoon.WiFiTransitions:processTransition('corpnet01', 'foo')\n end,\n },\n [\"Translate using Leo\"] = {\n url = \"http://dict.leo.org/ende/index_de.html#/search=${query}\",\n icon = 'favicon',\n keyword = \"leo\",\n },\n [\"Tell me something\"] = {\n keyword = \"tellme\",\n fn = function(str) hs.alert.show(str) end,\n }\n```", + "file": "Source/Seal.spoon//seal_useractions.lua", + "lineno": "12", + "name": "actions", + "notes": [ + " * A table containing the definitions of static user-defined actions. Each entry is indexed by the name of the entry as it will be shown in the chooser. Its value is a table which can have the following keys (one of `fn` or `url` is required. If both are provided, `url` is ignored):", + " * fn - A function which will be called when the entry is selected. The function receives no arguments.", + " * url - A URL which will be opened when the entry is selected. Can also be non-HTTP URLs, such as `mailto:` or other app-specific URLs.", + " * description - (optional) A string or `hs.styledtext` object that will be shown underneath the main text of the choice.", + " * icon - (optional) An `hs.image` object that will be shown next to the entry in the chooser. If not provided, `Seal.plugins.useractions.default_icon` is used. For `url` bookmarks, it can be set to `\"favicon\"` to fetch and use the website's favicon.", + " * keyword - (optional) A command by which this action will be invoked, effectively turning it into a Seal command. Any arguments passed to the command will be handled as follows:", + " * For `fn` actions, passed as an argument to the function", + " * For `url` actions, substituted into the URL, taking the place of any occurrences of `${query}`.", + " * hotkey - (optional) A hotkey specification in the form `{ modifiers, key }` by which this action can be invoked.", + " * Example configuration:", + "```", + "spoon.Seal:loadPlugins({\"useractions\"})", + "spoon.Seal.plugins.useractions.actions =", + " {", + " [\"Hammerspoon docs webpage\"] = {", + " url = \"http://hammerspoon.org/docs/\",", + " icon = hs.image.imageFromName(hs.image.systemImageNames.ApplicationIcon),", + " description = \"Open Hammerspoon documentation\",", + " hotkey = { hyper, \"h\" },", + " },", + " [\"Leave corpnet\"] = {", + " fn = function()", + " spoon.WiFiTransitions:processTransition('foo', 'corpnet01')", + " end,", + " },", + " [\"Arrive in corpnet\"] = {", + " fn = function()", + " spoon.WiFiTransitions:processTransition('corpnet01', 'foo')", + " end,", + " },", + " [\"Translate using Leo\"] = {", + " url = \"http://dict.leo.org/ende/index_de.html#/search=${query}\",", + " icon = 'favicon',", + " keyword = \"leo\",", + " },", + " [\"Tell me something\"] = {", + " keyword = \"tellme\",", + " fn = function(str) hs.alert.show(str) end,", + " }", + "```" + ], + "signature": "Seal.plugins.useractions.actions", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "Seal.plugins.useractions.get_favicon", + "desc": "", + "doc": "\nIf `true`, attempt to obtain the favicon for URLs added through the `add` command, and use it in the chooser. Defaults to `true`", + "file": "Source/Seal.spoon//seal_useractions.lua", + "lineno": "58", + "name": "get_favicon", + "signature": "Seal.plugins.useractions.get_favicon", + "stripped_doc": "If `true`, attempt to obtain the favicon for URLs added through the `add` command, and use it in the chooser. Defaults to `true`", + "type": "Variable" + } + ], + "desc": "Allow accessing user-defined bookmarks and arbitrary actions from Seal.", + "doc": "Allow accessing user-defined bookmarks and arbitrary actions from Seal.\n", + "items": [ + { + "def": "Seal.plugins.useractions.actions", + "desc": "", + "doc": "\nNotes:\n * A table containing the definitions of static user-defined actions. Each entry is indexed by the name of the entry as it will be shown in the chooser. Its value is a table which can have the following keys (one of `fn` or `url` is required. If both are provided, `url` is ignored):\n * fn - A function which will be called when the entry is selected. The function receives no arguments.\n * url - A URL which will be opened when the entry is selected. Can also be non-HTTP URLs, such as `mailto:` or other app-specific URLs.\n * description - (optional) A string or `hs.styledtext` object that will be shown underneath the main text of the choice.\n * icon - (optional) An `hs.image` object that will be shown next to the entry in the chooser. If not provided, `Seal.plugins.useractions.default_icon` is used. For `url` bookmarks, it can be set to `\"favicon\"` to fetch and use the website's favicon.\n * keyword - (optional) A command by which this action will be invoked, effectively turning it into a Seal command. Any arguments passed to the command will be handled as follows:\n * For `fn` actions, passed as an argument to the function\n * For `url` actions, substituted into the URL, taking the place of any occurrences of `${query}`.\n * hotkey - (optional) A hotkey specification in the form `{ modifiers, key }` by which this action can be invoked.\n * Example configuration:\n```\nspoon.Seal:loadPlugins({\"useractions\"})\nspoon.Seal.plugins.useractions.actions =\n {\n [\"Hammerspoon docs webpage\"] = {\n url = \"http://hammerspoon.org/docs/\",\n icon = hs.image.imageFromName(hs.image.systemImageNames.ApplicationIcon),\n description = \"Open Hammerspoon documentation\",\n hotkey = { hyper, \"h\" },\n },\n [\"Leave corpnet\"] = {\n fn = function()\n spoon.WiFiTransitions:processTransition('foo', 'corpnet01')\n end,\n },\n [\"Arrive in corpnet\"] = {\n fn = function()\n spoon.WiFiTransitions:processTransition('corpnet01', 'foo')\n end,\n },\n [\"Translate using Leo\"] = {\n url = \"http://dict.leo.org/ende/index_de.html#/search=${query}\",\n icon = 'favicon',\n keyword = \"leo\",\n },\n [\"Tell me something\"] = {\n keyword = \"tellme\",\n fn = function(str) hs.alert.show(str) end,\n }\n```", + "file": "Source/Seal.spoon//seal_useractions.lua", + "lineno": "12", + "name": "actions", + "notes": [ + " * A table containing the definitions of static user-defined actions. Each entry is indexed by the name of the entry as it will be shown in the chooser. Its value is a table which can have the following keys (one of `fn` or `url` is required. If both are provided, `url` is ignored):", + " * fn - A function which will be called when the entry is selected. The function receives no arguments.", + " * url - A URL which will be opened when the entry is selected. Can also be non-HTTP URLs, such as `mailto:` or other app-specific URLs.", + " * description - (optional) A string or `hs.styledtext` object that will be shown underneath the main text of the choice.", + " * icon - (optional) An `hs.image` object that will be shown next to the entry in the chooser. If not provided, `Seal.plugins.useractions.default_icon` is used. For `url` bookmarks, it can be set to `\"favicon\"` to fetch and use the website's favicon.", + " * keyword - (optional) A command by which this action will be invoked, effectively turning it into a Seal command. Any arguments passed to the command will be handled as follows:", + " * For `fn` actions, passed as an argument to the function", + " * For `url` actions, substituted into the URL, taking the place of any occurrences of `${query}`.", + " * hotkey - (optional) A hotkey specification in the form `{ modifiers, key }` by which this action can be invoked.", + " * Example configuration:", + "```", + "spoon.Seal:loadPlugins({\"useractions\"})", + "spoon.Seal.plugins.useractions.actions =", + " {", + " [\"Hammerspoon docs webpage\"] = {", + " url = \"http://hammerspoon.org/docs/\",", + " icon = hs.image.imageFromName(hs.image.systemImageNames.ApplicationIcon),", + " description = \"Open Hammerspoon documentation\",", + " hotkey = { hyper, \"h\" },", + " },", + " [\"Leave corpnet\"] = {", + " fn = function()", + " spoon.WiFiTransitions:processTransition('foo', 'corpnet01')", + " end,", + " },", + " [\"Arrive in corpnet\"] = {", + " fn = function()", + " spoon.WiFiTransitions:processTransition('corpnet01', 'foo')", + " end,", + " },", + " [\"Translate using Leo\"] = {", + " url = \"http://dict.leo.org/ende/index_de.html#/search=${query}\",", + " icon = 'favicon',", + " keyword = \"leo\",", + " },", + " [\"Tell me something\"] = {", + " keyword = \"tellme\",", + " fn = function(str) hs.alert.show(str) end,", + " }", + "```" + ], + "signature": "Seal.plugins.useractions.actions", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "Seal.plugins.useractions.get_favicon", + "desc": "", + "doc": "\nIf `true`, attempt to obtain the favicon for URLs added through the `add` command, and use it in the chooser. Defaults to `true`", + "file": "Source/Seal.spoon//seal_useractions.lua", + "lineno": "58", + "name": "get_favicon", + "signature": "Seal.plugins.useractions.get_favicon", + "stripped_doc": "If `true`, attempt to obtain the favicon for URLs added through the `add` command, and use it in the chooser. Defaults to `true`", + "type": "Variable" + } + ], + "name": "Seal.plugins.useractions", + "stripped_doc": "", + "submodules": [], + "type": "Module" + } +] \ No newline at end of file diff --git a/Spoons/Seal.spoon/init.lua b/Spoons/Seal.spoon/init.lua new file mode 100644 index 0000000..30813ba --- /dev/null +++ b/Spoons/Seal.spoon/init.lua @@ -0,0 +1,359 @@ +--- === Seal === +--- +--- Pluggable launch bar +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Seal.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/Seal.spoon.zip) +--- +--- Seal includes a number of plugins, which you can choose to load (see `:loadPlugins()` below): +--- * apps : Launch applications by name +--- * calc : Simple calculator +--- * rot13 : Apply ROT13 substitution cipher +--- * safari_bookmarks : Open Safari bookmarks (this is broken since at least High Sierra) +--- * screencapture : Lets you take screenshots in various ways +--- * urlformats : User defined URL formats to open +--- * useractions : User defined custom actions +--- * vpn : Connect and disconnect VPNs (currently supports Viscosity and macOS system preferences)A + + +local obj = {} +obj.__index = obj + +-- Metadata +obj.name = "Seal" +obj.version = "1.0" +obj.author = "Chris Jones " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +obj.chooser = nil +obj.hotkeyShow = nil +obj.hotkeyToggle = nil +obj.plugins = {} +obj.commands = {} +obj.queryChangedTimer = nil + +obj.spoonPath = hs.spoons.scriptPath() + +--- Seal.queryChangedTimerDuration +--- Variable +--- Time between the last keystroke and the start of the recalculation of the choices to display, in seconds. +--- +--- Notes: +--- * Defaults to 0.02s (20ms). +obj.queryChangedTimerDuration = 0.02 + +--- Seal.plugin_search_paths +--- Variable +--- List of directories where Seal will look for plugins. Defaults to `~/.hammerspoon/seal_plugins/` and the Seal Spoon directory. +obj.plugin_search_paths = { hs.configdir .. "/seal_plugins", obj.spoonPath } + +--- Seal:refreshCommandsForPlugin(plugin_name) +--- Method +--- Refresh the list of commands provided by the given plugin. +--- +--- Parameters: +--- * plugin_name - the name of the plugin. Should be the name as passed to `loadPlugins()` or `loadPluginFromFile`. +--- +--- Returns: +--- * The Seal object +--- +--- Notes: +--- * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands. +function obj:refreshCommandsForPlugin(plugin_name) + plugin = self.plugins[plugin_name] + if plugin.commands then + for cmd,cmdInfo in pairs(plugin:commands()) do + if not self.commands[cmd] then + print("-- Adding Seal command: "..cmd) + self.commands[cmd] = cmdInfo + end + end + end + return self +end + +--- Seal:refreshAllCommands() +--- Method +--- Refresh the list of commands provided by all the currently loaded plugins. +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * The Seal object +--- +--- Notes: +--- * Most Seal plugins expose a static list of commands (if any), which are registered at the time the plugin is loaded. This method is used for plugins which expose a dynamic or changing (e.g. depending on configuration) list of commands. +function obj:refreshAllCommands() + for p, _ in pairs(self.plugins) do + self:refreshCommandsForPlugin(p) + end + return self +end + +--- Seal:loadPluginFromFile(plugin_name, file) +--- Method +--- Loads a plugin from a given file +--- +--- Parameters: +--- * plugin_name - the name of the plugin, without "seal_" at the beginning or ".lua" at the end +--- * file - the file where the plugin code is stored. +--- +--- Returns: +--- * The Seal object if the plugin was successfully loaded, `nil` otherwise +--- +--- Notes: +--- * You should normally use `Seal:loadPlugins()`. This method allows you to load plugins +--- from non-standard locations and is mostly a development interface. +--- * Some plugins may immediately begin doing background work (e.g. Spotlight searches) +function obj:loadPluginFromFile(plugin_name, file) + local f,err = loadfile(file) + if f~= nil then + local plugin = f() + plugin.seal = self + self.plugins[plugin_name] = plugin + self:refreshCommandsForPlugin(plugin_name) + return self + else + return nil + end +end + +--- Seal:loadPlugins(plugins) +--- Method +--- Loads a list of Seal plugins +--- +--- Parameters: +--- * plugins - A list containing the names of plugins to load +--- +--- Returns: +--- * The Seal object +--- +--- Notes: +--- * The plugins live inside the Seal.spoon directory +--- * The plugin names in the list, should not have `seal_` at the start, or `.lua` at the end +--- * Some plugins may immediately begin doing background work (e.g. Spotlight searches) +function obj:loadPlugins(plugins) + self.chooser = hs.chooser.new(self.completionCallback) + self.chooser:choices(self.choicesCallback) + self.chooser:queryChangedCallback(self.queryChangedCallback) + + for k,plugin_name in pairs(plugins) do + local loaded=nil + print("-- Loading Seal plugin: " .. plugin_name) + for _,dir in ipairs(self.plugin_search_paths) do + if obj.plugins[plugin_name] == nil then + local file = dir .. "/seal_" .. plugin_name .. ".lua" + loaded = (self:loadPluginFromFile(plugin_name, file) ~= nil) + end + end + if (not loaded) then + hs.showError(string.format("Error: could not find Seal plugin %s in any of the load paths %s", plugin_name, hs.inspect(self.plugin_search_paths))) + end + end + return self +end + +--- Seal:bindHotkeys(mapping) +--- Method +--- Binds hotkeys for Seal +--- +--- Parameters: +--- * mapping - A table containing hotkey modifier/key details for the following (optional) items: +--- * show - This will cause Seal's UI to be shown +--- * toggle - This will cause Seal's UI to be shown or hidden depending on its current state +--- +--- Returns: +--- * The Seal object +function obj:bindHotkeys(mapping) + if (self.hotkeyShow) then + self.hotkeyShow:delete() + end + if (self.hotkeyToggle) then + self.hotkeyToggle:delete() + end + + if mapping["show"] ~= nil then + local showMods = mapping["show"][1] + local showKey = mapping["show"][2] + self.hotkeyShow = hs.hotkey.new(showMods, showKey, function() self:show() end) + end + if mapping["toggle"] ~= nil then + local toggleMods = mapping["toggle"][1] + local toggleKey = mapping["toggle"][2] + self.hotkeyToggle = hs.hotkey.new(toggleMods, toggleKey, function() self:toggle() end) + end + + return self +end + +--- Seal:start() +--- Method +--- Starts Seal +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * The Seal object +function obj:start() + print("-- Starting Seal") + if self.hotkeyShow then + self.hotkeyShow:enable() + end + if self.hotkeyToggle then + self.hotkeyToggle:enable() + end + return self +end + +--- Seal:stop() +--- Method +--- Stops Seal +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * The Seal object +--- +--- Notes: +--- * Some Seal plugins will continue performing background work even after this call (e.g. Spotlight searches) +function obj:stop() + print("-- Stopping Seal") + self.chooser:hide() + if self.hotkeyShow then + self.hotkeyShow:disable() + end + if self.hotkeyToggle then + self.hotkeyToggle:disable() + end + return self +end + +--- Seal:show(query) +--- Method +--- Shows the Seal UI +--- +--- Parameters: +--- * query - An optional string to pre-populate the query box with +--- +--- Returns: +--- * None +--- +--- Notes: +--- * This may be useful if you wish to show Seal in response to something other than its hotkey +function obj:show(query) + self.chooser:show() + if query then self.chooser:query(query) end + return self +end + +--- Seal:toggle(query) +--- Method +--- Shows or hides the Seal UI +--- +--- Parameters: +--- * query - An optional string to pre-populate the query box with +--- +--- Returns: +--- * None +function obj:toggle(query) + if self.chooser:isVisible() then + self.chooser:hide() + else + self:show(query) + end + return self +end + +function obj.completionCallback(rowInfo) + if rowInfo == nil then + return + end + if rowInfo["type"] == "plugin_cmd" then + obj.chooser:query(rowInfo["cmd"]) + return + end + for k,plugin in pairs(obj.plugins) do + if plugin.__name == rowInfo["plugin"] then + plugin.completionCallback(rowInfo) + break + end + end +end + +function obj.choicesCallback() + -- TODO: Sort each of these clusters of choices, alphabetically + choices = {} + query = obj.chooser:query() + cmd = nil + query_words = {} + if tostring(query):find("^%s*$") ~= nil then + return choices + end + for word in string.gmatch(query, "%S+") do + if cmd == nil then + cmd = word + else + table.insert(query_words, word) + end + end + query_words = table.concat(query_words, " ") + -- First get any direct command matches + for _,cmdInfo in pairs(obj.commands) do + cmd_fn = cmdInfo["fn"] + if cmd:lower() == cmdInfo["cmd"]:lower() then + if (query_words or "") == "" then + query_words = ".*" + end + fn_choices = cmd_fn(query_words) + if fn_choices ~= nil then + for j,choice in pairs(fn_choices) do + table.insert(choices, choice) + end + end + end + end + -- Now get any bare matches + for k,plugin in pairs(obj.plugins) do + bare = plugin:bare() + if bare then + for i,choice in pairs(bare(query)) do + table.insert(choices, choice) + end + end + end + -- Now add in any matching commands + -- TODO: This only makes sense to do if we can select the choice without dismissing the chooser, which requires changes to HSChooser + for command,cmdInfo in pairs(obj.commands) do + if string.match(command, query) and #query_words == 0 then + choice = {} + choice["text"] = cmdInfo["name"] + choice["subText"] = cmdInfo["description"] + choice["type"] = "plugin_cmd" + table.insert(choices,choice) + end + end + + return choices +end + +function obj.queryChangedCallback(query) + if obj.queryChangedTimer then + obj.queryChangedTimer:stop() + end + obj.queryChangedTimer = hs.timer.doAfter(obj.queryChangedTimerDuration, + function() obj.chooser:refreshChoicesCallback() end) +end + +return obj + +--- === Seal.plugins === +--- +--- Various APIs for Seal plugins + +-- This isn't really shown, but it's necessary to force Seal.plugins.html to render +--- Seal.plugins +--- Constant +--- This is a table containing all of the loaded plugins for Seal. You should interact with it only via documented API that the plugins expose. diff --git a/Spoons/Seal.spoon/seal_apps.lua b/Spoons/Seal.spoon/seal_apps.lua new file mode 100644 index 0000000..3537494 --- /dev/null +++ b/Spoons/Seal.spoon/seal_apps.lua @@ -0,0 +1,236 @@ +--- === Seal.plugins.apps === +--- +--- A plugin to add launchable apps/scripts, making Seal act as a launch bar +local obj = {} +obj.__index = obj +obj.__name = "seal_apps" +obj.appCache = {} + +--- Seal.plugins.apps.appSearchPaths +--- Variable +--- Table containing the paths to search for launchable items +--- +--- Notes: +--- * If you change this, you will need to call `spoon.Seal.plugins.apps:restart()` to force Spotlight to search for new items. +obj.appSearchPaths = { + "/Applications", + "/System/Applications", + "~/Applications", + "/Developer/Applications", + "/Applications/Xcode.app/Contents/Applications", + "/System/Library/PreferencePanes", + "/Library/PreferencePanes", + "~/Library/PreferencePanes", + "/System/Library/CoreServices/Applications", + "/System/Library/CoreServices/", + "/usr/local/Cellar", + "/Library/Scripts", + "~/Library/Scripts" +} + +local modifyNameMap = function(info, add) + for _, item in ipairs(info) do + icon = nil + local displayname = item.kMDItemDisplayName or hs.fs.displayName(item.kMDItemPath) + displayname = displayname:gsub("%.app$", "", 1) + if string.find(item.kMDItemPath, "%.prefPane$") then + displayname = displayname .. " preferences" + if add then + icon = hs.image.iconForFile(item.kMDItemPath) + end + end + if add then + bundleID = item.kMDItemCFBundleIdentifier + if (not icon) and (bundleID) then + icon = hs.image.imageFromAppBundle(bundleID) + end + obj.appCache[displayname] = { + path = item.kMDItemPath, + bundleID = bundleID, + icon = icon + } + else + obj.appCache[displayname] = nil + end + end +end + +local updateNameMap = function(obj, msg, info) + if info then + -- all three can occur in either message, so check them all! + if info.kMDQueryUpdateAddedItems then modifyNameMap(info.kMDQueryUpdateAddedItems, true) end + if info.kMDQueryUpdateChangedItems then modifyNameMap(info.kMDQueryUpdateChangedItems, true) end + if info.kMDQueryUpdateRemovedItems then modifyNameMap(info.kMDQueryUpdateRemovedItems, false) end + else + -- shouldn't happen for didUpdate or inProgress + print("~~~ userInfo from SpotLight was empty for " .. msg) + end +end + +--- Seal.plugins.apps:start() +--- Method +--- Starts the Spotlight app searcher +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +--- +--- Notes: +--- * This is called automatically when the plugin is loaded +function obj:start() + obj.spotlight = hs.spotlight.new():queryString([[ (kMDItemContentType = "com.apple.application-bundle") || (kMDItemContentType = "com.apple.systempreference.prefpane") || (kMDItemContentType = "com.apple.applescript.text") || (kMDItemContentType = "com.apple.applescript.script") ]]) + :callbackMessages("didUpdate", "inProgress") + :setCallback(updateNameMap) + :searchScopes(obj.appSearchPaths) + :start() +end + +--- Seal.plugins.apps:stop() +--- Method +--- Stops the Spotlight app searcher +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function obj:stop() + obj.spotlight:stop() + obj.spotlight = nil + obj.appCache = {} +end + +--- Seal.plugins.apps:restart() +--- Method +--- Restarts the Spotlight app searcher +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +function obj:restart() + self:stop() + self:start() +end + +hs.application.enableSpotlightForNameSearches(true) +obj:start() + +function obj:commands() + return {kill = { + cmd = "kill", + fn = obj.choicesKillCommand, + plugin = obj.__name, + name = "Kill", + description = "Kill an application" + }, + reveal = { + cmd = "reveal", + fn = obj.choicesRevealCommand, + plugin = obj.__name, + name = "Reveal", + description = "Reveal an application in the Finder" + } + } +end + +function obj:bare() + return self.choicesApps +end + +function obj.choicesApps(query) + local choices = {} + if query == nil or query == "" then + return choices + end + for name,app in pairs(obj.appCache) do + if string.match(name:lower(), query:lower()) then + local choice = {} + local instances = {} + if app["bundleID"] then + instances = hs.application.applicationsForBundleID(app["bundleID"]) + end + if #instances > 0 then + choice["text"] = name .. " (Running)" + else + choice["text"] = name + end + choice["subText"] = app["path"] + if app["icon"] then + choice["image"] = app["icon"] + end + choice["path"] = app["path"] + choice["uuid"] = obj.__name.."__"..(app["bundleID"] or name) + choice["plugin"] = obj.__name + choice["type"] = "launchOrFocus" + table.insert(choices, choice) + end + end + return choices +end + +function obj.choicesKillCommand(query) + local choices = {} + if query == nil then + return choices + end + local apps = hs.application.runningApplications() + for k, app in pairs(apps) do + local name = app:name() + if string.match(name:lower(), query:lower()) and app:mainWindow() then + local choice = {} + choice["text"] = "Kill "..name + choice["subText"] = app:path().." PID: "..app:pid() + choice["pid"] = app:pid() + choice["plugin"] = obj.__name + choice["type"] = "kill" + choice["image"] = hs.image.imageFromAppBundle(app:bundleID()) + table.insert(choices, choice) + end + end + return choices +end + +function obj.choicesRevealCommand(query) + local choices = {} + if query == nil then + return choices + end + local apps = obj.choicesApps(query) + for k, app in pairs(apps) do + local name = app.text + if string.match(name:lower(), query:lower()) then + local choice = {} + choice["text"] = "Reveal "..name + choice["path"] = app.path + choice["subText"] = app.path + choice["plugin"] = obj.__name + choice["type"] = "reveal" + if app.image then + choice["image"] = app.image + end + table.insert(choices, choice) + end + end + return choices +end + +function obj.completionCallback(rowInfo) + if rowInfo["type"] == "launchOrFocus" then + if string.find(rowInfo["path"], "%.applescript$") or string.find(rowInfo["path"], "%.scpt$") then + hs.task.new("/usr/bin/osascript", nil, { rowInfo["path"] }):start() + else + hs.task.new("/usr/bin/open", nil, { rowInfo["path"] }):start() + end + elseif rowInfo["type"] == "kill" then + hs.application.get(rowInfo["pid"]):kill() + elseif rowInfo["type"] == "reveal" then + hs.osascript.applescript(string.format([[tell application "Finder" to reveal (POSIX file "%s")]], rowInfo["path"])) + hs.application.launchOrFocus("Finder") + end +end + +return obj diff --git a/Spoons/Seal.spoon/seal_calc.lua b/Spoons/Seal.spoon/seal_calc.lua new file mode 100644 index 0000000..24e710f --- /dev/null +++ b/Spoons/Seal.spoon/seal_calc.lua @@ -0,0 +1,47 @@ +local obj = {} +obj.__index = obj +obj.__name = "seal_calc" +obj.icon = hs.image.imageFromAppBundle("com.apple.Calculator") + +function obj:commands() + return {} +end + +function obj:bare() + return self.bareCalc +end + +function obj.bareCalc(query) + local choices = {} + if query == nil or query == "" then + return choices + end + + -- Filter out commas and dollar signs + query, _ = query:gsub("[%,%$]", "") + + -- We need to determine if the query only contains mathematical calculations + -- To do this we'll see if it matches the inverse of that set of characters + if string.match(query, "[^%d^%.^%+^%-^/^%*^%^^ ^%(^%)]") == nil then + local choice = {} + local compile_result, fn = load("return " .. query) + if type(compile_result) == "function" then + local result = compile_result() + choice["text"] = result + choice["subText"] = "Copy result to clipboard" + choice["image"] = obj.icon + choice["plugin"] = obj.__name + choice["type"] = "copyToClipboard" + table.insert(choices, choice) + end + end + return choices +end + +function obj.completionCallback(rowInfo) + if rowInfo["type"] == "copyToClipboard" then + hs.pasteboard.setContents(rowInfo["text"]) + end +end + +return obj diff --git a/Spoons/Seal.spoon/seal_filesearch.lua b/Spoons/Seal.spoon/seal_filesearch.lua new file mode 100644 index 0000000..d55cc62 --- /dev/null +++ b/Spoons/Seal.spoon/seal_filesearch.lua @@ -0,0 +1,168 @@ +--- === Seal.plugins.filesearch === +--- +--- A plugin to add file search capabilities, making Seal act as a spotlight file search +local obj = {} +obj.__index = obj +obj.__name = "seal_filesearch" + +--- Seal.plugins.filesearch.fileSearchPaths +--- Variable +--- Table containing the paths to search for files +--- +--- Notes: +--- * You will need to authorize hammerspoon to access the folders in this list in order for this to work. +obj.fileSearchPaths = {"~/", "~/Downloads", "~/Documents", "~/Movies", "~/Desktop", "~/Music", "~/Pictures"} + +--- Seal.plugins.filesearch.maxResults +--- Variable +--- Maximum number of results to display +obj.maxQueryResults = 40 + +--- Seal.plugins.filesearch.displayResultsTimeout +--- Variable +--- Maximum time to wait before displaying the results +--- Defaults to 0.2s (200ms). +--- +--- Notes: +--- * higher value might give you more results but will give a less snappy experience +obj.displayResultsTimeout = 0.2 + +-- Variables +obj.currentQuery = nil +obj.currentQueryResults = {} +obj.currentQueryResultsDisplayed = false +obj.showQueryResultsTimer = nil + +obj.spotlight = hs.spotlight.new() + +-- hammerspoon passes .* as empty query +EMPTY_QUERY = ".*" + +-- Private functions + +local stopCurrentSearch = function() + if obj.spotlight:isRunning() then + obj.spotlight:stop() + end + if obj.showQueryResultsTimer ~= nil and obj.showQueryResultsTimer:running() then + obj.showQueryResultsTimer:stop() + end +end + +local displayQueryResults = function() + stopCurrentSearch() + if not obj.currentQueryResultsDisplayed then + obj.currentQueryResultsDisplayed = true + -- we force seal to refresh the choices so we can serve the real query results + obj.seal.chooser:refreshChoicesCallback() + end +end + +local buildSpotlightQuery = function(query) + local queryWords = hs.fnutils.split(query, "%s+") + local searchFilters = hs.fnutils.map(queryWords, function(word) + return [[kMDItemFSName like[c] "*]] .. word .. [[*"]] + end) + local spotligthQuery = table.concat(searchFilters, [[ && ]]) + return spotligthQuery +end + +local convertSpotlightResultToQueryResult = function(item) + local icon = hs.image.iconForFile(item.kMDItemPath) + local bundleID = item.kMDItemCFBundleIdentifier + if (not icon) and (bundleID) then + icon = hs.image.imageFromAppBundle(bundleID) + end + return { + text = item.kMDItemDisplayName, + subText = item.kMDItemPath, + path = item.kMDItemPath, + uuid = obj.__name .. "__" .. (bundleID or item.kMDItemDisplayName), + plugin = obj.__name, + type = "open", + image = icon + } +end + +local updateQueryResults = function(items) + for _, item in ipairs(items) do + if #obj.currentQueryResults >= obj.maxQueryResults then + break + end + table.insert(obj.currentQueryResults, convertSpotlightResultToQueryResult(item)) + end +end + +local handleSpotlightCallback = function(_, msg, info) + if msg == "inProgress" and info.kMDQueryUpdateAddedItems ~= nil then + updateQueryResults(info.kMDQueryUpdateAddedItems) + end + + if msg == "didFinish" or #obj.currentQueryResults >= obj.maxQueryResults then + displayQueryResults() + end +end + +-- Public methods + +function obj:commands() + return { + filesearch = { + cmd = "'", + fn = obj.fileSearch, + name = "Search file", + description = "Search file", + plugin = obj.__name + } + } +end + +function obj:bare() + return nil +end + +function obj.completionCallback(rowInfo) + if rowInfo["type"] == "open" then + if string.find(rowInfo["path"], "%.applescript$") or string.find(rowInfo["path"], "%.scpt$") then + hs.task.new("/usr/bin/osascript", nil, {rowInfo["path"]}):start() + else + hs.task.new("/usr/bin/open", nil, {rowInfo["path"]}):start() + end + end +end + +function obj.fileSearch(query) + stopCurrentSearch() + + if query == EMPTY_QUERY then + obj.currentQuery = "" + obj.currentQueryResults = {} + return {} + end + + if query ~= obj.currentQuery then + -- Seal want the results synchronously, but spotlight will return then asynchronously + -- to workaround that, we launch the spotlight search in the background and + -- return the previous results (so that Seal doesn't change the current results list) + -- We force a refresh later once we have the results + local previousResults = obj.currentQueryResults + obj.currentQuery = query + obj.currentQueryResults = {} + obj.currentQueryResultsDisplayed = false + + obj.spotlight:queryString(buildSpotlightQuery(query)):start() + obj.showQueryResultsTimer = hs.timer.doAfter(obj.displayResultsTimeout, displayQueryResults) + + return previousResults + else + -- If we are here, it's mean the force refreshed has been triggered after receving spotlight results + -- we just return the results we accumulated from spotlight + return obj.currentQueryResults + end + +end + +obj.spotlight:searchScopes(obj.fileSearchPaths):callbackMessages("inProgress", "didFinish"):setCallback( + handleSpotlightCallback) + +return obj diff --git a/Spoons/Seal.spoon/seal_pasteboard.lua b/Spoons/Seal.spoon/seal_pasteboard.lua new file mode 100644 index 0000000..f7c6f47 --- /dev/null +++ b/Spoons/Seal.spoon/seal_pasteboard.lua @@ -0,0 +1,182 @@ +--- ==== Seal.plugins.pasteboard ==== +--- +--- Visual, searchable pasteboard (ie clipboard) history +local obj = {} +obj.__index = obj +obj.__name = "seal_pasteboard" +obj.timer = nil +obj.lastItem = nil +obj.itemBuffer = {} +obj.choices = {} + +--- Seal.plugins.pasteboard.historySize +--- Variable +--- +--- The number of history items to keep. Defaults to 50 +obj.historySize = 50 + +--- Seal.plugins.pasteboard.saveHistory +--- Variable +--- +--- A boolean, true if Seal should automatically load/save clipboard history. Defaults to true +obj.saveHistory = true + +--- Seal.plugins.pasteboard.skipUTIs +--- Variable +--- +--- An array of UTIs to skip when saving to the history. Defaults to: +--- ``` +--- { +--- "de.petermaurer.TransientPasteboardType", +--- "com.typeit4me.clipping", +--- "Pasteboard generator type", +--- "com.agilebits.onepassword", +--- "org.nspasteboard.TransientType", +--- "org.nspasteboard.ConcealedType", +--- "org.nspasteboard.AutoGeneratedType" +--- } +--- ``` +obj.skipUTIs = { + "de.petermaurer.TransientPasteboardType", + "com.typeit4me.clipping", + "Pasteboard generator type", + "com.agilebits.onepassword", + "org.nspasteboard.TransientType", + "org.nspasteboard.ConcealedType", + "org.nspasteboard.AutoGeneratedType" +} + +function obj:commands() + return { + pb = { + cmd = "pb", + fn = obj.choicesPasteboardCommand, + name = "Pasteboard", + description = "Pasteboard history", + plugin = obj.__name + } + } +end + +function obj:bare() + return nil +end + +function obj.choicesPasteboardCommand(query) + -- Return the choices that match the query + return hs.fnutils.filter(obj.choices, function(choice) + return string.find(string.lower(choice["text"]), string.lower(query)) + end) +end + +function obj.pasteboardToChoice(item) + local choice = {} + + choice["uuid"] = item["uuid"] + choice["name"] = item["text"] + choice["text"] = item["text"] + choice["kind"] = kind + choice["plugin"] = obj.__name + choice["type"] = "copy" + choice["subText"] = "" + + if item["uti"] then + choice["subText"] = item["uti"] + if hs.application.defaultAppForUTI then + local bundleID = hs.application.defaultAppForUTI(item["uti"]) + print("Default app for " .. item["uti"] .. " :: " .. (bundleID or "(null)")) + if bundleID then + choice["image"] = hs.image.imageFromAppBundle(bundleID) + end + end + end + if item["dateTime"] then + choice["subText"] = choice["subText"] .. " :: " .. item["dateTime"] + end + + return choice +end + +function obj.completionCallback(rowInfo) + if rowInfo["type"] == "copy" then + hs.pasteboard.setContents(rowInfo["name"]) + end +end + +function obj.checkPasteboard() + local pasteboard = hs.pasteboard.getContents() + local shouldSave = false + + if pasteboard == nil then + return + end + + if (#obj.itemBuffer == 0) or (pasteboard ~= obj.itemBuffer[#obj.itemBuffer]["text"]) then + local currentTypes = hs.pasteboard.allContentTypes()[1] + if currentTypes == nil then + print("ERROR: NO PASTEBOARD CURRENT TYPES. Please file a bug so we can understand this:") + print(hs.inspect(pasteboard)) + return + end + for _, aType in pairs(currentTypes) do + for _, uti in pairs(obj.skipUTIs) do + if uti == aType then + return + end + end + end + local item = {} + item["uuid"] = hs.host.uuid() + item["text"] = pasteboard + item["uti"] = currentTypes[1] + item["dateTime"] = os.date() + + table.insert(obj.itemBuffer, item) + table.insert(obj.choices, obj.pasteboardToChoice(item)) + + shouldSave = true + end + if #obj.itemBuffer > obj.historySize then + table.remove(obj.itemBuffer, 1) + table.remove(obj.choices, 1) + + shouldSave = true + end + + if shouldSave then + obj.save() + end +end + +function obj.save() + local json = hs.json.encode(obj.itemBuffer) + local file = io.open(os.getenv("HOME") .. "/.hammerspoon/pasteboard_history.json", "w") + if file then + file:write(json) + file:close() + end +end + +function obj.load() + local file = io.open(os.getenv("HOME") .. "/.hammerspoon/pasteboard_history.json", "r") + if file then + local json = hs.json.decode(file:read()) + if json then + obj.itemBuffer = json + + -- Convert all the items to the choice buffer + for _, v in ipairs(obj.itemBuffer) do + table.insert(obj.choices, obj.pasteboardToChoice(v)) + end + end + file:close() + end +end + +obj.load() +if obj.timer == nil then + obj.timer = hs.timer.doEvery(1, function() obj.checkPasteboard(obj) end) + obj.timer:start() +end + +return obj diff --git a/Spoons/Seal.spoon/seal_rot13.lua b/Spoons/Seal.spoon/seal_rot13.lua new file mode 100644 index 0000000..4f20821 --- /dev/null +++ b/Spoons/Seal.spoon/seal_rot13.lua @@ -0,0 +1,48 @@ +local obj = {} +obj.__index = obj +obj.__name = "seal_rot13" + +function obj:commands() + return { + rot13 = { + cmd = "rot13", + fn = obj.rot13, + name = "ROT13", + description = "Apply ROT13 substitution cipher" + } + } +end + +function obj:bare() + return nil +end + +function obj.rot13(query) + -- ROT13 implementation taken from https://rosettacode.org/wiki/Rot-13#Lua + local a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + local b = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm" + local rot13Text = + query:gsub( + "%a", + function(c) + return b:sub(a:find(c)) + end + ) + + return { + { + text = rot13Text, + subText = "Copy result to clipboard", + plugin = obj.__name, + type = "copyToClipboard" + } + } +end + +function obj.completionCallback(rowInfo) + if rowInfo["type"] == "copyToClipboard" then + hs.pasteboard.setContents(rowInfo["text"]) + end +end + +return obj diff --git a/Spoons/Seal.spoon/seal_safari_bookmarks.lua b/Spoons/Seal.spoon/seal_safari_bookmarks.lua new file mode 100644 index 0000000..377b30b --- /dev/null +++ b/Spoons/Seal.spoon/seal_safari_bookmarks.lua @@ -0,0 +1,90 @@ +--- === Seal.plugins.safari_bookmarks === +--- Access Safari bookmarks from Seal +--- +--- Note: Apple has changed the way Safari stores bookmarks and this plugin no longer works on recent macOS releases. +local obj = {} +obj.__index = obj +obj.__name = "seal_safari_bookmarks" +obj.bookmarkCache = {} +obj.icon = hs.image.iconForFileType("com.apple.safari.bookmark") + +--- Seal.plugins.safari_bookmarks.always_open_with_safari +--- Variable +--- If `true` (default), bookmarks are always opened with Safari, otherwise they are opened with the default application using the `/usr/bin/open` command. +obj.always_open_with_safari = true + +local modifyNameMap = function(info, add) + local name + for _, item in ipairs(info) do + name = item.kMDItemDisplayName + if name ~= nil then + if add then + obj.bookmarkCache[name] = { + url = item.kMDItemURL, + } + else + obj.bookmarkCache[name] = nil + end + end + end +end + +local updateNameMap = function(obj, msg, info) + if info then + -- all three can occur in either message, so check them all! + if info.kMDQueryUpdateAddedItems then modifyNameMap(info.kMDQueryUpdateAddedItems, true) end + if info.kMDQueryUpdateChangedItems then modifyNameMap(info.kMDQueryUpdateChangedItems, true) end + if info.kMDQueryUpdateRemovedItems then modifyNameMap(info.kMDQueryUpdateRemovedItems, false) end + else + -- shouldn't happen for didUpdate or inProgress + print("~~~ userInfo from SpotLight was empty for " .. msg) + end +end + +obj.spotlight = hs.spotlight.new():queryString([[ kMDItemContentType = "com.apple.safari.bookmark" ]]) + :callbackMessages("didUpdate", "inProgress") + :setCallback(updateNameMap) + :start() + +function obj:commands() + return {} +end + +function obj:bare() + return self.choicesBookmarks +end + +function obj.choicesBookmarks(query) + local choices = {} + if query == nil or query == "" then + return choices + end + for name,bookmark in pairs(obj.bookmarkCache) do + url = bookmark["url"] + if url and (string.match(name:lower(), query:lower()) or string.match(url:lower(), query:lower())) then + local choice = {} + local instances = {} + choice["text"] = name + choice["subText"] = url + choice["url"] = url + choice["image"] = obj.icon + choice["uuid"] = obj.__name.."__"..name.."__"..url + choice["plugin"] = obj.__name + choice["type"] = "openURL" + table.insert(choices, choice) + end + end + return choices +end + +function obj.completionCallback(rowInfo) + if rowInfo["type"] == "openURL" then + if obj.always_open_with_safari then + hs.urlevent.openURLWithBundle(rowInfo["url"], "com.apple.Safari") + else + hs.execute(string.format("/usr/bin/open '%s'", rowInfo["url"])) + end + end +end + +return obj diff --git a/Spoons/Seal.spoon/seal_screencapture.lua b/Spoons/Seal.spoon/seal_screencapture.lua new file mode 100644 index 0000000..d7c35da --- /dev/null +++ b/Spoons/Seal.spoon/seal_screencapture.lua @@ -0,0 +1,98 @@ +--- === Seal.plugins.screencapture === +--- +--- A plugin to capture the screen in various ways +local obj = {} +obj.__index = obj +obj.__name = "seal_screencapture" + +--- Seal.plugins.screencapture.showPostUI +--- Variable +--- Whether or not to show the screen capture UI in macOS 10.14 or later +obj.showPostUI = true + +local static_choices = { + { + text = "Capture menu", + subText = "Show macOS screen capture menu", + plugin = obj.__name, + type = "screenUI" + }, + { + text = "Capture Screen", + subText = "Capture the current screen", + plugin = obj.__name, + type = "screen" + }, + { + text = "Capture Screen to Clipboard", + subText = "Capture the current screen to the clipboard", + plugin = obj.__name, + type = "screen_clipboard" + }, + { + text = "Capture Interactive", + subText = "Draw a rectangle to capture", + plugin = obj.__name, + type = "interactive" + }, + { + text = "Capture Interactive to Clipboard", + subText = "Draw a rectangle to capture to the clipboard", + plugin = obj.__name, + type = "interactive_clipboard" + } +} + +function obj:commands() + return {sc = { + cmd = "sc", + fn = obj.choicesScreenCaptureCommand, + name = "Screencapture", + description = "Capture the screen", + plugin = obj.__name + } + } +end + +function obj:bare() + return nil +end + +function obj.choicesScreenCaptureCommand(query) + local choices = {} + + for k,choice in pairs(static_choices) do + if string.match(choice["text"]:lower(), query:lower()) then + table.insert(choices, choice) + end + end + + return choices +end + +function obj.completionCallback(rowInfo) + local filename = hs.fs.pathToAbsolute("~").."/Desktop/Screen Capture at "..os.date("!%Y-%m-%d-%T")..".png" + local args = "" + local scType = rowInfo["type"] + + if scType == "screen" then + -- Nothing required here + elseif scType == "screen_clipboard" then + args = "-c" + elseif scType == "interactive" then + args = "-i" + elseif scType == "screenUI" then + args = "-iU" + elseif scType == "interactive_clipboard" then + args = "-ci" + end + + if obj.showPostUI then + args = args .. "u" + end + + print(hs.inspect(args)) + hs.task.new("/usr/sbin/screencapture", nil, {args, filename}):start() +end + +return obj diff --git a/Spoons/Seal.spoon/seal_urlformats.lua b/Spoons/Seal.spoon/seal_urlformats.lua new file mode 100644 index 0000000..c1947e8 --- /dev/null +++ b/Spoons/Seal.spoon/seal_urlformats.lua @@ -0,0 +1,125 @@ +--- === Seal.plugins.urlformats === +--- +--- A plugin to quickly open URLs containing a search/query term +--- This plugin is invoked with the `uf` keyword and requires some configuration, see `:providersTable()` +--- +--- The way this works is by defining a set of providers, each of which contains a URL with a `%s` somewhere insert it. +--- When the user types `uf` in Seal, followed by some more characters, those characters will be inserted into the string at the point where the `%s` is. +--- +--- By way of an example, you could define a provider with a url like `http://bugs.mycorp.com/showBug?id=%s`, and just need to type `uf 123456` in Seal to get a quick shortcut to open the full URL. +local obj = {} +obj.__index = obj +obj.__name = "seal_urlformats" + +obj.providers = {} + +-- Example format for providers table +-- { +-- rhbz = { +-- name = "Red Hat Bugzilla", +-- url = "https://bugzilla.redhat.com/show_bug.cgi?id=%s", +-- }, +-- lp = { +-- name = "Launchpad Bug", +-- url = "https://launchpad.net/bugs/%s", +-- }, +-- } + +function obj:commands() + return {uf = { + cmd = "uf", + fn = obj.choicesURLPart, + name = "URL Formats", + description = "Open a full URL with a search term", + plugin = obj.__name + } + } +end + +function obj:bare() + return obj.choicesBareURL +end + +function obj.choicesBareURL(query) + local choices = {} + if string.find(query, "://") ~= nil then + local scheme = string.sub(query, 1, string.find(query, "://") - 1) + local handlers = hs.urlevent.getAllHandlersForScheme(scheme) + for _,bundleID in pairs(handlers) do + local choice = {} + local bundleInfo = hs.application.infoForBundleID(bundleID) + if bundleInfo and bundleInfo["CFBundleName"] then + choice["text"] = "Open URI with "..bundleInfo["CFBundleName"] + choice["handler"] = bundleID + choice["scheme"] = scheme + choice["type"] = "launch" + choice["url"] = query + choice["plugin"] = obj.__name + choice["image"] = hs.image.imageFromAppBundle(bundleID) + table.insert(choices, choice) + end + end + end + return choices +end + +function obj.choicesURLPart(query) + --print("choicesURLPart for: "..query) + local choices = {} + for name,data in pairs(obj.providers) do + local data_url = data["url"]:gsub("([^%%])%%([^s])", "%1%%%%%2") + local full_url = string.format(data_url, query) + local url_scheme = string.sub(full_url, 1, string.find(full_url, "://") - 1) + local choice = {} + choice["text"] = data["name"] + choice["subText"] = full_url + choice["plugin"] = obj.__name + choice["type"] = "launch" + choice["url"] = full_url + choice["scheme"] = url_scheme + table.insert(choices, choice) + end + return choices +end + +function obj.completionCallback(rowInfo) + if rowInfo["type"] == "launch" then + local handler = nil + if rowInfo["handler"] == nil then + handler = hs.urlevent.getDefaultHandler(rowInfo["scheme"]) + else + handler = rowInfo["handler"] + end + hs.urlevent.openURLWithBundle(rowInfo["url"], handler) + end +end + +--- Seal.plugins.urlformats:providersTable(aTable) +--- Method +--- Gets or sets the current providers table +--- +--- Parameters: +--- * aTable - An optional table of providers, which must contain the following keys: +--- * name - A string naming the provider, which will be shown in the Seal results +--- * url - A string containing the URL to insert the user's query into. This should contain one and only one `%s` +--- +--- Returns: +--- * Either a table of current providers, if no parameter was passed, or nothing if a parmameter was passed. +--- +--- Notes: +--- * An example table might look like: +--- ```lua +--- { +--- rhbz = { name = "Red Hat Bugzilla", url = "https://bugzilla.redhat.com/show_bug.cgi?id=%s", }, +--- lp = { name = "Launchpad Bug", url = "https://launchpad.net/bugs/%s", }, +--- } +--- ``` +function obj:providersTable(aTable) + if aTable then + self.providers = aTable + else + return self.providers + end +end + +return obj diff --git a/Spoons/Seal.spoon/seal_useractions.lua b/Spoons/Seal.spoon/seal_useractions.lua new file mode 100644 index 0000000..0e3559f --- /dev/null +++ b/Spoons/Seal.spoon/seal_useractions.lua @@ -0,0 +1,286 @@ +--- ==== Seal.plugins.useractions ==== +--- +--- Allow accessing user-defined bookmarks and arbitrary actions from Seal. +--- + +local obj = {} +obj.__index = obj +obj.__basename = "useractions" +obj.__name = "seal_" .. obj.__basename +obj.default_icon = hs.image.imageFromName(hs.image.systemImageNames.ActionTemplate) + +--- Seal.plugins.useractions.actions +--- Variable +--- +--- Notes: +--- * A table containing the definitions of static user-defined actions. Each entry is indexed by the name of the entry as it will be shown in the chooser. Its value is a table which can have the following keys (one of `fn` or `url` is required. If both are provided, `url` is ignored): +--- * fn - A function which will be called when the entry is selected. The function receives no arguments. +--- * url - A URL which will be opened when the entry is selected. Can also be non-HTTP URLs, such as `mailto:` or other app-specific URLs. +--- * description - (optional) A string or `hs.styledtext` object that will be shown underneath the main text of the choice. +--- * icon - (optional) An `hs.image` object that will be shown next to the entry in the chooser. If not provided, `Seal.plugins.useractions.default_icon` is used. For `url` bookmarks, it can be set to `"favicon"` to fetch and use the website's favicon. +--- * keyword - (optional) A command by which this action will be invoked, effectively turning it into a Seal command. Any arguments passed to the command will be handled as follows: +--- * For `fn` actions, passed as an argument to the function +--- * For `url` actions, substituted into the URL, taking the place of any occurrences of `${query}`. +--- * hotkey - (optional) A hotkey specification in the form `{ modifiers, key }` by which this action can be invoked. +--- * Example configuration: +--- ``` +--- spoon.Seal:loadPlugins({"useractions"}) +--- spoon.Seal.plugins.useractions.actions = +--- { +--- ["Hammerspoon docs webpage"] = { +--- url = "http://hammerspoon.org/docs/", +--- icon = hs.image.imageFromName(hs.image.systemImageNames.ApplicationIcon), +--- description = "Open Hammerspoon documentation", +--- hotkey = { hyper, "h" }, +--- }, +--- ["Leave corpnet"] = { +--- fn = function() +--- spoon.WiFiTransitions:processTransition('foo', 'corpnet01') +--- end, +--- }, +--- ["Arrive in corpnet"] = { +--- fn = function() +--- spoon.WiFiTransitions:processTransition('corpnet01', 'foo') +--- end, +--- }, +--- ["Translate using Leo"] = { +--- url = "http://dict.leo.org/ende/index_de.html#/search=${query}", +--- icon = 'favicon', +--- keyword = "leo", +--- }, +--- ["Tell me something"] = { +--- keyword = "tellme", +--- fn = function(str) hs.alert.show(str) end, +--- } +--- ``` +obj.actions = {} + +--- Seal.plugins.useractions.get_favicon +--- Variable +--- +--- If `true`, attempt to obtain the favicon for URLs added through the `add` command, and use it in the chooser. Defaults to `true` +obj.get_favicon = true + +-- Internal functions for storing/retrieving bookmarks in the settings database. +local getSetting = function(label, default) return hs.settings.get(obj.__name.."."..label) or default end +local setSetting = function(label, value) hs.settings.set(obj.__name.."."..label, value); return value end + +-- Internal variable where the dynamically-added bookmarks are kept +obj.stored_actions = getSetting('stored_actions', {}) + +-- Internal variable where the merged list of bookmarks/actions is kept +obj.all_actions = nil + +function update_all_actions() + if (obj.all_actions == nil) then + obj.all_actions = {} + for k,v in pairs(obj.actions) do obj.all_actions[k] = hs.fnutils.copy(v) end + for k,v in pairs(obj.stored_actions) do + obj.all_actions[k] = hs.fnutils.copy(v) + if v.encoded_icon then + obj.all_actions[k].icon = hs.image.imageFromURL(v.encoded_icon) + end + end + end +end + +function obj:commands() + local cmds={ + add = { + cmd = "add", + fn = obj.choicesAddURLCommand, + name = "Add URL", + description = "Add URL to bookmarks", + plugin = obj.__name + }, + del = { + cmd = "del", + fn = obj.choicesDelURLCommand, + name = "Delete URL", + description = "Delete URL from bookmarks", + plugin = obj.__name + } + } + local hotkeys_def = {} + local hotkeys_map = {} + local any_hotkeys = false + for k,v in pairs(self.actions or {}) do + if v.keyword and (not cmds[v.keyword]) then + if v.url ~= nil and v.icon == 'favicon' then + v.icon = obj.favIcon(v.url) + end + cmds[v.keyword] = { + cmd = v.keyword, + fn = hs.fnutils.partial(obj.choicesActionKeyword, k, v), + name = k, + icon = v.icon, + plugin = obj.__name + } + end + if v.hotkey then + local choice = obj.buildChoice(k,v) + hotkeys_def[k] = hs.fnutils.partial(obj.completionCallback, choice) + hotkeys_map[k] = v.hotkey + any_hotkeys = true + end + end + if any_hotkeys then + hs.spoons.bindHotkeysToSpec(hotkeys_def, hotkeys_map) + end + return cmds +end + +function obj:bare() + return self.bareActions +end + +function obj.buildChoice(action, v) + local icon, kind + local choice=nil + if type(v) == 'table' then + if v.fn then + kind = 'runFunction' + elseif v.url then + kind = 'openURL' + if v.icon == 'favicon' then + v.icon = obj.favIcon(v.url) + end + end + icon = v.icon or obj.default_icon + choice = {} + choice.text = action + choice.type = kind + choice.plugin = obj.__name + choice.image = icon + if v.description then + choice.subText = v.description + end + end + return choice +end + +function obj.bareActions(query) + local choices = {} + if query == nil or query == "" then + return choices + end + + update_all_actions() + obj.seal:refreshCommandsForPlugin(obj.__basename) + + for action,v in pairs(obj.all_actions) do + if string.match(action:lower(), query:lower()) then + local choice = obj.buildChoice(action, v) + if choice then + table.insert(choices, choice) + end + end + end + + return choices +end + +function obj.favIcon(url) + local query=string.format("http://www.google.com/s2/favicons?sz=64&domain_url=%s", hs.http.encodeForQuery(url)) + return hs.image.imageFromURL(query) +end + +function obj.choicesAddURLCommand(query) + local choices = {} + if query == ".*" then + query = " " + end + local url,name = string.match(query, "([^%s]+)%s+(.*)") + local subtext = "" + if url then + subtext = string.format("New bookmark '%s' pointing to %s", name,url) + end + local choice = { + text = "add " .. query, + subText = subtext, + url = url, + name = name, + plugin = obj.__name, + type = 'addURL', + } + table.insert(choices, choice) + return choices +end + +function obj.choicesDelURLCommand(query) + local choices = {} + for k,v in pairs(obj.stored_actions) do + if string.match(k:lower(), query:lower()) or string.match(v.url:lower(), query:lower()) then + local choice = { + text = string.format("delete '%s'", k), + subText = v.url, + delKey = k, + plugin = obj.__name, + type = 'delURL', + } + if v.encoded_icon then + choice.image = hs.image.imageFromURL(v.encoded_icon) + end + table.insert(choices, choice) + end + end + return choices +end + +function obj.choicesActionKeyword(action, def, query) + local choices = {} + if query == ".*" then + query = "" + end + local choice = { + text = def.keyword .. " " .. query, + subText = def.description or action, + actionname = action, + arg = query, + plugin = obj.__name, + image = def.icon, + type = 'invokeKeyword', + } + table.insert(choices, choice) + return choices +end + +function obj.openURL(url) + hs.execute(string.format("/usr/bin/open '%s'", url)) +end + +function obj.completionCallback(row) + update_all_actions() + if row.type == 'runFunction' then + local fn = obj.all_actions[row.text].fn + fn() + elseif row.type == 'openURL' then + local url = obj.all_actions[row.text].url + obj.openURL(url) + elseif row.type == 'addURL' then + obj.stored_actions[row.name] = { url = row.url } + obj.all_actions = nil + if obj.get_favicon then + local ico=obj.favIcon(row.url) + if ico then + obj.stored_actions[row.name]['encoded_icon'] = ico:encodeAsURLString() + end + end + setSetting('stored_actions', obj.stored_actions) + elseif row.type == 'delURL' then + obj.stored_actions[row.delKey] = nil + obj.all_actions = nil + setSetting('stored_actions', obj.stored_actions) + elseif row.type == 'invokeKeyword' then + if obj.actions[row.actionname].fn then + obj.actions[row.actionname].fn(row.arg) + elseif obj.actions[row.actionname].url then + row.arg = hs.http.encodeForQuery(row.arg) + local query = row.arg:gsub("%%", "%%%%") + local url = string.gsub(obj.actions[row.actionname].url, '${query}', query) + obj.openURL(url) + end + end +end + +return obj diff --git a/Spoons/Seal.spoon/seal_viscosity.lua b/Spoons/Seal.spoon/seal_viscosity.lua new file mode 100644 index 0000000..691a8d6 --- /dev/null +++ b/Spoons/Seal.spoon/seal_viscosity.lua @@ -0,0 +1,266 @@ +local obj = {} +obj.__index = obj +obj.__name = "seal_vpn" + +function obj:commands() + return {vpn = { + cmd = "vpn", + fn = obj.choicesVPNCommand, + name = "VPN", + description = "Manage VPN connections", + plugin = obj.__name + } + } +end + +function obj:bare() + return nil +end + +function obj.getVPNConnections() + connections = {} + code, output, descriptor = hs.osascript.applescript([[ + set output to "" + +on join_list(the_list, delimiter) + set the_string to "" + set old_delims to AppleScript's text item delimiters + repeat with the_item in the_list + if the_string is equal to "" then + set the_string to the_string & the_item + else + set the_string to the_string & delimiter & the_item + end if + end repeat + set AppleScript's text item delimiters to old_delims + return the_string +end join_list + +set vpn_connections to {} + +tell application "Viscosity" + repeat with the_connection in connections + set the end of vpn_connections to (name of the_connection) & tab & (state of the_connection) & tab & "Viscosity" + end repeat +end tell + +tell application "System Events" + tell current location of network preferences + repeat with vpn in (every service whose (kind is greater than 10 and kind is less than 17)) + set state to "Disconnected" + if connected of current configuration of vpn is equal to true then + set state to "Connected" + end if + set the end of vpn_connections to (name of vpn) & tab & state & tab & "macOS" + end repeat + end tell +end tell + +return my join_list(vpn_connections, linefeed) + ]]) + + if code == false or output == nil or output == "" then + return connections + end + + for line in output:gmatch("[^\r\n]+") do + parts = {} + for part in line:gmatch("%S+") do + table.insert(parts, part) + end + kind = parts[#parts] + table.remove(parts, #parts) + state = parts[#parts] + table.remove(parts, #parts) + name = table.concat(parts, " ") + table.insert(connections, {name=name, state=state, kind=kind}) + end + return connections +end + +function obj.disconnectVPN(name, kind) + if kind == "Viscosity" then + obj.disconnectViscosity(name) + elseif kind == "macOS" then + obj.disconnectMacOS(name) + end +end + +function obj.disconnectViscosity(name) + code, output, descriptor = hs.osascript.applescript(string.format([[ + -- Return true if VPN is active + on vpn_is_active(vpn_name) + tell application "Viscosity" + repeat with the_connection in connections + if (name of the_connection) is equal to vpn_name then + if (state of the_connection) is equal to "Connected" then + return true + else + return false + end if + end if + end repeat + end tell + return false + end vpn_is_active + + on run argv + set vpn_name to "%s" + if my vpn_is_active(vpn_name) then + tell application "Viscosity" + disconnect vpn_name + end tell + end if + end run + ]], name)) +end + +function obj.disconnectMacOS(name) + code, output, descriptor = hs.osascript.applescript(string.format([[ + -- Return true if VPN is active + on vpn_is_active(vpn_name) + tell application "System Events" + tell current location of network preferences + return connected of current configuration of service vpn_name + end tell + end tell + end vpn_is_active + + on connect_vpn(vpn_name) + tell application "System Events" + tell current location of network preferences + connect service vpn_name + end tell + end tell + end connect_vpn + + on run argv + set vpn_name to "%s" + if my vpn_is_active(vpn_name) then + tell application "System Events" + tell current location of network preferences + disconnect service vpn_name + end tell + end tell + end if + end run + ]], name)) +end + +function obj.connectVPN(name, kind) + if kind == "Viscosity" then + obj.connectViscosity(name) + elseif kind == "macOS" then + obj.connectMacOS(name) + end +end + +function obj.connectViscosity(name) + code, output, descriptor = hs.osascript.applescript(string.format([[ + -- Return true if VPN is active + on vpn_is_active(vpn_name) + tell application "Viscosity" + repeat with the_connection in connections + if (name of the_connection) is equal to vpn_name then + if (state of the_connection) is equal to "Connected" then + return true + else + return false + end if + end if + end repeat + end tell + return false + end vpn_is_active + + -- Connect to specified VPN + on connect_vpn(vpn_name) + tell application "Viscosity" + connect vpn_name + end tell + end connect_vpn + + on run argv + set vpn_name to "%s" + if my vpn_is_active(vpn_name) then + log "VPN " & quote & vpn_name & quote & " is already active." + return + end if + connect_vpn(vpn_name) + end run + ]], name)) +end + +function obj.connectMacOS(name) + code, output, descriptor = hs.osascript.applescript(string.format([[ + -- Return true if VPN is active + on vpn_is_active(vpn_name) + tell application "System Events" + tell current location of network preferences + return connected of current configuration of service vpn_name + end tell + end tell + end vpn_is_active + + -- Connect to specified VPN + on connect_vpn(vpn_name) + tell application "System Events" + tell current location of network preferences + connect service vpn_name + end tell + end tell + end connect_vpn + + on run argv + set vpn_name to "%s" + if my vpn_is_active(vpn_name) then + log "VPN " & quote & vpn_name & quote & " is already active." + return + end if + connect_vpn(vpn_name) + end run + ]], name)) +end + + +function obj.choicesVPNCommand(query) + local choices = {} + local connections = obj.getVPNConnections() + local img_connected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_locked.png") + local img_disconnected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_unlocked.png") + + for k,v in pairs(connections) do + name = v["name"] + if string.match(name:lower(), query:lower()) then + state = v["state"] + kind = v["kind"] + local choice = {} + choice["text"] = name + choice["subText"] = state .. " (" .. kind .. ")" + if state == "Connected" then + choice["image"] = img_connected + else + choice["image"] = img_disconnected + end + choice["name"] = name + choice["state"] = state + choice["kind"] = kind + choice["plugin"] = obj.__name + choice["type"] = "toggle" + table.insert(choices, choice) + end + end + return choices +end + +function obj.completionCallback(rowInfo) + if rowInfo["type"] == "toggle" then + if rowInfo["state"] == "Connected" then + obj.disconnectVPN(rowInfo["name"], rowInfo["kind"]) + else + obj.connectVPN(rowInfo["name"], rowInfo["kind"]) + end + end +end + +return obj diff --git a/Spoons/Seal.spoon/seal_vpn.lua b/Spoons/Seal.spoon/seal_vpn.lua new file mode 100644 index 0000000..691a8d6 --- /dev/null +++ b/Spoons/Seal.spoon/seal_vpn.lua @@ -0,0 +1,266 @@ +local obj = {} +obj.__index = obj +obj.__name = "seal_vpn" + +function obj:commands() + return {vpn = { + cmd = "vpn", + fn = obj.choicesVPNCommand, + name = "VPN", + description = "Manage VPN connections", + plugin = obj.__name + } + } +end + +function obj:bare() + return nil +end + +function obj.getVPNConnections() + connections = {} + code, output, descriptor = hs.osascript.applescript([[ + set output to "" + +on join_list(the_list, delimiter) + set the_string to "" + set old_delims to AppleScript's text item delimiters + repeat with the_item in the_list + if the_string is equal to "" then + set the_string to the_string & the_item + else + set the_string to the_string & delimiter & the_item + end if + end repeat + set AppleScript's text item delimiters to old_delims + return the_string +end join_list + +set vpn_connections to {} + +tell application "Viscosity" + repeat with the_connection in connections + set the end of vpn_connections to (name of the_connection) & tab & (state of the_connection) & tab & "Viscosity" + end repeat +end tell + +tell application "System Events" + tell current location of network preferences + repeat with vpn in (every service whose (kind is greater than 10 and kind is less than 17)) + set state to "Disconnected" + if connected of current configuration of vpn is equal to true then + set state to "Connected" + end if + set the end of vpn_connections to (name of vpn) & tab & state & tab & "macOS" + end repeat + end tell +end tell + +return my join_list(vpn_connections, linefeed) + ]]) + + if code == false or output == nil or output == "" then + return connections + end + + for line in output:gmatch("[^\r\n]+") do + parts = {} + for part in line:gmatch("%S+") do + table.insert(parts, part) + end + kind = parts[#parts] + table.remove(parts, #parts) + state = parts[#parts] + table.remove(parts, #parts) + name = table.concat(parts, " ") + table.insert(connections, {name=name, state=state, kind=kind}) + end + return connections +end + +function obj.disconnectVPN(name, kind) + if kind == "Viscosity" then + obj.disconnectViscosity(name) + elseif kind == "macOS" then + obj.disconnectMacOS(name) + end +end + +function obj.disconnectViscosity(name) + code, output, descriptor = hs.osascript.applescript(string.format([[ + -- Return true if VPN is active + on vpn_is_active(vpn_name) + tell application "Viscosity" + repeat with the_connection in connections + if (name of the_connection) is equal to vpn_name then + if (state of the_connection) is equal to "Connected" then + return true + else + return false + end if + end if + end repeat + end tell + return false + end vpn_is_active + + on run argv + set vpn_name to "%s" + if my vpn_is_active(vpn_name) then + tell application "Viscosity" + disconnect vpn_name + end tell + end if + end run + ]], name)) +end + +function obj.disconnectMacOS(name) + code, output, descriptor = hs.osascript.applescript(string.format([[ + -- Return true if VPN is active + on vpn_is_active(vpn_name) + tell application "System Events" + tell current location of network preferences + return connected of current configuration of service vpn_name + end tell + end tell + end vpn_is_active + + on connect_vpn(vpn_name) + tell application "System Events" + tell current location of network preferences + connect service vpn_name + end tell + end tell + end connect_vpn + + on run argv + set vpn_name to "%s" + if my vpn_is_active(vpn_name) then + tell application "System Events" + tell current location of network preferences + disconnect service vpn_name + end tell + end tell + end if + end run + ]], name)) +end + +function obj.connectVPN(name, kind) + if kind == "Viscosity" then + obj.connectViscosity(name) + elseif kind == "macOS" then + obj.connectMacOS(name) + end +end + +function obj.connectViscosity(name) + code, output, descriptor = hs.osascript.applescript(string.format([[ + -- Return true if VPN is active + on vpn_is_active(vpn_name) + tell application "Viscosity" + repeat with the_connection in connections + if (name of the_connection) is equal to vpn_name then + if (state of the_connection) is equal to "Connected" then + return true + else + return false + end if + end if + end repeat + end tell + return false + end vpn_is_active + + -- Connect to specified VPN + on connect_vpn(vpn_name) + tell application "Viscosity" + connect vpn_name + end tell + end connect_vpn + + on run argv + set vpn_name to "%s" + if my vpn_is_active(vpn_name) then + log "VPN " & quote & vpn_name & quote & " is already active." + return + end if + connect_vpn(vpn_name) + end run + ]], name)) +end + +function obj.connectMacOS(name) + code, output, descriptor = hs.osascript.applescript(string.format([[ + -- Return true if VPN is active + on vpn_is_active(vpn_name) + tell application "System Events" + tell current location of network preferences + return connected of current configuration of service vpn_name + end tell + end tell + end vpn_is_active + + -- Connect to specified VPN + on connect_vpn(vpn_name) + tell application "System Events" + tell current location of network preferences + connect service vpn_name + end tell + end tell + end connect_vpn + + on run argv + set vpn_name to "%s" + if my vpn_is_active(vpn_name) then + log "VPN " & quote & vpn_name & quote & " is already active." + return + end if + connect_vpn(vpn_name) + end run + ]], name)) +end + + +function obj.choicesVPNCommand(query) + local choices = {} + local connections = obj.getVPNConnections() + local img_connected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_locked.png") + local img_disconnected = hs.image.imageFromPath(obj.seal.spoonPath.."/viscosity_unlocked.png") + + for k,v in pairs(connections) do + name = v["name"] + if string.match(name:lower(), query:lower()) then + state = v["state"] + kind = v["kind"] + local choice = {} + choice["text"] = name + choice["subText"] = state .. " (" .. kind .. ")" + if state == "Connected" then + choice["image"] = img_connected + else + choice["image"] = img_disconnected + end + choice["name"] = name + choice["state"] = state + choice["kind"] = kind + choice["plugin"] = obj.__name + choice["type"] = "toggle" + table.insert(choices, choice) + end + end + return choices +end + +function obj.completionCallback(rowInfo) + if rowInfo["type"] == "toggle" then + if rowInfo["state"] == "Connected" then + obj.disconnectVPN(rowInfo["name"], rowInfo["kind"]) + else + obj.connectVPN(rowInfo["name"], rowInfo["kind"]) + end + end +end + +return obj diff --git a/Spoons/Seal.spoon/viscosity_locked.png b/Spoons/Seal.spoon/viscosity_locked.png new file mode 100644 index 0000000..6a9483c Binary files /dev/null and b/Spoons/Seal.spoon/viscosity_locked.png differ diff --git a/Spoons/Seal.spoon/viscosity_unlocked.png b/Spoons/Seal.spoon/viscosity_unlocked.png new file mode 100644 index 0000000..8596f7c Binary files /dev/null and b/Spoons/Seal.spoon/viscosity_unlocked.png differ diff --git a/Spoons/SpeedMenu.spoon/docs.json b/Spoons/SpeedMenu.spoon/docs.json new file mode 100644 index 0000000..f57b965 --- /dev/null +++ b/Spoons/SpeedMenu.spoon/docs.json @@ -0,0 +1,39 @@ +[ + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [ + { + "def": "SpeedMenu:rescan()", + "desc": "Redetect the active interface, darkmode …And redraw everything.", + "doc": "Redetect the active interface, darkmode …And redraw everything.\n", + "name": "rescan", + "signature": "SpeedMenu:rescan()", + "stripped_doc": "", + "type": "Method" + } + ], + "Variable": [], + "desc": "Menubar netspeed meter", + "doc": "Menubar netspeed meter\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip)", + "items": [ + { + "def": "SpeedMenu:rescan()", + "desc": "Redetect the active interface, darkmode …And redraw everything.", + "doc": "Redetect the active interface, darkmode …And redraw everything.\n", + "name": "rescan", + "signature": "SpeedMenu:rescan()", + "stripped_doc": "", + "type": "Method" + } + ], + "name": "SpeedMenu", + "stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip)", + "submodules": [], + "type": "Module" + } +] \ No newline at end of file diff --git a/Spoons/SpeedMenu.spoon/init.lua b/Spoons/SpeedMenu.spoon/init.lua new file mode 100644 index 0000000..8b2f298 --- /dev/null +++ b/Spoons/SpeedMenu.spoon/init.lua @@ -0,0 +1,128 @@ +--- === SpeedMenu === +--- +--- Menubar netspeed meter +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpeedMenu.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "SpeedMenu" +obj.version = "1.0" +obj.author = "ashfinal " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +function obj:init() + self.menubar = hs.menubar.new(false) +end + +function obj:start() + obj.menubar:returnToMenuBar() + obj:rescan() +end + +function obj:stop() + obj.menubar:removeFromMenuBar() + obj.timer:stop() +end + +function obj:toggle() + if obj.timer:running() then + obj:stop() + else + obj:start() + end +end + +local function data_diff() + local in_seq = hs.execute(obj.instr) + local out_seq = hs.execute(obj.outstr) + local in_diff = in_seq - obj.inseq + local out_diff = out_seq - obj.outseq + if in_diff/1024 > 1024 then + obj.kbin = string.format("%6.2f", in_diff/1024/1024) .. ' mb/s' + else + obj.kbin = string.format("%6.2f", in_diff/1024) .. ' kb/s' + end + if out_diff/1024 > 1024 then + obj.kbout = string.format("%6.2f", out_diff/1024/1024) .. ' mb/s' + else + obj.kbout = string.format("%6.2f", out_diff/1024) .. ' kb/s' + end + local disp_str = '⥄ ' .. obj.kbout .. '\n⥂ ' .. obj.kbin + if obj.darkmode then + obj.disp_str = hs.styledtext.new(disp_str, {font={size=9.0, color={hex="#FFFFFF"}}}) + else + obj.disp_str = hs.styledtext.new(disp_str, {font={size=9.0, color={hex="#000000"}}}) + end + obj.menubar:setTitle(obj.disp_str) + obj.inseq = in_seq + obj.outseq = out_seq +end + +--- SpeedMenu:rescan() +--- Method +--- Redetect the active interface, darkmode …And redraw everything. +--- + +function obj:rescan() + obj.interface = hs.network.primaryInterfaces() + obj.darkmode = hs.osascript.applescript('tell application "System Events"\nreturn dark mode of appearance preferences\nend tell') + local menuitems_table = {} + if obj.interface then + -- Inspect active interface and create menuitems + local interface_detail = hs.network.interfaceDetails(obj.interface) + if interface_detail.AirPort then + local ssid = interface_detail.AirPort.SSID + table.insert(menuitems_table, { + title = "SSID: " .. ssid, + tooltip = "Copy SSID to clipboard", + fn = function() hs.pasteboard.setContents(ssid) end + }) + end + if interface_detail.IPv4 then + local ipv4 = interface_detail.IPv4.Addresses[1] + table.insert(menuitems_table, { + title = "IPv4: " .. ipv4, + tooltip = "Copy IPv4 to clipboard", + fn = function() hs.pasteboard.setContents(ipv4) end + }) + end + if interface_detail.IPv6 then + local ipv6 = interface_detail.IPv6.Addresses[1] + table.insert(menuitems_table, { + title = "IPv6: " .. ipv6, + tooltip = "Copy IPv6 to clipboard", + fn = function() hs.pasteboard.setContents(ipv6) end + }) + end + local macaddr = hs.execute('ifconfig ' .. obj.interface .. ' | grep ether | awk \'{print $2}\'') + table.insert(menuitems_table, { + title = "MAC Addr: " .. macaddr, + tooltip = "Copy MAC Address to clipboard", + fn = function() hs.pasteboard.setContents(macaddr) end + }) + -- Start watching the netspeed delta + obj.instr = 'netstat -ibn | grep -e ' .. obj.interface .. ' -m 1 | awk \'{print $7}\'' + obj.outstr = 'netstat -ibn | grep -e ' .. obj.interface .. ' -m 1 | awk \'{print $10}\'' + + obj.inseq = hs.execute(obj.instr) + obj.outseq = hs.execute(obj.outstr) + + if obj.timer then + obj.timer:stop() + obj.timer = nil + end + obj.timer = hs.timer.doEvery(1, data_diff) + end + table.insert(menuitems_table, { + title = "Rescan Network Interfaces", + fn = function() obj:rescan() end + }) + obj.menubar:setTitle("⚠︎") + obj.menubar:setMenu(menuitems_table) +end + +return obj diff --git a/Spoons/SpoonInstall.spoon/docs.json b/Spoons/SpoonInstall.spoon/docs.json new file mode 100644 index 0000000..29005e6 --- /dev/null +++ b/Spoons/SpoonInstall.spoon/docs.json @@ -0,0 +1,478 @@ +[ + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [ + { + "def": "SpoonInstall:andUse(name, arg)", + "desc": "Declaratively install, load and configure a Spoon", + "doc": "Declaratively install, load and configure a Spoon\n\nParameters:\n * name - the name of the Spoon to install (without the `.spoon` extension). If the Spoon is already installed, it will be loaded using `hs.loadSpoon()`. If it is not installed, it will be installed using `SpoonInstall:asyncInstallSpoonFromRepo()` and then loaded.\n * arg - if provided, can be used to specify the configuration of the Spoon. The following keys are recognized (all are optional):\n * repo - repository from where the Spoon should be installed if not present in the system, as defined in `SpoonInstall.repos`. Defaults to `\"default\"`.\n * config - a table containing variables to be stored in the Spoon object to configure it. For example, `config = { answer = 42 }` will result in `spoon..answer` being set to 42.\n * hotkeys - a table containing hotkey bindings. If provided, will be passed as-is to the Spoon's `bindHotkeys()` method. The special string `\"default\"` can be given to use the Spoons `defaultHotkeys` variable, if it exists.\n * fn - a function which will be called with the freshly-loaded Spoon object as its first argument.\n * loglevel - if the Spoon has a variable called `logger`, its `setLogLevel()` method will be called with this value.\n * start - if `true`, call the Spoon's `start()` method after configuring everything else.\n * disable - if `true`, do nothing. Easier than commenting it out when you want to temporarily disable a spoon.\n\nReturns:\n * None", + "name": "andUse", + "parameters": [ + " * name - the name of the Spoon to install (without the `.spoon` extension). If the Spoon is already installed, it will be loaded using `hs.loadSpoon()`. If it is not installed, it will be installed using `SpoonInstall:asyncInstallSpoonFromRepo()` and then loaded.", + " * arg - if provided, can be used to specify the configuration of the Spoon. The following keys are recognized (all are optional):", + " * repo - repository from where the Spoon should be installed if not present in the system, as defined in `SpoonInstall.repos`. Defaults to `\"default\"`.", + " * config - a table containing variables to be stored in the Spoon object to configure it. For example, `config = { answer = 42 }` will result in `spoon..answer` being set to 42.", + " * hotkeys - a table containing hotkey bindings. If provided, will be passed as-is to the Spoon's `bindHotkeys()` method. The special string `\"default\"` can be given to use the Spoons `defaultHotkeys` variable, if it exists.", + " * fn - a function which will be called with the freshly-loaded Spoon object as its first argument.", + " * loglevel - if the Spoon has a variable called `logger`, its `setLogLevel()` method will be called with this value.", + " * start - if `true`, call the Spoon's `start()` method after configuring everything else.", + " * disable - if `true`, do nothing. Easier than commenting it out when you want to temporarily disable a spoon." + ], + "returns": [ + " * None" + ], + "signature": "SpoonInstall:andUse(name, arg)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:asyncInstallSpoonFromRepo(name, repo, callback)", + "desc": "Asynchronously install a Spoon from a registered repository", + "doc": "Asynchronously install a Spoon from a registered repository\n\nParameters:\n * name - Name of the Spoon to install.\n * repo - Name of the repository to use. Defaults to `\"default\"`\n * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:\n * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file\n * success - boolean indicating whether the installation was successful\n\nReturns:\n * `true` if the installation was correctly initiated (i.e. the repo and spoon name were correct), `false` otherwise.", + "name": "asyncInstallSpoonFromRepo", + "parameters": [ + " * name - Name of the Spoon to install.", + " * repo - Name of the repository to use. Defaults to `\"default\"`", + " * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:", + " * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file", + " * success - boolean indicating whether the installation was successful" + ], + "returns": [ + " * `true` if the installation was correctly initiated (i.e. the repo and spoon name were correct), `false` otherwise." + ], + "signature": "SpoonInstall:asyncInstallSpoonFromRepo(name, repo, callback)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:asyncInstallSpoonFromZipURL(url, callback)", + "desc": "Asynchronously download a Spoon zip file and install it.", + "doc": "Asynchronously download a Spoon zip file and install it.\n\nParameters:\n * url - URL of the zip file to install.\n * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:\n * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file\n * success - boolean indicating whether the installation was successful\n\nReturns:\n * `true` if the installation was correctly initiated (i.e. the URL is valid), `false` otherwise", + "name": "asyncInstallSpoonFromZipURL", + "parameters": [ + " * url - URL of the zip file to install.", + " * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:", + " * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file", + " * success - boolean indicating whether the installation was successful" + ], + "returns": [ + " * `true` if the installation was correctly initiated (i.e. the URL is valid), `false` otherwise" + ], + "signature": "SpoonInstall:asyncInstallSpoonFromZipURL(url, callback)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:asyncUpdateAllRepos()", + "desc": "Asynchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`", + "doc": "Asynchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`\n\nParameters:\n * None\n\nReturns:\n * None\n\nNotes:\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.", + "name": "asyncUpdateAllRepos", + "notes": [ + " * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions." + ], + "parameters": [ + " * None" + ], + "returns": [ + " * None" + ], + "signature": "SpoonInstall:asyncUpdateAllRepos()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:asyncUpdateRepo(repo, callback)", + "desc": "Asynchronously fetch the information about the contents of a Spoon repository", + "doc": "Asynchronously fetch the information about the contents of a Spoon repository\n\nParameters:\n * repo - name of the repository to update. Defaults to `\"default\"`.\n * callback - if given, a function to be called after the update finishes (also if it fails). The function will receive the following arguments:\n * repo - name of the repository\n * success - boolean indicating whether the update succeeded\n\nReturns:\n * `true` if the update was correctly initiated (i.e. the repo name is valid), `nil` otherwise\n\nNotes:\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.", + "name": "asyncUpdateRepo", + "notes": [ + " * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions." + ], + "parameters": [ + " * repo - name of the repository to update. Defaults to `\"default\"`.", + " * callback - if given, a function to be called after the update finishes (also if it fails). The function will receive the following arguments:", + " * repo - name of the repository", + " * success - boolean indicating whether the update succeeded" + ], + "returns": [ + " * `true` if the update was correctly initiated (i.e. the repo name is valid), `nil` otherwise" + ], + "signature": "SpoonInstall:asyncUpdateRepo(repo, callback)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:installSpoonFromRepo(name, repo)", + "desc": "Synchronously install a Spoon from a registered repository", + "doc": "Synchronously install a Spoon from a registered repository\n\nParameters:\n * name = Name of the Spoon to install.\n * repo - Name of the repository to use. Defaults to `\"default\"`\n\nReturns:\n * `true` if the installation was successful, `nil` otherwise.", + "name": "installSpoonFromRepo", + "parameters": [ + " * name = Name of the Spoon to install.", + " * repo - Name of the repository to use. Defaults to `\"default\"`" + ], + "returns": [ + " * `true` if the installation was successful, `nil` otherwise." + ], + "signature": "SpoonInstall:installSpoonFromRepo(name, repo)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:installSpoonFromZipURL(url)", + "desc": "Synchronously download a Spoon zip file and install it.", + "doc": "Synchronously download a Spoon zip file and install it.\n\nParameters:\n * url - URL of the zip file to install.\n\nReturns:\n * `true` if the installation was successful, `nil` otherwise", + "name": "installSpoonFromZipURL", + "parameters": [ + " * url - URL of the zip file to install." + ], + "returns": [ + " * `true` if the installation was successful, `nil` otherwise" + ], + "signature": "SpoonInstall:installSpoonFromZipURL(url)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:repolist()", + "desc": "Return a sorted list of registered Spoon repositories", + "doc": "Return a sorted list of registered Spoon repositories\n\nParameters:\n * None\n\nReturns:\n * Table containing a list of strings with the repository identifiers", + "name": "repolist", + "parameters": [ + " * None" + ], + "returns": [ + " * Table containing a list of strings with the repository identifiers" + ], + "signature": "SpoonInstall:repolist()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:search(pat)", + "desc": "Search repositories for a pattern", + "doc": "Search repositories for a pattern\n\nParameters:\n * pat - Lua pattern that will be matched against the name and description of each spoon in the registered repositories. All text is converted to lowercase before searching it, so you can use all-lowercase in your pattern.\n\nReturns:\n * Table containing a list of matching entries. Each entry is a table with the following keys:\n * name - Spoon name\n * desc - description of the spoon\n * repo - identifier in the repository where the match was found", + "name": "search", + "parameters": [ + " * pat - Lua pattern that will be matched against the name and description of each spoon in the registered repositories. All text is converted to lowercase before searching it, so you can use all-lowercase in your pattern." + ], + "returns": [ + " * Table containing a list of matching entries. Each entry is a table with the following keys:", + " * name - Spoon name", + " * desc - description of the spoon", + " * repo - identifier in the repository where the match was found" + ], + "signature": "SpoonInstall:search(pat)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:updateAllRepos()", + "desc": "Synchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`", + "doc": "Synchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`\n\nParameters:\n * None\n\nReturns:\n * None\n\nNotes:\n * This is a synchronous call, which means Hammerspoon will be blocked until it finishes.\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.", + "name": "updateAllRepos", + "notes": [ + " * This is a synchronous call, which means Hammerspoon will be blocked until it finishes.", + " * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions." + ], + "parameters": [ + " * None" + ], + "returns": [ + " * None" + ], + "signature": "SpoonInstall:updateAllRepos()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:updateRepo(repo)", + "desc": "Synchronously fetch the information about the contents of a Spoon repository", + "doc": "Synchronously fetch the information about the contents of a Spoon repository\n\nParameters:\n * repo - name of the repository to update. Defaults to `\"default\"`.\n\nReturns:\n * `true` if the update was successful, `nil` otherwise\n\nNotes:\n * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. For use in your configuration files, it's advisable to use `SpoonInstall.asyncUpdateRepo()` instead.\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.", + "name": "updateRepo", + "notes": [ + " * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. For use in your configuration files, it's advisable to use `SpoonInstall.asyncUpdateRepo()` instead.", + " * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions." + ], + "parameters": [ + " * repo - name of the repository to update. Defaults to `\"default\"`." + ], + "returns": [ + " * `true` if the update was successful, `nil` otherwise" + ], + "signature": "SpoonInstall:updateRepo(repo)", + "stripped_doc": "", + "type": "Method" + } + ], + "Variable": [ + { + "def": "SpoonInstall.logger", + "desc": "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.", + "doc": "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.", + "name": "logger", + "signature": "SpoonInstall.logger", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "SpoonInstall.repos", + "desc": "Table containing the list of available Spoon repositories. The key", + "doc": "Table containing the list of available Spoon repositories. The key\nof each entry is an identifier for the repository, and its value\nis a table with the following entries:\n * desc - Human-readable description for the repository\n * branch - Active git branch for the Spoon files\n * url - Base URL for the repository. For now the repository is assumed to be hosted in GitHub, and the URL should be the main base URL of the repository. Repository metadata needs to be stored under `docs/docs.json`, and the Spoon zip files need to be stored under `Spoons/`.\n\nDefault value:\n```\n{\n default = {\n url = \"https://github.com/Hammerspoon/Spoons\",\n desc = \"Main Hammerspoon Spoon repository\",\n branch = \"master\",\n }\n}\n```", + "name": "repos", + "signature": "SpoonInstall.repos", + "stripped_doc": "of each entry is an identifier for the repository, and its value\nis a table with the following entries:\n * desc - Human-readable description for the repository\n * branch - Active git branch for the Spoon files\n * url - Base URL for the repository. For now the repository is assumed to be hosted in GitHub, and the URL should be the main base URL of the repository. Repository metadata needs to be stored under `docs/docs.json`, and the Spoon zip files need to be stored under `Spoons/`.\nDefault value:\n```\n{\n default = {\n url = \"https://github.com/Hammerspoon/Spoons\",\n desc = \"Main Hammerspoon Spoon repository\",\n branch = \"master\",\n }\n}\n```", + "type": "Variable" + }, + { + "def": "SpoonInstall.use_syncinstall", + "desc": "If `true`, `andUse()` will update repos and install packages synchronously. Defaults to `false`.", + "doc": "If `true`, `andUse()` will update repos and install packages synchronously. Defaults to `false`.\n\nKeep in mind that if you set this to `true`, Hammerspoon will\nblock until all missing Spoons are installed, but the notifications\nwill happen at a more \"human readable\" rate.", + "name": "use_syncinstall", + "signature": "SpoonInstall.use_syncinstall", + "stripped_doc": "Keep in mind that if you set this to `true`, Hammerspoon will\nblock until all missing Spoons are installed, but the notifications\nwill happen at a more \"human readable\" rate.", + "type": "Variable" + } + ], + "desc": "Install and manage Spoons and Spoon repositories", + "doc": "Install and manage Spoons and Spoon repositories\n\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip)", + "items": [ + { + "def": "SpoonInstall:andUse(name, arg)", + "desc": "Declaratively install, load and configure a Spoon", + "doc": "Declaratively install, load and configure a Spoon\n\nParameters:\n * name - the name of the Spoon to install (without the `.spoon` extension). If the Spoon is already installed, it will be loaded using `hs.loadSpoon()`. If it is not installed, it will be installed using `SpoonInstall:asyncInstallSpoonFromRepo()` and then loaded.\n * arg - if provided, can be used to specify the configuration of the Spoon. The following keys are recognized (all are optional):\n * repo - repository from where the Spoon should be installed if not present in the system, as defined in `SpoonInstall.repos`. Defaults to `\"default\"`.\n * config - a table containing variables to be stored in the Spoon object to configure it. For example, `config = { answer = 42 }` will result in `spoon..answer` being set to 42.\n * hotkeys - a table containing hotkey bindings. If provided, will be passed as-is to the Spoon's `bindHotkeys()` method. The special string `\"default\"` can be given to use the Spoons `defaultHotkeys` variable, if it exists.\n * fn - a function which will be called with the freshly-loaded Spoon object as its first argument.\n * loglevel - if the Spoon has a variable called `logger`, its `setLogLevel()` method will be called with this value.\n * start - if `true`, call the Spoon's `start()` method after configuring everything else.\n * disable - if `true`, do nothing. Easier than commenting it out when you want to temporarily disable a spoon.\n\nReturns:\n * None", + "name": "andUse", + "parameters": [ + " * name - the name of the Spoon to install (without the `.spoon` extension). If the Spoon is already installed, it will be loaded using `hs.loadSpoon()`. If it is not installed, it will be installed using `SpoonInstall:asyncInstallSpoonFromRepo()` and then loaded.", + " * arg - if provided, can be used to specify the configuration of the Spoon. The following keys are recognized (all are optional):", + " * repo - repository from where the Spoon should be installed if not present in the system, as defined in `SpoonInstall.repos`. Defaults to `\"default\"`.", + " * config - a table containing variables to be stored in the Spoon object to configure it. For example, `config = { answer = 42 }` will result in `spoon..answer` being set to 42.", + " * hotkeys - a table containing hotkey bindings. If provided, will be passed as-is to the Spoon's `bindHotkeys()` method. The special string `\"default\"` can be given to use the Spoons `defaultHotkeys` variable, if it exists.", + " * fn - a function which will be called with the freshly-loaded Spoon object as its first argument.", + " * loglevel - if the Spoon has a variable called `logger`, its `setLogLevel()` method will be called with this value.", + " * start - if `true`, call the Spoon's `start()` method after configuring everything else.", + " * disable - if `true`, do nothing. Easier than commenting it out when you want to temporarily disable a spoon." + ], + "returns": [ + " * None" + ], + "signature": "SpoonInstall:andUse(name, arg)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:asyncInstallSpoonFromRepo(name, repo, callback)", + "desc": "Asynchronously install a Spoon from a registered repository", + "doc": "Asynchronously install a Spoon from a registered repository\n\nParameters:\n * name - Name of the Spoon to install.\n * repo - Name of the repository to use. Defaults to `\"default\"`\n * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:\n * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file\n * success - boolean indicating whether the installation was successful\n\nReturns:\n * `true` if the installation was correctly initiated (i.e. the repo and spoon name were correct), `false` otherwise.", + "name": "asyncInstallSpoonFromRepo", + "parameters": [ + " * name - Name of the Spoon to install.", + " * repo - Name of the repository to use. Defaults to `\"default\"`", + " * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:", + " * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file", + " * success - boolean indicating whether the installation was successful" + ], + "returns": [ + " * `true` if the installation was correctly initiated (i.e. the repo and spoon name were correct), `false` otherwise." + ], + "signature": "SpoonInstall:asyncInstallSpoonFromRepo(name, repo, callback)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:asyncInstallSpoonFromZipURL(url, callback)", + "desc": "Asynchronously download a Spoon zip file and install it.", + "doc": "Asynchronously download a Spoon zip file and install it.\n\nParameters:\n * url - URL of the zip file to install.\n * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:\n * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file\n * success - boolean indicating whether the installation was successful\n\nReturns:\n * `true` if the installation was correctly initiated (i.e. the URL is valid), `false` otherwise", + "name": "asyncInstallSpoonFromZipURL", + "parameters": [ + " * url - URL of the zip file to install.", + " * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments:", + " * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file", + " * success - boolean indicating whether the installation was successful" + ], + "returns": [ + " * `true` if the installation was correctly initiated (i.e. the URL is valid), `false` otherwise" + ], + "signature": "SpoonInstall:asyncInstallSpoonFromZipURL(url, callback)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:asyncUpdateAllRepos()", + "desc": "Asynchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`", + "doc": "Asynchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`\n\nParameters:\n * None\n\nReturns:\n * None\n\nNotes:\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.", + "name": "asyncUpdateAllRepos", + "notes": [ + " * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions." + ], + "parameters": [ + " * None" + ], + "returns": [ + " * None" + ], + "signature": "SpoonInstall:asyncUpdateAllRepos()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:asyncUpdateRepo(repo, callback)", + "desc": "Asynchronously fetch the information about the contents of a Spoon repository", + "doc": "Asynchronously fetch the information about the contents of a Spoon repository\n\nParameters:\n * repo - name of the repository to update. Defaults to `\"default\"`.\n * callback - if given, a function to be called after the update finishes (also if it fails). The function will receive the following arguments:\n * repo - name of the repository\n * success - boolean indicating whether the update succeeded\n\nReturns:\n * `true` if the update was correctly initiated (i.e. the repo name is valid), `nil` otherwise\n\nNotes:\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.", + "name": "asyncUpdateRepo", + "notes": [ + " * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions." + ], + "parameters": [ + " * repo - name of the repository to update. Defaults to `\"default\"`.", + " * callback - if given, a function to be called after the update finishes (also if it fails). The function will receive the following arguments:", + " * repo - name of the repository", + " * success - boolean indicating whether the update succeeded" + ], + "returns": [ + " * `true` if the update was correctly initiated (i.e. the repo name is valid), `nil` otherwise" + ], + "signature": "SpoonInstall:asyncUpdateRepo(repo, callback)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:installSpoonFromRepo(name, repo)", + "desc": "Synchronously install a Spoon from a registered repository", + "doc": "Synchronously install a Spoon from a registered repository\n\nParameters:\n * name = Name of the Spoon to install.\n * repo - Name of the repository to use. Defaults to `\"default\"`\n\nReturns:\n * `true` if the installation was successful, `nil` otherwise.", + "name": "installSpoonFromRepo", + "parameters": [ + " * name = Name of the Spoon to install.", + " * repo - Name of the repository to use. Defaults to `\"default\"`" + ], + "returns": [ + " * `true` if the installation was successful, `nil` otherwise." + ], + "signature": "SpoonInstall:installSpoonFromRepo(name, repo)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:installSpoonFromZipURL(url)", + "desc": "Synchronously download a Spoon zip file and install it.", + "doc": "Synchronously download a Spoon zip file and install it.\n\nParameters:\n * url - URL of the zip file to install.\n\nReturns:\n * `true` if the installation was successful, `nil` otherwise", + "name": "installSpoonFromZipURL", + "parameters": [ + " * url - URL of the zip file to install." + ], + "returns": [ + " * `true` if the installation was successful, `nil` otherwise" + ], + "signature": "SpoonInstall:installSpoonFromZipURL(url)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall.logger", + "desc": "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.", + "doc": "Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon.", + "name": "logger", + "signature": "SpoonInstall.logger", + "stripped_doc": "", + "type": "Variable" + }, + { + "def": "SpoonInstall:repolist()", + "desc": "Return a sorted list of registered Spoon repositories", + "doc": "Return a sorted list of registered Spoon repositories\n\nParameters:\n * None\n\nReturns:\n * Table containing a list of strings with the repository identifiers", + "name": "repolist", + "parameters": [ + " * None" + ], + "returns": [ + " * Table containing a list of strings with the repository identifiers" + ], + "signature": "SpoonInstall:repolist()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall.repos", + "desc": "Table containing the list of available Spoon repositories. The key", + "doc": "Table containing the list of available Spoon repositories. The key\nof each entry is an identifier for the repository, and its value\nis a table with the following entries:\n * desc - Human-readable description for the repository\n * branch - Active git branch for the Spoon files\n * url - Base URL for the repository. For now the repository is assumed to be hosted in GitHub, and the URL should be the main base URL of the repository. Repository metadata needs to be stored under `docs/docs.json`, and the Spoon zip files need to be stored under `Spoons/`.\n\nDefault value:\n```\n{\n default = {\n url = \"https://github.com/Hammerspoon/Spoons\",\n desc = \"Main Hammerspoon Spoon repository\",\n branch = \"master\",\n }\n}\n```", + "name": "repos", + "signature": "SpoonInstall.repos", + "stripped_doc": "of each entry is an identifier for the repository, and its value\nis a table with the following entries:\n * desc - Human-readable description for the repository\n * branch - Active git branch for the Spoon files\n * url - Base URL for the repository. For now the repository is assumed to be hosted in GitHub, and the URL should be the main base URL of the repository. Repository metadata needs to be stored under `docs/docs.json`, and the Spoon zip files need to be stored under `Spoons/`.\nDefault value:\n```\n{\n default = {\n url = \"https://github.com/Hammerspoon/Spoons\",\n desc = \"Main Hammerspoon Spoon repository\",\n branch = \"master\",\n }\n}\n```", + "type": "Variable" + }, + { + "def": "SpoonInstall:search(pat)", + "desc": "Search repositories for a pattern", + "doc": "Search repositories for a pattern\n\nParameters:\n * pat - Lua pattern that will be matched against the name and description of each spoon in the registered repositories. All text is converted to lowercase before searching it, so you can use all-lowercase in your pattern.\n\nReturns:\n * Table containing a list of matching entries. Each entry is a table with the following keys:\n * name - Spoon name\n * desc - description of the spoon\n * repo - identifier in the repository where the match was found", + "name": "search", + "parameters": [ + " * pat - Lua pattern that will be matched against the name and description of each spoon in the registered repositories. All text is converted to lowercase before searching it, so you can use all-lowercase in your pattern." + ], + "returns": [ + " * Table containing a list of matching entries. Each entry is a table with the following keys:", + " * name - Spoon name", + " * desc - description of the spoon", + " * repo - identifier in the repository where the match was found" + ], + "signature": "SpoonInstall:search(pat)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:updateAllRepos()", + "desc": "Synchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`", + "doc": "Synchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos`\n\nParameters:\n * None\n\nReturns:\n * None\n\nNotes:\n * This is a synchronous call, which means Hammerspoon will be blocked until it finishes.\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.", + "name": "updateAllRepos", + "notes": [ + " * This is a synchronous call, which means Hammerspoon will be blocked until it finishes.", + " * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions." + ], + "parameters": [ + " * None" + ], + "returns": [ + " * None" + ], + "signature": "SpoonInstall:updateAllRepos()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall:updateRepo(repo)", + "desc": "Synchronously fetch the information about the contents of a Spoon repository", + "doc": "Synchronously fetch the information about the contents of a Spoon repository\n\nParameters:\n * repo - name of the repository to update. Defaults to `\"default\"`.\n\nReturns:\n * `true` if the update was successful, `nil` otherwise\n\nNotes:\n * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. For use in your configuration files, it's advisable to use `SpoonInstall.asyncUpdateRepo()` instead.\n * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions.", + "name": "updateRepo", + "notes": [ + " * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. For use in your configuration files, it's advisable to use `SpoonInstall.asyncUpdateRepo()` instead.", + " * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions." + ], + "parameters": [ + " * repo - name of the repository to update. Defaults to `\"default\"`." + ], + "returns": [ + " * `true` if the update was successful, `nil` otherwise" + ], + "signature": "SpoonInstall:updateRepo(repo)", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "SpoonInstall.use_syncinstall", + "desc": "If `true`, `andUse()` will update repos and install packages synchronously. Defaults to `false`.", + "doc": "If `true`, `andUse()` will update repos and install packages synchronously. Defaults to `false`.\n\nKeep in mind that if you set this to `true`, Hammerspoon will\nblock until all missing Spoons are installed, but the notifications\nwill happen at a more \"human readable\" rate.", + "name": "use_syncinstall", + "signature": "SpoonInstall.use_syncinstall", + "stripped_doc": "Keep in mind that if you set this to `true`, Hammerspoon will\nblock until all missing Spoons are installed, but the notifications\nwill happen at a more \"human readable\" rate.", + "type": "Variable" + } + ], + "name": "SpoonInstall", + "stripped_doc": "\nDownload: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip)", + "submodules": [], + "type": "Module" + } +] \ No newline at end of file diff --git a/Spoons/SpoonInstall.spoon/init.lua b/Spoons/SpoonInstall.spoon/init.lua new file mode 100644 index 0000000..f69cfb2 --- /dev/null +++ b/Spoons/SpoonInstall.spoon/init.lua @@ -0,0 +1,447 @@ +--- === SpoonInstall === +--- +--- Install and manage Spoons and Spoon repositories +--- +--- Download: [https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip](https://github.com/Hammerspoon/Spoons/raw/master/Spoons/SpoonInstall.spoon.zip) + +local obj={} +obj.__index = obj + +-- Metadata +obj.name = "SpoonInstall" +obj.version = "0.1" +obj.author = "Diego Zamboni " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +--- SpoonInstall.logger +--- Variable +--- Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon. +obj.logger = hs.logger.new('SpoonInstall') + +--- SpoonInstall.repos +--- Variable +--- Table containing the list of available Spoon repositories. The key +--- of each entry is an identifier for the repository, and its value +--- is a table with the following entries: +--- * desc - Human-readable description for the repository +--- * branch - Active git branch for the Spoon files +--- * url - Base URL for the repository. For now the repository is assumed to be hosted in GitHub, and the URL should be the main base URL of the repository. Repository metadata needs to be stored under `docs/docs.json`, and the Spoon zip files need to be stored under `Spoons/`. +--- +--- Default value: +--- ``` +--- { +--- default = { +--- url = "https://github.com/Hammerspoon/Spoons", +--- desc = "Main Hammerspoon Spoon repository", +--- branch = "master", +--- } +--- } +--- ``` +obj.repos = { + default = { + url = "https://github.com/Hammerspoon/Spoons", + desc = "Main Hammerspoon Spoon repository", + branch = "master", + } +} + +--- SpoonInstall.use_syncinstall +--- Variable +--- If `true`, `andUse()` will update repos and install packages synchronously. Defaults to `false`. +--- +--- Keep in mind that if you set this to `true`, Hammerspoon will +--- block until all missing Spoons are installed, but the notifications +--- will happen at a more "human readable" rate. +obj.use_syncinstall = false + +-- Execute a command and return its output with trailing EOLs trimmed. If the command fails, an error message is logged. +local function _x(cmd, errfmt, ...) + local output, status = hs.execute(cmd) + if status then + local trimstr = string.gsub(output, "\n*$", "") + return trimstr + else + obj.logger.ef(errfmt, ...) + return nil + end +end + +-- -------------------------------------------------------------------- +-- Spoon repository management + +-- Internal callback to process and store the data from docs.json about a repository +-- callback is called with repo as arguments, only if the call is successful +function obj:_storeRepoJSON(repo, callback, status, body, hdrs) + local success=nil + if (status < 100) or (status >= 400) then + self.logger.ef("Error fetching JSON data for repository '%s'. Error code %d: %s", repo, status, body or "") + else + local json = hs.json.decode(body) + if json then + self.repos[repo].data = {} + for i,v in ipairs(json) do + v.download_url = self.repos[repo].download_base_url .. v.name .. ".spoon.zip" + self.repos[repo].data[v.name] = v + end + self.logger.df("Updated JSON data for repository '%s'", repo) + success=true + else + self.logger.ef("Invalid JSON received for repository '%s': %s", repo, body) + end + end + if callback then + callback(repo, success) + end + return success +end + +-- Internal function to return the URL of the docs.json file based on the URL of a GitHub repo +function obj:_build_repo_json_url(repo) + if self.repos[repo] and self.repos[repo].url then + local branch = self.repos[repo].branch or "master" + self.repos[repo].json_url = string.gsub(self.repos[repo].url, "/$", "") .. "/raw/"..branch.."/docs/docs.json" + self.repos[repo].download_base_url = string.gsub(self.repos[repo].url, "/$", "") .. "/raw/"..branch.."/Spoons/" + return true + else + self.logger.ef("Invalid or unknown repository '%s'", repo) + return nil + end +end + +--- SpoonInstall:asyncUpdateRepo(repo, callback) +--- Method +--- Asynchronously fetch the information about the contents of a Spoon repository +--- +--- Parameters: +--- * repo - name of the repository to update. Defaults to `"default"`. +--- * callback - if given, a function to be called after the update finishes (also if it fails). The function will receive the following arguments: +--- * repo - name of the repository +--- * success - boolean indicating whether the update succeeded +--- +--- Returns: +--- * `true` if the update was correctly initiated (i.e. the repo name is valid), `nil` otherwise +--- +--- Notes: +--- * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions. +function obj:asyncUpdateRepo(repo, callback) + if not repo then repo = 'default' end + if self:_build_repo_json_url(repo) then + hs.http.asyncGet(self.repos[repo].json_url, nil, hs.fnutils.partial(self._storeRepoJSON, self, repo, callback)) + return true + else + return nil + end +end + +--- SpoonInstall:updateRepo(repo) +--- Method +--- Synchronously fetch the information about the contents of a Spoon repository +--- +--- Parameters: +--- * repo - name of the repository to update. Defaults to `"default"`. +--- +--- Returns: +--- * `true` if the update was successful, `nil` otherwise +--- +--- Notes: +--- * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. For use in your configuration files, it's advisable to use `SpoonInstall.asyncUpdateRepo()` instead. +--- * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions. +function obj:updateRepo(repo) + if not repo then repo = 'default' end + if self:_build_repo_json_url(repo) then + local a,b,c = hs.http.get(self.repos[repo].json_url) + return self:_storeRepoJSON(repo, nil, a, b, c) + else + return nil + end +end + +--- SpoonInstall:asyncUpdateAllRepos() +--- Method +--- Asynchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos` +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +--- +--- Notes: +--- * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions. +function obj:asyncUpdateAllRepos() + for k,v in pairs(self.repos) do + self:asyncUpdateRepo(k) + end +end + +--- SpoonInstall:updateAllRepos() +--- Method +--- Synchronously fetch the information about the contents of all Spoon repositories registered in `SpoonInstall.repos` +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * None +--- +--- Notes: +--- * This is a synchronous call, which means Hammerspoon will be blocked until it finishes. +--- * For now, the repository data is not persisted, so you need to update it after every restart if you want to use any of the install functions. +function obj:updateAllRepos() + for k,v in pairs(self.repos) do + self:updateRepo(k) + end +end + +--- SpoonInstall:repolist() +--- Method +--- Return a sorted list of registered Spoon repositories +--- +--- Parameters: +--- * None +--- +--- Returns: +--- * Table containing a list of strings with the repository identifiers +function obj:repolist() + local keys={} + -- Create sorted list of keys + for k,v in pairs(self.repos) do table.insert(keys, k) end + table.sort(keys) + return keys +end + +--- SpoonInstall:search(pat) +--- Method +--- Search repositories for a pattern +--- +--- Parameters: +--- * pat - Lua pattern that will be matched against the name and description of each spoon in the registered repositories. All text is converted to lowercase before searching it, so you can use all-lowercase in your pattern. +--- +--- Returns: +--- * Table containing a list of matching entries. Each entry is a table with the following keys: +--- * name - Spoon name +--- * desc - description of the spoon +--- * repo - identifier in the repository where the match was found +function obj:search(pat) + local res={} + for repo,v in pairs(self.repos) do + if v.data then + for spoon,rec in pairs(v.data) do + if string.find(string.lower(rec.name .. "\n" .. rec.desc), pat) then + table.insert(res, { name = rec.name, desc = rec.desc, repo = repo }) + end + end + else + self.logger.ef("Repository data for '%s' not available - call spoon.SpoonInstall:updateRepo('%s'), then try again.", repo, repo) + end + end + return res +end + +-- -------------------------------------------------------------------- +-- Spoon installation + +-- Internal callback function to finalize the installation of a spoon after the zip file has been downloaded. +-- callback, if given, is called with (urlparts, success) as arguments +function obj:_installSpoonFromZipURLgetCallback(urlparts, callback, status, body, headers) + local success=nil + if (status < 100) or (status >= 400) then + self.logger.ef("Error downloading %s. Error code %d: %s", urlparts.absoluteString, status, body or "") + else + -- Write the zip file to disk in a temporary directory + local tmpdir=_x("/usr/bin/mktemp -d", "Error creating temporary directory to download new spoon.") + if tmpdir then + local outfile = string.format("%s/%s", tmpdir, urlparts.lastPathComponent) + local f=assert(io.open(outfile, "w")) + f:write(body) + f:close() + + -- Check its contents - only one *.spoon directory should be in there + output = _x(string.format("/usr/bin/unzip -l %s '*.spoon/' | /usr/bin/awk '$NF ~ /\\.spoon\\/$/ { print $NF }' | /usr/bin/wc -l", outfile), + "Error examining downloaded zip file %s, leaving it in place for your examination.", outfile) + if output then + if (tonumber(output) or 0) == 1 then + -- Uncompress the zip file + local outdir = string.format("%s/Spoons", hs.configdir) + if _x(string.format("/usr/bin/unzip -o %s -d %s 2>&1", outfile, outdir), + "Error uncompressing file %s, leaving it in place for your examination.", outfile) then + -- And finally, install it using Hammerspoon itself + self.logger.f("Downloaded and installed %s", urlparts.absoluteString) + _x(string.format("/bin/rm -rf '%s'", tmpdir), "Error removing directory %s", tmpdir) + success=true + end + else + self.logger.ef("The downloaded zip file %s is invalid - it should contain exactly one spoon. Leaving it in place for your examination.", outfile) + end + end + end + end + if callback then + callback(urlparts, success) + end + return success +end + +--- SpoonInstall:asyncInstallSpoonFromZipURL(url, callback) +--- Method +--- Asynchronously download a Spoon zip file and install it. +--- +--- Parameters: +--- * url - URL of the zip file to install. +--- * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments: +--- * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file +--- * success - boolean indicating whether the installation was successful +--- +--- Returns: +--- * `true` if the installation was correctly initiated (i.e. the URL is valid), `false` otherwise +function obj:asyncInstallSpoonFromZipURL(url, callback) + local urlparts = hs.http.urlParts(url) + local dlfile = urlparts.lastPathComponent + if dlfile and dlfile ~= "" and urlparts.pathExtension == "zip" then + hs.http.asyncGet(url, nil, hs.fnutils.partial(self._installSpoonFromZipURLgetCallback, self, urlparts, callback)) + return true + else + self.logger.ef("Invalid URL %s, must point to a zip file", url) + return nil + end +end + +--- SpoonInstall:installSpoonFromZipURL(url) +--- Method +--- Synchronously download a Spoon zip file and install it. +--- +--- Parameters: +--- * url - URL of the zip file to install. +--- +--- Returns: +--- * `true` if the installation was successful, `nil` otherwise +function obj:installSpoonFromZipURL(url) + local urlparts = hs.http.urlParts(url) + local dlfile = urlparts.lastPathComponent + if dlfile and dlfile ~= "" and urlparts.pathExtension == "zip" then + a,b,c=hs.http.get(url) + return self:_installSpoonFromZipURLgetCallback(urlparts, nil, a, b, c) + else + self.logger.ef("Invalid URL %s, must point to a zip file", url) + return nil + end +end + +-- Internal function to check if a Spoon/Repo combination is valid +function obj:_is_valid_spoon(name, repo) + if self.repos[repo] then + if self.repos[repo].data then + if self.repos[repo].data[name] then + return true + else + self.logger.ef("Spoon '%s' does not exist in repository '%s'. Please check and try again.", name, repo) + end + else + self.logger.ef("Repository data for '%s' not available - call spoon.SpoonInstall:updateRepo('%s'), then try again.", repo, repo) + end + else + self.logger.ef("Invalid or unknown repository '%s'", repo) + end + return nil +end + +--- SpoonInstall:asyncInstallSpoonFromRepo(name, repo, callback) +--- Method +--- Asynchronously install a Spoon from a registered repository +--- +--- Parameters: +--- * name - Name of the Spoon to install. +--- * repo - Name of the repository to use. Defaults to `"default"` +--- * callback - if given, a function to call after the installation finishes (also if it fails). The function receives the following arguments: +--- * urlparts - Result of calling `hs.http.urlParts` on the URL of the Spoon zip file +--- * success - boolean indicating whether the installation was successful +--- +--- Returns: +--- * `true` if the installation was correctly initiated (i.e. the repo and spoon name were correct), `false` otherwise. +function obj:asyncInstallSpoonFromRepo(name, repo, callback) + if not repo then repo = 'default' end + if self:_is_valid_spoon(name, repo) then + self:asyncInstallSpoonFromZipURL(self.repos[repo].data[name].download_url, callback) + end + return nil +end + +--- SpoonInstall:installSpoonFromRepo(name, repo) +--- Method +--- Synchronously install a Spoon from a registered repository +--- +--- Parameters: +--- * name = Name of the Spoon to install. +--- * repo - Name of the repository to use. Defaults to `"default"` +--- +--- Returns: +--- * `true` if the installation was successful, `nil` otherwise. +function obj:installSpoonFromRepo(name, repo, callback) + if not repo then repo = 'default' end + if self:_is_valid_spoon(name, repo) then + return self:installSpoonFromZipURL(self.repos[repo].data[name].download_url) + end + return nil +end + +--- SpoonInstall:andUse(name, arg) +--- Method +--- Declaratively install, load and configure a Spoon +--- +--- Parameters: +--- * name - the name of the Spoon to install (without the `.spoon` extension). If the Spoon is already installed, it will be loaded using `hs.loadSpoon()`. If it is not installed, it will be installed using `SpoonInstall:asyncInstallSpoonFromRepo()` and then loaded. +--- * arg - if provided, can be used to specify the configuration of the Spoon. The following keys are recognized (all are optional): +--- * repo - repository from where the Spoon should be installed if not present in the system, as defined in `SpoonInstall.repos`. Defaults to `"default"`. +--- * config - a table containing variables to be stored in the Spoon object to configure it. For example, `config = { answer = 42 }` will result in `spoon..answer` being set to 42. +--- * hotkeys - a table containing hotkey bindings. If provided, will be passed as-is to the Spoon's `bindHotkeys()` method. The special string `"default"` can be given to use the Spoons `defaultHotkeys` variable, if it exists. +--- * fn - a function which will be called with the freshly-loaded Spoon object as its first argument. +--- * loglevel - if the Spoon has a variable called `logger`, its `setLogLevel()` method will be called with this value. +--- * start - if `true`, call the Spoon's `start()` method after configuring everything else. +--- * disable - if `true`, do nothing. Easier than commenting it out when you want to temporarily disable a spoon. +--- +--- Returns: +--- * None +function obj:andUse(name, arg) + if not arg then arg = {} end + if arg.disable then return true end + if hs.spoons.use(name, arg, true) then + return true + else + local repo = arg.repo or "default" + if self.repos[repo] then + if self.repos[repo].data then + local load_and_config = function(_, success) + if success then + hs.notify.show("Spoon installed by SpoonInstall", name .. ".spoon is now available", "") + hs.spoons.use(name, arg) + else + obj.logger.ef("Error installing Spoon '%s' from repo '%s'", name, repo) + end + end + if self.use_syncinstall then + return load_and_config(nil, self:installSpoonFromRepo(name, repo)) + else + self:asyncInstallSpoonFromRepo(name, repo, load_and_config) + end + else + local update_repo_and_continue = function(_, success) + if success then + obj:andUse(name, arg) + else + obj.logger.ef("Error updating repository '%s'", repo) + end + end + if self.use_syncinstall then + return update_repo_and_continue(nil, self:updateRepo(repo)) + else + self:asyncUpdateRepo(repo, update_repo_and_continue) + end + end + else + obj.logger.ef("Unknown repository '%s' for Spoon", repo, name) + end + end +end + +return obj diff --git a/System_Tweaks.lua b/System_Tweaks.lua new file mode 100644 index 0000000..4a415dd --- /dev/null +++ b/System_Tweaks.lua @@ -0,0 +1,16 @@ +-- System_Tweaks.lua +-- Disable Time Machine throttling silently +local function disableTMThrottling() + -- Use hs.execute to run the command directly via sudo + -- Since we added the NOPASSWD rule, this will be silent and instant + local output, status, type, rc = hs.execute("sudo /usr/sbin/sysctl debug.lowpri_throttle_enabled=0", true) + + if status then + hs.notify.new({title="Hammerspoon", informativeText="Time Machine unthrottled (Silent)."}):send() + else + print("Sudo Error: " .. output) + end +end + +-- Run on Hammerspoon load/reload +disableTMThrottling() \ No newline at end of file diff --git a/WindowManager copy.lua b/WindowManager copy.lua new file mode 100644 index 0000000..15445b8 --- /dev/null +++ b/WindowManager copy.lua @@ -0,0 +1,202 @@ +local obj = {} +local json = require("hs.json") +local styledtext = require("hs.styledtext") + +-- ========================================== +-- CONFIGURATION +-- ========================================== +obj.layoutFile = os.getenv("HOME") .. "/.hammerspoon/saved_layout.json" +obj.saveInterval = 300 +obj.isRescued = false +obj.isTransitioning = false +obj.lastScreenCount = #hs.screen.allScreens() +obj.lastSavedTime = "Never" + +local stubbornAppsList = { + ["Gemini"] = true, ["AFFiNE"] = true, ["Terminal"] = true, + ["System Settings"] = true, ["Hammerspoon"] = true +} + +local ignoreListItems = { + ["TheBoringNotch"] = true, ["Control Center"] = true, ["Notification Center"] = true, + ["Dock"] = true, ["BetterDisplay"] = true, ["Stats"] = true, ["DockDoor"] = true +} + +-- ========================================== +-- INTERNAL UTILITIES +-- ========================================== + +local function wrapText(text, limit) + local lines = {} + local currentLine = "" + for word in text:gmatch("%S+") do + if #currentLine + #word >= limit then + table.insert(lines, currentLine) + currentLine = " " .. word + else + currentLine = currentLine == "" and " ↳ " .. word or currentLine .. " " .. word + end + end + table.insert(lines, currentLine) + return lines +end + +-- ========================================== +-- CORE LOGIC +-- ========================================== + +function obj.saveLayout(silent) + if hs.caffeinate.get("displayIdle") or obj.isTransitioning or #hs.screen.allScreens() ~= obj.lastScreenCount then + return + end + + local screens = hs.screen.allScreens() + local layout = { + saveTime = os.date("%H:%M:%S"), + screenCount = #screens, + windows = {} + } + + for _, win in ipairs(hs.window.allWindows()) do + local app = win:application() + local screen = win:screen() + if app and screen and win:isVisible() and win:frame().w > 0 then + local appName = app:name() or "" + if not (ignoreListItems[appName] or appName:find("TheBoringNotch")) then + table.insert(layout.windows, { + appName = appName, + bundleID = app:bundleID(), + winTitle = win:title(), + screenName = screen:name(), + x = math.floor(win:frame().x), + y = math.floor(win:frame().y), + w = math.floor(win:frame().w), + h = math.floor(win:frame().h) + }) + end + end + end + hs.json.write(layout, obj.layoutFile, true, true) + obj.lastSavedTime = layout.saveTime + if not silent then hs.alert.show("Layout Saved", 1.5) end +end + +function obj.restoreLayout() + local data = hs.json.read(obj.layoutFile) + if not data then return end + obj.isRescued = false + local appWindowTracker = {} + + for _, winData in ipairs(data.windows or {}) do + local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName) + if app then + local appName = app:name() + appWindowTracker[appName] = appWindowTracker[appName] or 1 + + local validWins = {} + for _, w in ipairs(app:allWindows()) do if w:isVisible() then table.insert(validWins, w) end end + + local win = validWins[appWindowTracker[appName]] + if win then + if win:isMinimized() then win:unminimize() end + + local x, y, w, h = winData.x, winData.y, winData.w, winData.h + local isStubborn = stubbornAppsList[appName] or (app:path() or ""):find("Electron") + + local function moveAction() + if isStubborn then + local winTarget = (appName == "Gemini") and "window 1" or string.format('first window whose name is "%s"', win:title():gsub('"', '\\"')) + local script = string.format([[ + tell application "System Events" to tell (first process whose unix id is %d) + try + set targetWin to %s + set position of targetWin to {%d, %d} + set size of targetWin to {%d, %d} + end try + end tell + ]], app:pid(), winTarget, x, y, w, h) + hs.applescript.applescript(script) + else + win:setFrame({x=x, y=y, w=w, h=h}, 0) + end + end + + moveAction() + hs.timer.doAfter(0.5, moveAction) + hs.timer.doAfter(1.5, moveAction) + + appWindowTracker[appName] = appWindowTracker[appName] + 1 + end + end + end + hs.alert.show("Layout Restored", 1.5) +end + +function obj.rescueWindowsToLaptop() + local primary = hs.screen.primaryScreen() + for _, win in ipairs(hs.window.allWindows()) do + if win:isVisible() then win:moveToScreen(primary, false, true) end + end + obj.isRescued = true + hs.alert.show("Rescued to Laptop", 1.5) +end + +-- ========================================== +-- MENUBAR & TIMERS +-- ========================================== +local saveCountdown = obj.saveInterval +local timerMenu = hs.menubar.new() + +function updateMenu() + if timerMenu then + local screens = hs.screen.allScreens() + local minutes, seconds = math.floor(saveCountdown / 60), saveCountdown % 60 + timerMenu:setTitle(string.format("💠 %d:%02d", minutes, seconds)) + + local data = hs.json.read(obj.layoutFile) + local menuTable = { + { title = "📅 Last Saved: " .. (obj.lastSavedTime or "Never"), disabled = true }, + { title = ((#screens > 1) and "🖥️ Docked" or "💻 Laptop") .. " (" .. #screens .. " Screens)", disabled = true }, + { title = "-" }, + { title = "📸 Save Layout (⇧⌘W)", fn = function() obj.saveLayout(false); saveCountdown = obj.saveInterval end }, + { title = "🔄 Restore Layout (⇧⌘R)", fn = obj.restoreLayout }, + { title = "🚀 Rescue Windows (Bring to Laptop) (⇧⌘⌃L)", fn = obj.rescueWindowsToLaptop }, + { title = "-" }, + { title = "📦 Saved Apps:", disabled = true } + } + if data and data.windows then + local seen, names = {}, {} + for _, w in ipairs(data.windows) do if not seen[w.appName] then table.insert(names, w.appName .. ", "); seen[w.appName] = true end end + for _, line in ipairs(wrapText(table.concat(names):gsub(", $", ""), 45)) do table.insert(menuTable, { title = line, disabled = true }) end + end + timerMenu:setMenu(menuTable) + end +end + +-- ========================================== +-- WATCHERS & HOTKEYS +-- ========================================== + +hs.hotkey.bind({"shift", "cmd"}, "W", function() obj.saveLayout(false); saveCountdown = obj.saveInterval end) +hs.hotkey.bind({"shift", "cmd"}, "R", obj.restoreLayout) +hs.hotkey.bind({"shift", "cmd", "ctrl"}, "L", obj.rescueWindowsToLaptop) + +obj.screenWatcher = hs.screen.watcher.new(function() + obj.isTransitioning = true + hs.timer.doAfter(30, function() + obj.lastScreenCount = #hs.screen.allScreens() + obj.isTransitioning = false + if obj.lastScreenCount > 1 then obj.restoreLayout() else obj.rescueWindowsToLaptop() end + updateMenu() + end) +end):start() + +obj.autoSaveTimer = hs.timer.doEvery(obj.saveInterval, function() obj.saveLayout(true); saveCountdown = obj.saveInterval end) +obj.clockTimer = hs.timer.doEvery(1, function() + saveCountdown = saveCountdown - 1 + if saveCountdown < 0 then saveCountdown = obj.saveInterval end + updateMenu() +end) + +updateMenu() +return obj \ No newline at end of file diff --git a/WindowManager.lua b/WindowManager.lua new file mode 100644 index 0000000..8f300bb --- /dev/null +++ b/WindowManager.lua @@ -0,0 +1,341 @@ +-- WindowManager.lua + +local obj = {} +local json = require("hs.json") +local styledtext = require("hs.styledtext") + +-- ========================================== +-- CONFIGURATION & CENTRAL CONFIG LOADING +-- ========================================== +obj.layoutFile = os.getenv("HOME") .. "/.hammerspoon/saved_layout.json" +local configFile = os.getenv("HOME") .. "/.hammerspoon/config.json" + +local stubbornAppsList = { + ["Gemini"] = true, ["AFFiNE"] = true, ["Terminal"] = true, + ["System Settings"] = true, ["Hammerspoon"] = true +} + +local ignoreListItems = { + ["TheBoringNotch"] = true, ["Control Center"] = true, ["Notification Center"] = true, + ["Dock"] = true, ["BetterDisplay"] = true, ["Stats"] = true, ["DockDoor"] = true, + ["Bartender 6"] = true, ["Bartender"] = true, ["Arq"] = true, ["Arq Agent"] = true, ["TypeWhisper"] = true, + ["com.typewhisper.mac"] = true +} + +-- Load Central Config if available +local cfgData = hs.json.read(configFile) +if cfgData then + if cfgData.stubbornAppsList then stubbornAppsList = cfgData.stubbornAppsList end + if cfgData.ignoreListItems then ignoreListItems = cfgData.ignoreListItems end +end + +obj.saveInterval = 300 +obj.isRescued = false +obj.isTransitioning = false +obj.isRestoring = false +obj.wakeTimer = nil +obj.lastScreenCount = #hs.screen.allScreens() +obj.lastSavedTime = "Never" + +-- ========================================== +-- INTERNAL UTILITIES +-- ========================================== + +local function log(msg) + print(string.format("WindowManager [%s]: %s", os.date("%H:%M:%S"), msg)) +end + +local function wrapText(text, limit) + local lines = {} + local currentLine = "" + for word in text:gmatch("%S+") do + if #currentLine + #word >= limit then + table.insert(lines, currentLine) + currentLine = " " .. word + else + currentLine = currentLine == "" and " ↳ " .. word or currentLine .. " " .. word + end + end + table.insert(lines, currentLine) + return lines +end + +-- ========================================== +-- CORE LOGIC +-- ========================================== + +function obj.saveLayout(silent) + local currentScreens = #hs.screen.allScreens() + + if currentScreens ~= obj.lastScreenCount then + log(string.format("Screen mismatch (%d vs %d). Syncing count.", currentScreens, obj.lastScreenCount)) + obj.lastScreenCount = currentScreens + end + + local layout = { + saveTime = os.date("%H:%M:%S"), + screenCount = currentScreens, + windows = {} + } + + for _, win in ipairs(hs.window.allWindows()) do + pcall(function() + local app = win:application() + if app and win:isVisible() and win:frame().w > 0 then + local appName = app:name() or "" + local bundleID = app:bundleID() or "" + + if not ignoreListItems[appName] and not ignoreListItems[bundleID] then + table.insert(layout.windows, { + appName = appName, + bundleID = bundleID, + winTitle = win:title(), + x = math.floor(win:frame().x), + y = math.floor(win:frame().y), + w = math.floor(win:frame().w), + h = math.floor(win:frame().h) + }) + end + end + end) + end + + hs.json.write(layout, obj.layoutFile, true, true) + obj.lastSavedTime = layout.saveTime + + if silent then + log("Autosave successful.") + else + log("Manual save successful.") + hs.alert.show("Layout Saved", 1.5) + end +end + +function obj.restoreLayout() + obj.isRestoring = true + obj.isTransitioning = false + obj.isRescued = false + + local data = hs.json.read(obj.layoutFile) + if not data or not data.windows then + log("Restore FAILED: No data found in JSON.") + obj.isRestoring = false + return + end + + hs.screen.restoreGamma() + + local savedByApp = {} + for _, winData in ipairs(data.windows) do + if not ignoreListItems[winData.appName] and not ignoreListItems[winData.bundleID] then + savedByApp[winData.appName] = savedByApp[winData.appName] or {} + table.insert(savedByApp[winData.appName], winData) + end + end + + for appName, entries in pairs(savedByApp) do + table.sort(entries, function(a, b) + if a.y == b.y then return a.x < b.x end + return a.y < b.y + end) + end + + log("RESTORE STARTING...") + + for appName, savedEntries in pairs(savedByApp) do + local firstEntry = savedEntries[1] + local app = hs.application.get(firstEntry.bundleID) or hs.application.get(appName) + + if app then + local physicalWins = {} + for _, w in ipairs(app:allWindows()) do + if w:isVisible() and w:frame().w > 0 then table.insert(physicalWins, w) end + end + + table.sort(physicalWins, function(a, b) + local af, bf = a:frame(), b:frame() + if af.y == bf.y then return af.x < bf.x end + return af.y < bf.y + end) + + for i, winData in ipairs(savedEntries) do + local win = physicalWins[i] + if win then + log(string.format("Moving %s (%d/%d)", appName, i, #savedEntries)) + if win:isMinimized() then win:unminimize() end + + pcall(function() + win:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0) + + if stubbornAppsList[appName] then + hs.timer.doAfter(0.5, function() + pcall(function() win:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0) end) + end) + end + end) + end + end + else + log(string.format("Skip: %s is not running.", appName)) + end + end + + hs.alert.show("Layout Restored", 1.5) + hs.timer.doAfter(5, function() + obj.isRestoring = false + obj.isTransitioning = false + log("RESTORE CYCLE COMPLETE.") + end) +end + +function obj.rescueWindowsToLaptop() + local primary = hs.screen.primaryScreen() + if not primary then return end + local maxFrame = primary:frame() + log("RESCUE: Cascading windows on laptop.") + + local allWindows = hs.window.allWindows() + local staggerOffset = 0 + + for _, win in ipairs(allWindows) do + pcall(function() + local app = win:application() + if app and win:isVisible() and win:frame().w > 0 then + local appName = app:name() or "" + local bundleID = app:bundleID() or "" + + if not ignoreListItems[appName] and not ignoreListItems[bundleID] then + local f = win:frame() + if f.w > maxFrame.w then f.w = maxFrame.w - 100 end + if f.h > maxFrame.h then f.h = maxFrame.h - 100 end + + -- NEW LOGIC: Start at top-left corner (50px offset) instead of center + f.x = maxFrame.x + 50 + staggerOffset + f.y = maxFrame.y + 50 + staggerOffset + + win:setFrame(f, 0) + staggerOffset = staggerOffset + 30 + if staggerOffset > 150 then staggerOffset = 0 end + end + end + end) + end + obj.isRescued = true + obj.isTransitioning = false + hs.alert.show("Windows Cascaded", 1.5) +end + +-- ========================================== +-- MENUBAR & WATCHERS +-- ========================================== +local saveCountdown = obj.saveInterval +local timerMenu = hs.menubar.new() + +function updateMenu() + if timerMenu then + local screens = hs.screen.allScreens() + timerMenu:setTitle(string.format("💠 %d:%02d", math.floor(saveCountdown / 60), saveCountdown % 60)) + local menuTable = { + { title = "📅 Last Saved: " .. (obj.lastSavedTime or "Never"), disabled = true }, + { title = ((#screens > 1) and "🖥️ Docked" or "💻 Laptop") .. " (" .. #screens .. " Screens)", disabled = true }, + { title = "-" }, + { title = "📸 Save Layout (⇧⌘W)", fn = function() obj.saveLayout(false); saveCountdown = obj.saveInterval end }, + { title = "🔄 Restore Layout (⇧⌘R)", fn = function() obj.restoreLayout() end }, + { title = "🚀 Rescue Windows (Cascade on Laptop) (⇧⌘⌃L)", fn = obj.rescueWindowsToLaptop }, + { title = "-" }, + { title = "📦 Saved Apps:", disabled = true } + } + + local data = hs.json.read(obj.layoutFile) + if data and data.windows then + local seen, names = {}, {} + for _, w in ipairs(data.windows) do + if not seen[w.appName] then table.insert(names, w.appName .. ", "); seen[w.appName] = true end + end + for _, line in ipairs(wrapText(table.concat(names):gsub(", $", ""), 45)) do + table.insert(menuTable, { title = line, disabled = true }) + end + end + timerMenu:setMenu(menuTable) + end +end + +obj.powerWatcher = hs.caffeinate.watcher.new(function(event) + if event == hs.caffeinate.watcher.systemWillSleep or event == hs.caffeinate.watcher.screensDidSleep or event == hs.caffeinate.watcher.screensDidLock then + log("POWER: Sleep event.") + obj.isTransitioning = true + if obj.autoSaveTimer then obj.autoSaveTimer:stop() end + elseif event == hs.caffeinate.watcher.systemDidWake or event == hs.caffeinate.watcher.screensDidWake or event == hs.caffeinate.watcher.screensDidUnlock then + log("POWER: Wake event.") + saveCountdown = obj.saveInterval + if obj.autoSaveTimer then obj.autoSaveTimer:start() end + if obj.wakeTimer then obj.wakeTimer:stop() end + + local currentScreens = #hs.screen.allScreens() + + if currentScreens > 1 then + obj.wakeTimer = hs.timer.doAfter(12, function() + obj.isTransitioning = false + obj.isRestoring = false + obj.lastScreenCount = currentScreens + obj.restoreLayout() + obj.wakeTimer = nil + end) + else + log("WAKE SKIP: Single screen detected. Syncing count only.") + obj.isTransitioning = false + obj.isRestoring = false + obj.lastScreenCount = currentScreens + end + end +end):start() + +hs.hotkey.bind({"shift", "cmd"}, "W", function() obj.saveLayout(false); saveCountdown = obj.saveInterval end) +hs.hotkey.bind({"shift", "cmd"}, "R", function() obj.restoreLayout() end) +hs.hotkey.bind({"shift", "cmd", "ctrl"}, "L", obj.rescueWindowsToLaptop) + +obj.screenWatcher = hs.screen.watcher.new(function() + if obj.isTransitioning or obj.isRestoring or obj.wakeTimer then + log("DOCK EVENT: Ignored.") + return + end + + local currentScreens = #hs.screen.allScreens() + + -- NEW GUARD: If count hasn't changed, ignore ghost/handshake events + if currentScreens == obj.lastScreenCount then + log("DOCK EVENT: Ignored (Screen count unchanged).") + return + end + + log("DOCK EVENT: Detected.") + obj.isTransitioning = true + + hs.timer.doAfter(15, function() + if obj.isTransitioning and not obj.isRestoring then + obj.isTransitioning = false + log("STATE GUARD: Emergency clear of busy flag.") + end + end) + + hs.timer.doAfter(7, function() + obj.lastScreenCount = #hs.screen.allScreens() + obj.isTransitioning = false + if obj.lastScreenCount > 1 then obj.restoreLayout() else obj.rescueWindowsToLaptop() end + updateMenu() + end) +end):start() + +obj.autoSaveTimer = hs.timer.doEvery(obj.saveInterval, function() + obj.saveLayout(true) + saveCountdown = obj.saveInterval +end) + +obj.clockTimer = hs.timer.doEvery(1, function() + saveCountdown = saveCountdown - 1 + if saveCountdown < 0 then saveCountdown = obj.saveInterval end + updateMenu() +end) + +updateMenu() +return obj \ No newline at end of file diff --git a/affine_clipper.lua b/affine_clipper.lua new file mode 100644 index 0000000..622300a --- /dev/null +++ b/affine_clipper.lua @@ -0,0 +1,72 @@ +local obj = {} + +local function getBrowserURL() + local app = hs.application.frontmostApplication() + local name = app:name() + local script = "" + if name == "Safari" then + script = 'tell app "Safari" to return URL of front document' + elseif name == "Google Chrome" or name == "Brave Browser" or name == "Microsoft Edge" then + script = 'tell app "' .. name .. '" to return URL of active tab of front window' + else + return nil + end + local ok, url = hs.applescript(script) + return ok and url or nil +end + +function obj.clip() + hs.timer.usleep(200000) + + local url = getBrowserURL() + hs.alert.show("Clipping to AFFiNE...", 1.5) + + hs.pasteboard.clearContents() + hs.eventtap.keyStroke({"cmd"}, "c") + hs.timer.usleep(800000) + + local clippedText = hs.pasteboard.getContents() + + if not clippedText or clippedText == "" then + hs.alert.show("Nothing selected! ❌", 2) + return + end + + hs.urlevent.openURL("affine://") + + hs.timer.doAfter(1.2, function() + local affine = hs.application.get("AFFiNE") + if affine then + affine:activate() + hs.eventtap.keyStroke({"cmd"}, "n") + hs.timer.usleep(1000000) + + hs.eventtap.keyStroke({}, "return") + hs.timer.usleep(500000) + + if url then + hs.pasteboard.setContents("Source: " .. url .. "\n\n") + hs.timer.usleep(300000) + hs.eventtap.keyStroke({"cmd"}, "v") + hs.timer.usleep(800000) + end + + hs.pasteboard.setContents(clippedText) + hs.timer.usleep(300000) + hs.eventtap.keyStroke({"cmd"}, "v") + + hs.alert.show("Clipped Successfully ✅", 1.5) + else + hs.alert.show("AFFiNE App not found! ❌", 3) + end + end) +end + +function obj.init() + -- Replaced the binding to use your Hyper Key + C + hs.hotkey.bind(hyper, "C", function() + obj.clip() + end) +end + +return obj \ No newline at end of file diff --git a/affine_quick_note copy.lua b/affine_quick_note copy.lua new file mode 100644 index 0000000..6892aab --- /dev/null +++ b/affine_quick_note copy.lua @@ -0,0 +1,57 @@ +-- ~/.hammerspoon/affine_quick_note.lua +local obj = {} + +-- CONFIGURATION +local NODE_PATH = "/opt/homebrew/bin/node" +local SYNC_SCRIPT = os.getenv("HOME") .. "/.hammerspoon/scripts/sync_note.js" + +local function affineQuickNote(text) + local syncNotify = hs.notify.new({ + title="AFFiNE Sync", + informativeText="Syncing large note... Please wait." + }):send() + + local task = hs.task.new(NODE_PATH, function(exitCode, stdOut, stdErr) + syncNotify:withdraw() + if exitCode == 0 then + hs.notify.new({title="AFFiNE Success", informativeText="Multi-line note appended."}):send() + else + hs.notify.new({title="AFFiNE Error", informativeText="Check Console."}):send() + print("Sync Error: " .. stdErr) + end + end, {SYNC_SCRIPT, text}) + + task:start() +end + +-- Function to trigger a proper macOS multi-line text box +local function getMultiLineInput() + local appleScript = [[ + tell application "System Events" + activate + set theResponse to display dialog "Enter your detailed note:" default answer "" with title "AFFiNE Multi-Line Capture" buttons {"Cancel", "Send"} default button "Send" + return text returned of theResponse + end tell + ]] + + local ok, result = hs.applescript(appleScript) + if ok and result ~= "" then + return result + end + return nil +end + +function obj.init() + if hyper then + hs.hotkey.bind(hyper, "N", function() + local text = getMultiLineInput() + if text then + affineQuickNote(text) + end + end) + else + print("AFFiNE Error: 'hyper' variable not found.") + end +end + +return obj \ No newline at end of file diff --git a/affine_quick_note.lua b/affine_quick_note.lua new file mode 100644 index 0000000..4e9c560 --- /dev/null +++ b/affine_quick_note.lua @@ -0,0 +1,72 @@ +local obj = {} + +-- Helper to handle the UI automation for creating and pasting into a new page +local function createAndPaste(content) + hs.alert.show("Sending to AFFiNE...", 1.5) + + -- 1. Open/Focus the AFFiNE Desktop App + hs.urlevent.openURL("affine://") + + -- 2. Wait for the app to come to the front and settle + hs.timer.doAfter(1.2, function() + local affine = hs.application.get("AFFiNE") + if affine then + affine:activate() + + -- Create a new page (Cmd+N) + hs.eventtap.keyStroke({"cmd"}, "n") + hs.timer.usleep(1000000) -- Wait 1s for the editor to load + + -- Move from Title to Body (Return) + hs.eventtap.keyStroke({}, "return") + hs.timer.usleep(500000) + + -- Set the clipboard to our note and paste (Cmd+V) + hs.pasteboard.setContents(content) + hs.timer.usleep(300000) + hs.eventtap.keyStroke({"cmd"}, "v") + + hs.alert.show("Note Created ✅", 1.5) + else + hs.alert.show("AFFiNE App not found! ❌", 3) + end + end) +end + +-- Function to get multi-line text input +local function getNoteInput() + local script = [[ + tell application "System Events" + activate + set prompt to "Enter your quick note:" + set theResponse to display dialog prompt default answer " " with title "AFFiNE Quick Note" buttons {"Cancel", "Send"} default button "Send" + return text returned of theResponse + end tell + ]] + + local ok, result = hs.applescript(script) + if ok and result and result ~= "" then + createAndPaste(result) + end +end + +function obj.init() + if not hyper then return end + + -- 1. Hyper + N: Manual text input dialog + hs.hotkey.bind(hyper, "N", function() + getNoteInput() + end) + + -- 2. Hyper + V: Instantly send current clipboard contents to a new page + hs.hotkey.bind(hyper, "V", function() + local clipboard = hs.pasteboard.getContents() + if clipboard and clipboard ~= "" then + createAndPaste(clipboard) + else + hs.alert.show("Clipboard is empty!") + end + end) +end + +return obj \ No newline at end of file diff --git a/bento.lua b/bento.lua new file mode 100644 index 0000000..1e68260 --- /dev/null +++ b/bento.lua @@ -0,0 +1,113 @@ +-- ~~~~~~~~~~ CONFIGURATION & STATE ~~~~~~~~~~ +local bento = {} +local k = _G.hyper or {"alt"} + +_G.BentoState = _G.BentoState or { + gridSize = "3x2", + autoRestore = true, + windowTracker = {}, + isSnapping = false -- LOCK: Prevents the bounce +} + +if _G.BentoResources then + for _, item in ipairs(_G.BentoResources) do + if item.delete then item:delete() end + if item.stop then item:stop() end + end +end +_G.BentoResources = {} + +hs.grid.setGrid(_G.BentoState.gridSize) +hs.grid.setMargins({5, 5}) +hs.window.animationDuration = 0 -- Set to 0 to prevent movement lag during snaps + +-- ~~~~~~~~~~ UTILITIES ~~~~~~~~~~ + +local function isWindowInGrid(win) + local cell = hs.grid.get(win) + local frame = win:frame() + local screen = win:screen() + local gridFrame = hs.grid.getCell(cell, screen) + + -- Using a wider 20px buffer to account for macOS window shadows/borders + return math.abs(frame.x - gridFrame.x) < 20 and + math.abs(frame.y - gridFrame.y) < 20 and + math.abs(frame.w - gridFrame.w) < 20 and + math.abs(frame.h - gridFrame.h) < 20 +end + +local function manage(action) + local win = hs.window.focusedWindow() + if not win then return end + + local id = win:id() + if not _G.BentoState.windowTracker[id] then + _G.BentoState.windowTracker[id] = win:frame() + end + + -- ENABLE LOCK + _G.BentoState.isSnapping = true + + if action == "ui" then + hs.grid.show() + else + action(win) + end + + -- RELEASE LOCK after a short delay + hs.timer.doAfter(0.3, function() + _G.BentoState.isSnapping = false + end) +end + +-- ~~~~~~~~~~ DYNAMIC MENU BAR ~~~~~~~~~~ + +local menu = hs.menubar.new() +table.insert(_G.BentoResources, menu) + +local function updateMenu() + return { + { title = "Grid: " .. _G.BentoState.gridSize, disabled = true }, + { title = "-" }, + { title = "Standard (2x2)", fn = function() _G.BentoState.gridSize = "2x2"; hs.reload() end, checked = (_G.BentoState.gridSize == "2x2") }, + { title = "Bento (3x2)", fn = function() _G.BentoState.gridSize = "3x2"; hs.reload() end, checked = (_G.BentoState.gridSize == "3x2") }, + { title = "-" }, + { title = "Reload Bento Only", fn = function() package.loaded["bento"] = nil; require("bento") end }, + { title = "Hard Reload", fn = hs.reload } + } +end + +menu:setTitle("⊞") +menu:setMenu(updateMenu) + +-- ~~~~~~~~~~ BINDINGS & WATCHERS ~~~~~~~~~~ + +local function bind(key, fn) + local b = hs.hotkey.bind(k, key, fn) + if b then table.insert(_G.BentoResources, b) end +end + +bind("G", function() manage("ui") end) +bind("Left", function() manage(hs.grid.pushWindowLeft) end) +bind("Right", function() manage(hs.grid.pushWindowRight) end) +bind("Up", function() manage(hs.grid.pushWindowUp) end) +bind("Down", function() manage(hs.grid.pushWindowDown) end) + +if _G.BentoState.autoRestore then + local wf = hs.window.filter.new() + wf:subscribe(hs.window.filter.windowMoved, function(win) + -- ONLY trigger if we aren't currently snapping via keyboard + if not _G.BentoState.isSnapping then + local id = win:id() + if _G.BentoState.windowTracker[id] and not isWindowInGrid(win) then + win:setFrame(_G.BentoState.windowTracker[id]) + _G.BentoState.windowTracker[id] = nil + hs.alert.show("Restored", 0.5) + end + end + end) + table.insert(_G.BentoResources, wf) +end + +hs.alert.show("Bento Ready", 1) +return bento \ No newline at end of file diff --git a/bump.lua b/bump.lua new file mode 100644 index 0000000..d5aa0ae --- /dev/null +++ b/bump.lua @@ -0,0 +1,81 @@ +-- ~/.hammerspoon/bump.lua + +local bump = {} + +-- Persistence and State +_G.BumpState = _G.BumpState or { + gridSize = "3x2", -- 3 columns, 2 rows + enabled = true +} + +-- Cleanup existing resources +if _G.BumpResources then + for _, item in ipairs(_G.BumpResources) do + if item.stop then item:stop() end + if item.delete then item:delete() end + end +end +_G.BumpResources = {} + +-- ~~~~~~~~~~ MANUAL BUMP LOGIC ~~~~~~~~~~ +local bentoWatcher = hs.window.filter.new() + +bentoWatcher:subscribe(hs.window.filter.windowMoved, function(newWin) + if not _G.BumpState.enabled then return end + + local f1 = newWin:frame() + local screen = newWin:screen() + local maxW = screen:frame().w + local allWindows = hs.window.visibleWindows() + + -- Calculate how wide one "slot" is (e.g., 1/3 of screen) + local cols = tonumber(_G.BumpState.gridSize:sub(1,1)) + local slotWidth = maxW / cols + + for _, oldWin in ipairs(allWindows) do + if oldWin:id() ~= newWin:id() and oldWin:screen() == screen and oldWin:isStandard() then + local f2 = oldWin:frame() + + -- Manual Intersection check + local dx = math.max(0, math.min(f1.x + f1.w, f2.x + f2.w) - math.max(f1.x, f2.x)) + local dy = math.max(0, math.min(f1.y + f1.h, f2.y + f2.h) - math.max(f1.y, f2.y)) + + -- If they overlap significantly + if (dx * dy) > (f1.w * f1.h * 0.4) then + local newX = f2.x + slotWidth + + -- If pushing right stays on screen, move it. + -- Otherwise, move it to the start of the next row (y + height) + if (newX + 50) > maxW then + oldWin:setFrame({x = 0, y = f2.y + f2.h, w = f2.w, h = f2.h}) + else + oldWin:setFrame({x = newX, y = f2.y, w = f2.w, h = f2.h}) + end + + hs.alert.show("Bumping " .. oldWin:application():name()) + end + end + end +end) +table.insert(_G.BumpResources, bentoWatcher) + +-- ~~~~~~~~~~ MENU BAR ~~~~~~~~~~ +local menu = hs.menubar.new() +table.insert(_G.BumpResources, menu) + +local function updateMenu() + return { + { title = "Bump Status: " .. (_G.BumpState.enabled and "ON" or "OFF"), disabled = true }, + { title = "-" }, + { title = "Grid: 2x2", fn = function() _G.BumpState.gridSize = "2x2"; hs.reload() end, checked = (_G.BumpState.gridSize == "2x2") }, + { title = "Grid: 3x2", fn = function() _G.BumpState.gridSize = "3x2"; hs.reload() end, checked = (_G.BumpState.gridSize == "3x2") }, + { title = "-" }, + { title = "Reload Bump Only", fn = function() package.loaded["bump"] = nil; require("bump") end }, + { title = "Hard Reload All", fn = hs.reload } + } +end + +menu:setTitle("⇄") +menu:setMenu(updateMenu) + +return bump \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..fe10fd8 --- /dev/null +++ b/config.json @@ -0,0 +1,33 @@ +{ + "stubbornAppsList": { + "Gemini": true, + "AFFiNE": true, + "Terminal": true, + "System Settings": true, + "Hammerspoon": true + }, + "ignoreListItems": { + "TheBoringNotch": true, + "The Boring Notch": true, + "theboringteam.boringnotch": true, + "boring.notch": true, + "Control Center": true, + "Notification Center": true, + "Dock": true, + "BetterDisplay": true, + "pro.betterdisplay.BetterDisplay": true, + "Stats": true, + "DockDoor": true, + "Bartender 6": true, + "com.surteesstudios.Bartender": true, + "Bartender": true, + "Arq": true, + "Arq Agent": true, + "com.apple.controlcenter": true, + "com.apple.notificationcenterui": true, + "TypeWhisper": true, + "com.typewhisper.mac": true, + "dk.heyiam.monocle": true, + "Monocle": true + } +} \ No newline at end of file diff --git a/gcp-key copy.json b/gcp-key copy.json new file mode 100644 index 0000000..b1aa172 --- /dev/null +++ b/gcp-key copy.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "gen-lang-client-0842463959", + "private_key_id": "cc31094c10c8d69ed840136f7ecab21fa560e25f", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC8tstAGMMtc38q\n580yI8PGDSrOcolm4eKVxOBSuzHwYjN1Q1Ay7ZKhuK+l7pv1vY/04b+Er/bPIviB\n9+A3S3tx+S+o28eEBbs+a7i2rAlNNJX/JHY18/L70s+x37qyIoPagoRDYkGrKsZI\nVLmHwQZCkdoMyvCNuzbFfkR5FurJSB9bxWVfx6I6L/fSPiBsZyvUzCoA3aJiN1SO\ngn+GdCOlqc/DQFIP9tCwGb2J8a29ma915ZTrKi6A0IiFkNuL+56EB9TwwZOT2KVC\nOSbAnJBHAbPBoirmS2qfpfhTvs/FZMaxbIkiSnzLujV4QwVjQG15KT2JWmabK7A/\nEMH6cJkbAgMBAAECggEACmVwKVXz5PfUDYDpE7rE9GpatqQBjhdGw05SkoDG+2Gi\nJGClGbST+4v0smUNxHxlq7arfoBBcrHZOFfAy2sKsl0gHFCgbZ6DyKhokIH853rx\nJsguu4sJEokGcZKVTkodV3J/Qo5/m1seC+EycRhFmnGH94lPJCIDwt5jxLrXwvlw\nZIMKkyDg8HnSosadr8DABK+W4SfqkTOFuZkIh4jpuhyLwByh8kFfOxqHliIiK9hf\n+QoXT3Ds1oula5Rdlu2ZZKYTThNVJbrpixMEcN5bl7NGKGQ47r4qt2ViDjjs24S9\nPE/JlYCrOO6+ovWdW29ZN/HqtM9U9pvXXdiGN41MaQKBgQDgHa5rZNs87Gnj5H4g\nIOFeGPLp9mRjU/S/tAXuTOrN2yG6lqNdwIUiAE9R6ip3uOPmzdnPCAUsgUVTtDEg\nHuziEvXB+4p7+at2PFhO5aH6Ye+npyRHzhcLKAMkheMZvSU2TSNeJpnKLnU6oBtP\n5VmTbPfsaTPwC6mkaFRh+69AlQKBgQDXj8XS5iqi0I+dRWmBANP31LvlR4cc4YW5\naXrjDNg5d7bw3+LflVrTjeeVqiunFYlrfMIy30TA9u4KisRdoJe2mYZIGy+CfkQL\n7PNdGv2XdfNQjRv0FYDMBkqRgUBFG9Y0mHsc+VKA99QZHy9QXdpd1JU1DmFup1s2\nQKLdXwyW7wKBgDC6I1cUOZqYaDl1T3ray0UzNXVq7c6uzVL06Ck0rgSN9VplMCXN\nGuUWnihYOl2HZH2lGgsqWj2f6ZvWXKv4LVbF+orvjt9/nCj729NjmAEhVALmkzvN\ncjMpwu0o8wSAnFufD+aDjAJqcXCKqQWI/x3PnmPXR8SUNJEbYeVf3G21AoGAcQJI\n3dYZCB57DCJ1u1HpzoXSs9MZ/IQnDRtFd38mZIpkeEeHs1ujsEE25fm+xOu/jYBs\ndysh6mAKT7CMXeFxaCN4iJjoAWuc1Pu2YltiE2Oc2eAAhag4S74Indu7DAAZ/pzp\n/jifjklfAoSc029AqexnBNezMMXAReMA/zlzajUCgYA2LU0JWqGjtZyk2u0BMOLy\nlpCzdzXMAN5A4TGQU8Mv2fvKQup8weRUyDctf+SE/jy2XkcjuViJU6c0PtAzn3t9\nne3cfB4Ee0D2Lv9dAJVHzgDqv9NMFQdGasKd53Ik6J5ryxRltQmjnjlS+KFmrssX\nyTMsGo5T5u36ZA4CZsxIag==\n-----END PRIVATE KEY-----\n", + "client_email": "ais-gemini-key-be1e93e9d76147d@423785749889.iam.gserviceaccount.com", + "client_id": "108179228765412243710", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ais-gemini-key-be1e93e9d76147d%40423785749889.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/gcp-key.json b/gcp-key.json new file mode 100644 index 0000000..21a05dc --- /dev/null +++ b/gcp-key.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "gen-lang-client-0842463959", + "private_key_id": "f488586958fba6d2205f937e63be3c5dcdc57446", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwnPNxJcufUhaF\nC7aE2xWe8IJbQoZiXo7gR9Vlw8eIGVsaRPaFdlDJeAiL1bKqyfLER1LuLv4obFuZ\n6cZYnZ39UfnifHBEfzmLOFGE1O4FOfcaWcC7CKrqprUrDtZl12du2sqzSVFGW7om\nUFkfOFcLQVUKoOW+BRMl5cY96Ve9LJXeEuxaPlGMhPKtUaIKPikN4l0Flmattx7v\nfezaPFOKHrxSMNgDfoWxbLvGCMOmOiwec9zBmNiedkDIuFRiVSVlz5NLNkXSqhTk\nTy6tL3gH6M/yDS0LBYGEB06LidTsKb9qyoMOISndKOzGM4aCaNpfeMDMt1CFf55j\nPPy3WGu5AgMBAAECggEACyqJK7SviI2oldXVPtYZKljbbQiqNWr0DpMMF2iG44By\n5ZjJm0uNNQetRqYGq0ykWCuIPz3iXMzpB7SjkTO56uu1aE/kyzfd/ALeCI9r0xHE\noTpeeJTGNZq/po7EGGFDjVsw6J/wjwm6nbSDxfkKa9BAH7FnhRmOCd3aqExp+zBN\nDXHaFsTL2hNxiqQEKJnhvrgo3YgZJbtlO2wtlHGEQzPrwqMcHWcpEOZKQmIp3klj\n25qSSwzMD7uhf6tGzBj/iCZKlpqWy/nwxa2ueHXhszs5urGstNtjijzZRmpnopo2\nTOzGSmX1chFyPc9EKMkaTRoBMIQjtuvUsW9olbytTwKBgQDav4Gm7Ol8Agj24XPJ\nrr+ihgOS84AG0YaLJDV5W96JAD+JI1ktZeHzz5aI9W8dyTUbV0aULKy9pJbVOnze\nsLRNKUpCmjYnlWgiBgAaDhu8GVn4gQtvrpU7YDpMadauYIvtx2gR0gOLF2vWatBO\nyqO6y9oQy34BEwkAqAaUNsXj2wKBgQDOsIl0GNl51MqoWj49Zae2tzPMWEaV3pDW\nl+J6MItIMVMjlDy6jhZBIJ4ewWAUvKj/ybbhvE2/UYAyirGPnqxj45A8drpHKrU4\nrSm5JGJX/XPQT3bbQynJDSIVXGIOevHB/59oymsrDNuuk2TTcKQYPh+cmn0KraYu\nsJxB0RdM+wKBgGPYqfthhCmAXEskGU/jncE3XoZC8xsppDn6qxXb4zWxkU6tfdUE\n/h/ljxawwKld4Am8ypBz290sNVTav4h+K65UvHquHS3wOnndN0qtSeePwst1S50M\nmT1i7PlYDg/GLdi4/j20GL3yUNysIKz2PcnTppOn7rna0G8mQuqAHc+lAoGBAJpF\nQhMZiRlaLnlCAjqpaWkDjPH04ZpzKQBDFZPGL++OgVOJDVrhOtMZmWuzQhe4SRvC\nX3JVrmIiXuFai/V6pYlZDQtUQu1jfyfyd7Xs5kpursbIyRoXI1UhLFNtRPMx6Mi4\nFy+MBjl3u3CuKw92f8RoegfVd4dE3Uj0IW0ut4mJAoGACa26f6sk5NQQo918jArJ\nx+WZm22k4ktFbumV4QRejvccH5XOFSTnTjnNV9C2VefRR/1klydzsw/M5kMFms1T\n668vzglAx7zNp4X2WxZ1wpcn8eaRjneDnlo370jms72GOcRonx0gsrCO8QNWqYkJ\n+AILNz2hv4FoIubDyZkD0KI=\n-----END PRIVATE KEY-----\n", + "client_email": "ais-gemini-key-be1e93e9d76147d@423785749889.iam.gserviceaccount.com", + "client_id": "108179228765412243710", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ais-gemini-key-be1e93e9d76147d%40423785749889.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/get_token.py b/get_token.py new file mode 100644 index 0000000..055f5a0 --- /dev/null +++ b/get_token.py @@ -0,0 +1,52 @@ +import time +import json +import os +import urllib.request +import urllib.parse + +try: + import jwt + from cryptography.hazmat.primitives import serialization +except ImportError: + print("Error: Missing dependencies. Run: pip3 install PyJWT cryptography") + exit(1) + +base_path = os.path.expanduser("~/.hammerspoon/") +key_file = os.path.join(base_path, "google-key.json") + +if not os.path.exists(key_file): + print(f"Error: {key_file} not found") + exit(1) + +with open(key_file) as f: + key_data = json.load(f) + +iat = int(time.time()) +exp = iat + 3600 +payload = { + 'iss': key_data['client_email'], + 'sub': key_data['client_email'], + 'aud': 'https://oauth2.googleapis.com/token', + 'iat': iat, + 'exp': exp, + 'scope': 'https://www.googleapis.com/auth/monitoring.read' +} + +try: + # Sign the JWT using the private key from JSON + signed_jwt = jwt.encode(payload, key_data['private_key'], algorithm='RS256') + + # Exchange for Access Token + url = 'https://oauth2.googleapis.com/token' + params = urllib.parse.urlencode({ + 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', + 'assertion': signed_jwt + }).encode('utf-8') + + req = urllib.request.Request(url, data=params, method='POST') + with urllib.request.urlopen(req) as response: + res_data = json.loads(response.read().decode('utf-8')) + print(res_data['access_token']) +except Exception as e: + print(f"Auth Error: {str(e)}") + exit(1) \ No newline at end of file diff --git a/google-key.json b/google-key.json new file mode 100644 index 0000000..bd9d570 --- /dev/null +++ b/google-key.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "gen-lang-client-0842463959", + "private_key_id": "5b48241cc6b22f409552d1d71a58a649c0436c3b", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTUKVfH+AveJdX\nUSMUwpC040n3aVzuIlLofgYAA3z+k2on00WnC1Iz1N//XJKbcZlYQpZC9aCIGeI/\nq/nC+yHGvZnXBBUmrOV/a0VM/075+vGTSR+N7ZDoAN3jTpOZs8VXDgcChu4JHxaq\nnrpa6JTclLTlsADlClmHq+FxlDYOILZ34T3JT6IDksvlDMYRXkTYiX1gVHlBeCKb\n3v4tKsowBAmjxchWxnwy0vOXw7lqEMqojymAwhoF8KyzaKJd4cOEHaMnQCS+nLc1\nWRAKOWs11Qe6Odch6r/bO7b69PE9mHdppNyNHy4C+TCxfM+ZuF/f8CFtu8YfjRr6\nr1nBiI67AgMBAAECggEAFKH4M6yKGDh/FnB+q2U/+dmDfFvWTg4d6EN91HyCtNyx\n7ceMMn5BfXwYjvd7/R9/aIW/5DPVg9R7Kdph3a7lomEa50qnwgn5spJHxvfavz/I\nXKg0I4iBsJV9FNe0Q7qw1wowlweTsGJPtUMgUQYKmusOKqsfuz1idoPK1GszR/eu\nNpc7g6ran4SZh+RyWIM/0NDlnIle+h3nA4wZ57MSCMAYeT926H54WGKrzuFX+tl0\naFNeyhcolj1Yryf36VKOQKa+BiM+AkAQXqqTRt89sUsD4t5ntBEMnKqOuEjji052\nWVOVcbhNPxFSwqRtHqseKPUNvrQGrxEQi5Qf1vGyCQKBgQD3v/7LYg0cBW7+ata9\n8qnZRKxLlxOXgVdER/sRnrlOugD0htdToXsIVb9Tclo1S6wqXVh1ZgWih1EZxGLj\n+j+d59NmXYqIHGp1tR3eXMiEiwscs8KGAQtdXlXNJEqLzBe2M8Rrmc3/uDnXYdza\nAVPF4O0yEb9hJyTC40cTJ+SJPwKBgQDaWg1UUpoV4V3PTEjnt2zBgicb4qh6PBzS\nln8Cm5s4LXm7Z77h2M4ek3bp0Mr6FuT+9RbQXtQ0EXLuFnuE9/U3hmgilrDYYhp3\no+EFsVdGQr6CVWMCxv0KzjlZ3MMggTmjLJkUSKkdrLMxoE9bNRaEJlQ9V1q2swv8\nNhr3i4l/hQKBgEoGph0jGQOsY/PE/JEY9sMij9CuPX5heS+/yjcDlB7/2NU3jRNC\nBr8A0AAhBO8zwyeNaKb7auebQxJN6bZwyZ1m7XWCsaflxbGSAnC0jH1+Bj33QEJG\nAZ1OWJjJJTUMMHGSmjgEZtbntvCyHwlMqDlR8c2qG+LtjUBmJJCGtPSFAoGBAJTI\nJ6z9W+Ds328RG9xIL+LrPJrTptkjfMBBq+mq/ekZk4kO+BIMGObctHo9uxEN6JuR\nhSoWc6HHAfkZeLDyBDUBcJOg+n922XIMSJgIbt+BOy6z3/NUg7eJLar9sjfD1fJJ\nwUUA/bsqoi9+fJQ5aE5Dj8L8cuNqvQ/uDhH/EoHBAoGBAOwyoBPYWO9GVdgAt5eq\nD9wxCONE9flrhn5xpgZ+ROGyg6T0NG5pn4k9bz1HOJ+pQJEBgkpX+4TqSAmKuYvy\nq2QwAfOrTNmgie8oLoYEA3Ha1baiTRCL2R9bvD2VLo8XaMiLGoHc6oHkwi562Vyq\n2J/VKhTdmXl4wC64Lmt3CH4d\n-----END PRIVATE KEY-----\n", + "client_email": "hammerspoon-monitor@gen-lang-client-0842463959.iam.gserviceaccount.com", + "client_id": "110951543695511627948", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/hammerspoon-monitor%40gen-lang-client-0842463959.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/google_monitor.lua b/google_monitor.lua new file mode 100644 index 0000000..f259315 --- /dev/null +++ b/google_monitor.lua @@ -0,0 +1,171 @@ +local obj = {} +obj.__index = obj + +-- CONFIGURATION +local PROJECT_ID = "gen-lang-client-0842463959" +local PROJECT_NAME = "Google API Monitor" +local menu = hs.menubar.new() +local accessToken = nil + +-- Data storage +local history = {} +local lastMetrics = { + requests = 0, + throughput = "0 B", + errors = 0, + latency = "0 ms", + quotaPercent = 0 +} + +-- Helper: Format bytes +local function formatBytes(bytes) + if not bytes or bytes == 0 then return "0 B" end + local units = {"B", "KB", "MB", "GB"} + local i = 1 + while bytes > 1024 and i < #units do + bytes = bytes / 1024 + i = i + 1 + end + return string.format("%.2f %s", bytes, units[i]) +end + +-- Helper: ASCII Trend Graph (Traffic) +local function getTrendString() + if #history < 2 then return "▂ Loading..." end + local bars = {" ", "▂", "▃", "▄", "▅", "▆", "▇", "█"} + local maxVal = 0 + for _, v in ipairs(history) do if v > maxVal then maxVal = v end end + local res = "" + for _, v in ipairs(history) do + local idx = 1 + if maxVal > 0 then + idx = math.floor((v / maxVal) * 7) + 1 + end + res = res .. bars[idx] + end + return res +end + +-- Helper: Token Refresh +local function refreshAuth(callback) + local scriptPath = os.getenv("HOME") .. "/.hammerspoon/get_token.py" + hs.task.new("/usr/bin/python3", function(exitCode, stdOut, stdErr) + if exitCode == 0 then + accessToken = stdOut:gsub("%s+", "") + if callback then callback() end + else + print("Python Auth Error: " .. (stdErr or "Unknown")) + end + end, {scriptPath}):start() +end + +-- Generic Fetch +local function fetchMetric(metricType, callback) + if not accessToken then return end + local now = os.date("!%Y-%m-%dT%H:%M:%SZ") + local start = os.date("!%Y-%m-%dT%H:%M:%SZ", os.time() - 3600) + local baseUrl = "https://monitoring.googleapis.com/v3/projects/" .. PROJECT_ID .. "/timeSeries" + + local fullUrl = baseUrl .. "?" .. + "filter=" .. hs.http.encodeForQuery('metric.type="' .. metricType .. '"') .. + "&interval.startTime=" .. hs.http.encodeForQuery(start) .. + "&interval.endTime=" .. hs.http.encodeForQuery(now) + + hs.http.asyncGet(fullUrl, {Authorization = "Bearer " .. accessToken}, function(status, body) + if status == 200 then + local data = hs.json.decode(body) + local total, count = 0, 0 + if data and data.timeSeries then + for _, series in ipairs(data.timeSeries) do + for _, point in ipairs(series.points or {}) do + local val = point.value.int64Value or point.value.doubleValue or 0 + total = total + tonumber(val) + count = count + 1 + end + end + end + callback(total, count) + elseif status == 401 or status == 403 then + accessToken = nil + end + end) +end + +function obj.updateMenu() + local trend = getTrendString() + local qColor = {white=1} + if lastMetrics.quotaPercent >= 90 then qColor = {red=1} + elseif lastMetrics.quotaPercent >= 75 then qColor = {orange=1} end + + menu:setMenu({ + { title = "Project: " .. PROJECT_NAME, disabled = true }, + { title = "Traffic Trend: " .. trend, disabled = true }, + { title = "-" }, + { title = "Quota Utilization: " .. lastMetrics.quotaPercent .. "%", font={color=qColor} }, + { title = "Total Requests (1h): " .. lastMetrics.requests }, + { title = "Average Latency: " .. lastMetrics.latency }, + { title = "Data Throughput: " .. lastMetrics.throughput }, + { title = "Errors (1h): " .. lastMetrics.errors, font = { color = (lastMetrics.errors > 0 and {red=1} or {white=1}) } }, + { title = "-" }, + { title = "Refresh Now", fn = function() obj.updateQuota() end }, + { title = "Open Google Console", fn = function() hs.urlevent.openURL("https://console.cloud.google.com/apis/dashboard?project=" .. PROJECT_ID) end } + }) +end + +function obj.updateQuota() + if not accessToken then + refreshAuth(function() obj.updateQuota() end) + return + end + + -- 1. Fetch Traffic + fetchMetric("serviceruntime.googleapis.com/api/request_count", function(reqCount) + lastMetrics.requests = reqCount + + -- Update history for trend bars + table.insert(history, reqCount) + if #history > 10 then table.remove(history, 1) end + + -- 2. Fetch Quota Usage vs Limit + fetchMetric("serviceruntime.googleapis.com/quota/allocation/usage", function(usage) + fetchMetric("serviceruntime.googleapis.com/quota/limit", function(limit) + lastMetrics.quotaPercent = (limit > 0) and math.floor((usage / limit) * 100) or 0 + + -- 3. Fetch Throughput and Latency + fetchMetric("serviceruntime.googleapis.com/api/response_sizes", function(bytes) + lastMetrics.throughput = formatBytes(bytes) + fetchMetric("serviceruntime.googleapis.com/api/request_latencies", function(totalLat, count) + lastMetrics.latency = count > 0 and string.format("%.2f ms", (totalLat / count) * 1000) or "0 ms" + + -- 4. Errors + local errFilter = 'serviceruntime.googleapis.com/api/request_count" AND metric.labels.response_code_class!="2xx' + fetchMetric(errFilter, function(errCount) + lastMetrics.errors = errCount + + -- Title Icon Logic + local statusIcon = "" + if errCount > 0 then statusIcon = "🔴 " + elseif lastMetrics.quotaPercent >= 80 then statusIcon = "⚠️ " + end + + menu:setTitle(statusIcon .. "G-API: " .. reqCount) + obj.updateMenu() + end) + end) + end) + end) + end) + end) +end + +function obj.init() + if menu then + menu:setTitle("G-API: ...") + obj.updateMenu() + -- Polling every 5 minutes + obj.timer = hs.timer.doEvery(300, function() obj.updateQuota() end) + obj.updateQuota() + end +end + +return obj \ No newline at end of file diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..284a642 --- /dev/null +++ b/init.lua @@ -0,0 +1,99 @@ +-- ~/.hammerspoon/init.lua +-- Load Config First +-- config = require("Config") +-- +-- Load your HyperKey +require("HyperKey") + +require('SearchWindows') +require('Caffeine') +require('AppBorders') +require('LayoutSelector') +require('System_Tweaks') -- Used for Time Machine Throttle Disable +-- require("Focus") -- Does not work with layout saver +Network = require("NetworkCenter") + +-- Load the window management module +local windowMgr = require("WindowManager") +local productivity = require("productivity") + +-- For Affine +local quickNote = require("affine_quick_note") +quickNote.init() +require("affine_clipper"):init() +--- +-- Load Google API Monitor +-- local googleMonitor = require("google_monitor") +-- googleMonitor.init() +-- + +-- Load Spoon Files +hs.loadSpoon('SpoonInstall') +hs.loadSpoon('SpeedMenu') +hs.loadSpoon('BrewInfo') +-- hs.loadSpoon('Seal') + +--- 3. Run/Configure Spoons + +---- SpeedMenu Config +if spoon.SpeedMenu then + -- 1. Define the Fix Function (includes MAC and IPv6) + local function applyFullMenuFix() + local interface = spoon.SpeedMenu.interface or "en0" + local ssid = hs.wifi.currentNetwork() or "Disconnected" + local details = hs.network.interfaceDetails(interface) + + local ipv4 = (details and details.IPv4) and details.IPv4.Addresses[1] or "N/A" + local ipv6 = (details and details.IPv6) and details.IPv6.Addresses[1] or "N/A" + + -- Get MAC Address via shell + local macaddr = hs.execute('ifconfig ' .. interface .. ' | grep ether | awk \'{print $2}\''):gsub("%s+", "") + + local menuitems = { + { title = "SSID: " .. ssid, fn = function() hs.pasteboard.setContents(ssid) end }, + { title = "IPv4: " .. ipv4, fn = function() hs.pasteboard.setContents(ipv4) end }, + { title = "IPv6: " .. ipv6, fn = function() hs.pasteboard.setContents(ipv6) end }, + { title = "MAC: " .. macaddr, fn = function() hs.pasteboard.setContents(macaddr) end }, + { title = "-" }, + { title = "Rescan Network Interfaces", fn = function() spoon.SpeedMenu:rescan() end } + } + spoon.SpeedMenu.menubar:setMenu(menuitems) + end + + -- 2. Hook the rescan method + local oldRescan = spoon.SpeedMenu.rescan + spoon.SpeedMenu.rescan = function(self) + oldRescan(self) + applyFullMenuFix() + end + + -- 3. Toggle Logic (Starts as OFF) + local speedMenuRunning = false + hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function() + if speedMenuRunning then + spoon.SpeedMenu:stop() + hs.alert.show("SpeedMenu Stopped") + else + spoon.SpeedMenu:start() -- This puts it in the menu bar + applyFullMenuFix() -- This populates the data + hs.alert.show("SpeedMenu Started") + end + speedMenuRunning = not speedMenuRunning + end) +end +---- SpeedMenu Config END + +---- BrewInfo +if spoon.BrewInfo then + hs.hotkey.bind({"cmd", "alt", "ctrl"}, "I", function() + spoon.BrewInfo:showBrewInfo() + end) + hs.hotkey.bind({"cmd", "alt", "ctrl"}, "J", function() + spoon.BrewInfo:showBrewInfoCurSel() + end) +end +---- BrewInfo END + +-- Add this line to the module section of your init.lua + +hs.alert.show("Hammerspoon Config Reloaded") \ No newline at end of file diff --git a/layouts/.DS_Store b/layouts/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/layouts/.DS_Store differ diff --git a/layouts/[D] Coding Workspace.json b/layouts/[D] Coding Workspace.json new file mode 100644 index 0000000..ecfadde --- /dev/null +++ b/layouts/[D] Coding Workspace.json @@ -0,0 +1,52 @@ +{ + "windows" : [ + { + "x" : 1920, + "winTitle" : "AFFiNE", + "y" : 0, + "appName" : "AFFiNE", + "h" : 1080, + "w" : 960, + "bundleID" : "pro.affine.app" + }, + { + "x" : 0, + "winTitle" : "TrueNAS - 192.168.1.135 - Google Chrome", + "y" : 30, + "appName" : "Google Chrome", + "h" : 957, + "w" : 1920, + "bundleID" : "com.google.Chrome" + }, + { + "x" : 2880, + "winTitle" : "francop — -zsh — 134×33", + "y" : 536, + "appName" : "Terminal", + "h" : 544, + "w" : 958, + "bundleID" : "com.apple.Terminal" + }, + { + "x" : 2880, + "winTitle" : "francop — tail -f \/tmp\/litellm.out.log — 134×33", + "y" : 0, + "appName" : "Terminal", + "h" : 544, + "w" : 958, + "bundleID" : "com.apple.Terminal" + }, + { + "x" : 0, + "winTitle" : "WindowManager.lua — .hammerspoon", + "y" : 30, + "appName" : "Code", + "h" : 956, + "w" : 1920, + "bundleID" : "com.microsoft.VSCode" + } + ], + "screenCount" : 3, + "saveTime" : "2026-05-08 23:19:07", + "mode" : "Docked" +} \ No newline at end of file diff --git a/layouts/[L] Coding Workspace.json b/layouts/[L] Coding Workspace.json new file mode 100644 index 0000000..69f013c --- /dev/null +++ b/layouts/[L] Coding Workspace.json @@ -0,0 +1,52 @@ +{ + "screenCount" : 1, + "windows" : [ + { + "x" : 0, + "winTitle" : "AFFiNE", + "appName" : "AFFiNE", + "y" : 33, + "h" : 855, + "w" : 756, + "bundleID" : "pro.affine.app" + }, + { + "x" : 296, + "winTitle" : "2026", + "appName" : "Finder", + "y" : 138, + "h" : 436, + "w" : 920, + "bundleID" : "com.apple.finder" + }, + { + "x" : 19, + "winTitle" : "Getting Started - Getting Started - Google Chrome", + "appName" : "Google Chrome", + "y" : 55, + "h" : 856, + "w" : 1512, + "bundleID" : "com.google.Chrome" + }, + { + "x" : 1107, + "winTitle" : "Gemini Mini Chat — Integrate Ollama with Spark AI", + "appName" : "Gemini", + "y" : 388, + "h" : 500, + "w" : 400, + "bundleID" : "com.google.GeminiMacOS" + }, + { + "x" : 0, + "winTitle" : "Laptop Coding.json — .hammerspoon", + "appName" : "Code", + "y" : 33, + "h" : 856, + "w" : 1512, + "bundleID" : "com.microsoft.VSCode" + } + ], + "saveTime" : "2026-05-08 17:45:56", + "mode" : "Laptop" +} \ No newline at end of file diff --git a/logs/affine_recovery.txt b/logs/affine_recovery.txt new file mode 100644 index 0000000..68bc23e --- /dev/null +++ b/logs/affine_recovery.txt @@ -0,0 +1,335 @@ + +[2026-05-05 16:56:37] +hello, my name is Franco Pellicciotti +line 2 + +------------------- + +[2026-05-05 16:57:45] +What's updated: + 1. LOG_DIR: Added a dedicated variable for the logs folder. + 2. hs.fs.mkdir: Added the command to make sure the folder exists. + 3. RECOVERY_FILE: Changed path from the root to the logs folder. + 4. Full Logic: Re-inserted the showNoteInput() (Chooser) and the full AppleScript dialog functions so your file is back to its original "dual-input" power. +Just update your JavaScript screenshot path (from my previous message) and you're set. +------------------- + +[2026-05-05 17:51:22] +local obj = {} +local json = require("hs.json") +local styledtext = require("hs.styledtext") + +-- ========================================== +-- CONFIGURATION +-- ========================================== +obj.layoutFile = os.getenv("HOME") .. "/.hammerspoon/saved_layout.json" +obj.saveInterval = 300 +obj.isRescued = false +obj.isTransitioning = false +obj.isRestoring = false +obj.wakeTimer = nil +obj.lastScreenCount = #hs.screen.allScreens() +obj.lastSavedTime = "Never" + +local stubbornAppsList = { + ["Gemini"] = true, ["AFFiNE"] = true, ["Terminal"] = true, + ["System Settings"] = true, ["Hammerspoon"] = true +} + +local ignoreListItems = { + ["TheBoringNotch"] = true, ["Control Center"] = true, ["Notification Center"] = true, + ["Dock"] = true, ["BetterDisplay"] = true, ["Stats"] = true, ["DockDoor"] = true, + ["Bartender 6"] = true, ["Bartender"] = true, ["Arq"] = true +} + +-- ========================================== +-- INTERNAL UTILITIES +-- ========================================== + +local function log(msg) + print(string.format("WindowManager [%s]: %s", os.date("%H:%M:%S"), msg)) +end + +local function wrapText(text, limit) + local lines = {} + local currentLine = "" + for word in text:gmatch("%S+") do + if #currentLine + #word >= limit then + table.insert(lines, currentLine) + currentLine = " " .. word + else + currentLine = currentLine == "" and " ↳ " .. word or currentLine .. " " .. word + end + end + table.insert(lines, currentLine) + return lines +end + +-- ========================================== +-- CORE LOGIC +-- ========================================== + +function obj.saveLayout(silent) + local currentScreens = #hs.screen.allScreens() + + if obj.isTransitioning or obj.isRestoring then + log("Save BLOCKED: System busy.") + return + end + + if currentScreens ~= obj.lastScreenCount then + log(string.format("Screen mismatch (%d vs %d). Syncing count...", currentScreens, obj.lastScreenCount)) + obj.lastScreenCount = currentScreens + return + end + + local layout = { + saveTime = os.date("%H:%M:%S"), + screenCount = currentScreens, + windows = {} + } + + for _, win in ipairs(hs.window.allWindows()) do + pcall(function() + local app = win:application() + if app and win:isVisible() and win:frame().w > 0 then + local appName = app:name() or "" + if not ignoreListItems[appName] then + table.insert(layout.windows, { + appName = appName, + bundleID = app:bundleID(), + winTitle = win:title(), + x = math.floor(win:frame().x), + y = math.floor(win:frame().y), + w = math.floor(win:frame().w), + h = math.floor(win:frame().h) + }) + end + end + end) + end + + hs.json.write(layout, obj.layoutFile, true, true) + obj.lastSavedTime = layout.saveTime + + if silent then + log("Autosave successful.") + else + log("Manual save successful.") + hs.alert.show("Layout Saved", 1.5) + end +end + +function obj.restoreLayout() + -- Manual trigger now clears busy flags to force execution + obj.isRestoring = false + obj.isTransitioning = false + + local data = hs.json.read(obj.layoutFile) + if not data or not data.windows then + log("Restore FAILED: No data found in JSON.") + return + end + + obj.isRestoring = true + obj.isRescued = false + hs.screen.restoreGamma() + + local savedByApp = {} + for _, winData in ipairs(data.windows) do + if not ignoreListItems[winData.appName] then + savedByApp[winData.appName] = savedByApp[winData.appName] or {} + table.insert(savedByApp[winData.appName], winData) + end + end + + for appName, entries in pairs(savedByApp) do + table.sort(entries, function(a, b) + if a.y == b.y then return a.x < b.x end + return a.y < b.y + end) + end + + log("RESTORE STARTING...") + + for appName, savedEntries in pairs(savedByApp) do + local firstEntry = savedEntries[1] + local app = hs.application.get(firstEntry.bundleID) or hs.application.get(appName) + + if app then + local physicalWins = {} + for _, w in ipairs(app:allWindows()) do + if w:isVisible() and w:frame().w > 0 then table.insert(physicalWins, w) end + end + + table.sort(physicalWins, function(a, b) + local af, bf = a:frame(), b:frame() + if af.y == bf.y then return af.x < bf.x end + return af.y < bf.y + end) + + for i, winData in ipairs(savedEntries) do + local win = physicalWins[i] + if win then + log(string.format("Moving %s (%d/%d)", appName, i, #savedEntries)) + if win:isMinimized() then win:unminimize() end + + pcall(function() + win:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0) + + if stubbornAppsList[appName] then + hs.timer.doAfter(0.5, function() + pcall(function() win:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0) end) + end) + end + end) + end + end + else + log(string.format("Skip: %s is not running.", appName)) + end + end + + hs.alert.show("Layout Restored", 1.5) + hs.timer.doAfter(5, function() + obj.isRestoring = false + log("RESTORE CYCLE COMPLETE.") + end) +end + +function obj.rescueWindowsToLaptop() + local primary = hs.screen.primaryScreen() + if not primary then return end + local maxFrame = primary:frame() + log("RESCUE: Cascading windows on laptop.") + + local allWindows = hs.window.allWindows() + local staggerOffset = 0 + + for _, win in ipairs(allWindows) do + pcall(function() + local app = win:application() + local appName = app and app:name() or "" + + if win and win:isVisible() and win:frame().w > 0 and not ignoreListItems[appName] then + local f = win:frame() + if f.w > maxFrame.w then f.w = maxFrame.w - 100 end + if f.h > maxFrame.h then f.h = maxFrame.h - 100 end + + f.x = maxFrame.x + (maxFrame.w / 2) - (f.w / 2) + staggerOffset + f.y = maxFrame.y + (maxFrame.h / 2) - (f.h / 2) + staggerOffset + + win:setFrame(f, 0) + staggerOffset = staggerOffset + 30 + if staggerOffset > 150 then staggerOffset = 0 end + end + end) + end + obj.isRescued = true + hs.alert.show("Windows Cascaded", 1.5) +end + +-- ========================================== +-- MENUBAR & WATCHERS +-- ========================================== +local saveCountdown = obj.saveInterval +local timerMenu = hs.menubar.new() + +function updateMenu() + if timerMenu then + local screens = hs.screen.allScreens() + timerMenu:setTitle(string.format("💠 %d:%02d", math.floor(saveCountdown / 60), saveCountdown % 60)) + local menuTable = { + { title = "📅 Last Saved: " .. (obj.lastSavedTime or "Never"), disabled = true }, + { title = ((#screens > 1) and "🖥️ Docked" or "💻 Laptop") .. " (" .. #screens .. " Screens)", disabled = true }, + { title = "-" }, + { title = "📸 Save Layout (⇧⌘W)", fn = function() obj.saveLayout(false); saveCountdown = obj.saveInterval end }, + { title = "🔄 Restore Layout (⇧⌘R)", fn = function() obj.restoreLayout() end }, + { title = "🚀 Rescue Windows (Cascade on Laptop) (⇧⌘⌃L)", fn = obj.rescueWindowsToLaptop }, + { title = "-" }, + { title = "📦 Saved Apps:", disabled = true } + } + + local data = hs.json.read(obj.layoutFile) + if data and data.windows then + local seen, names = {}, {} + for _, w in ipairs(data.windows) do + if not seen[w.appName] then table.insert(names, w.appName .. ", "); seen[w.appName] = true end + end + for _, line in ipairs(wrapText(table.concat(names):gsub(", $", ""), 45)) do + table.insert(menuTable, { title = line, disabled = true }) + end + end + timerMenu:setMenu(menuTable) + end +end + +obj.powerWatcher = hs.caffeinate.watcher.new(function(event) + if event == hs.caffeinate.watcher.systemWillSleep or event == hs.caffeinate.watcher.screensDidSleep or event == hs.caffeinate.watcher.screensDidLock then + log("POWER: Sleep event.") + obj.isTransitioning = true + if obj.autoSaveTimer then obj.autoSaveTimer:stop() end + elseif event == hs.caffeinate.watcher.systemDidWake or event == hs.caffeinate.watcher.screensDidWake or event == hs.caffeinate.watcher.screensDidUnlock then + log("POWER: Wake event.") + saveCountdown = obj.saveInterval + if obj.autoSaveTimer then obj.autoSaveTimer:start() end + if obj.wakeTimer then obj.wakeTimer:stop() end + + obj.wakeTimer = hs.timer.doAfter(12, function() + obj.isTransitioning = false + obj.isRestoring = false + obj.lastScreenCount = #hs.screen.allScreens() + obj.restoreLayout() + obj.wakeTimer = nil + end) + end +end):start() + +hs.hotkey.bind({"shift", "cmd"}, "W", function() obj.saveLayout(false); saveCountdown = obj.saveInterval end) +hs.hotkey.bind({"shift", "cmd"}, "R", function() obj.restoreLayout() end) +hs.hotkey.bind({"shift", "cmd", "ctrl"}, "L", obj.rescueWindowsToLaptop) + +obj.screenWatcher = hs.screen.watcher.new(function() + if obj.isTransitioning or obj.isRestoring or obj.wakeTimer then + log("DOCK EVENT: Ignored.") + return + end + log("DOCK EVENT: Detected.") + obj.isTransitioning = true + hs.timer.doAfter(7, function() + obj.lastScreenCount = #hs.screen.allScreens() + obj.isTransitioning = false + if obj.lastScreenCount > 1 then obj.restoreLayout() else obj.rescueWindowsToLaptop() end + updateMenu() + end) +end):start() + +obj.autoSaveTimer = hs.timer.doEvery(obj.saveInterval, function() + obj.saveLayout(true) + saveCountdown = obj.saveInterval +end) + +obj.clockTimer = hs.timer.doEvery(1, function() + saveCountdown = saveCountdown - 1 + if saveCountdown < 0 then saveCountdown = obj.saveInterval end + updateMenu() +end) + +updateMenu() +return obj +------------------- + +[2026-05-05 17:53:18] +dkhjfkdjflkadjkf dakjf das +dfjdskfjdkalfja +------------------- + +[2026-05-05 17:58:28] +dhfjdhfjkhdsjkfdjfhjdahfa +dfdhjlfhdkjhfdjfg +dfjhdkjfhd +------------------- + +[2026-05-05 18:00:05] +fgfgfgff +------------------- diff --git a/monocle.lua b/monocle.lua new file mode 100644 index 0000000..ceb5c51 --- /dev/null +++ b/monocle.lua @@ -0,0 +1,129 @@ +-- ~/.hammerspoon/Monocle.lua +local Monocle = {} + +-- CONFIGURATION +local blurRadius = 20 +local dimAlpha = 0.3 +local shakeTrigger = 250 -- Energy required (higher = harder shake) + +local overlay = nil +local filter = nil +local mouseTimer = nil +local clickWatcher = nil +local energy = 0 + +function Monocle.show() + local screen = hs.screen.mainScreen() + local res = screen:fullFrame() + local win = hs.window.focusedWindow() + if not win then return end + + if not overlay then + overlay = hs.canvas.new(res) + overlay:level(101) + overlay:behavior({ + hs.canvas.windowBehaviors.canJoinAllSpaces, + hs.canvas.windowBehaviors.ignoresMouseEvents + }) + + -- Layer 1: Background Dimming + overlay[1] = { + type = "rectangle", + action = "fill", + fillColor = { black = 1, alpha = dimAlpha }, + frame = { x = 0, y = 0, w = "100%", h = "100%" }, + } + + -- APPLY THE BLUR FILTER TO THE WHOLE CANVAS + overlay:setFilter({ + name = "CIGaussianBlur", + inputRadius = blurRadius + }) + end + + overlay:frame(res) + overlay:show() + win:raise() +end + +function Monocle.hide() + if overlay then overlay:hide() end + energy = 0 +end + +function Monocle.toggle() + if overlay and overlay:isShowing() then + Monocle.hide() + else + Monocle.show() + end +end + +function Monocle.init() + -- 1. HYPERKEY BINDING + if hyper then + hs.hotkey.bind(hyper, "f", function() + Monocle.toggle() + end) + end + + -- 2. Focus Follower + filter = hs.window.filter.new(nil) + filter:subscribe(hs.window.filter.windowFocused, function() + if overlay and overlay:isShowing() then + Monocle.show() + end + end) + + -- 3. Click Outside to Disable + clickWatcher = hs.eventtap.new({1}, function(event) + if overlay and overlay:isShowing() then + local clickPoint = event:location() + local win = hs.window.focusedWindow() + if win then + local f = win:frame() + local isInside = (clickPoint.x >= f.x and clickPoint.x <= (f.x + f.w) and + clickPoint.y >= f.y and clickPoint.y <= (f.y + f.h)) + if not isInside then + Monocle.hide() + end + end + end + return false + end):start() + + -- 4. Vector-Based Shake Detection + local lastPos = hs.mouse.absolutePosition() + local lastDx, lastDy = 0, 0 + + mouseTimer = hs.timer.doEvery(0.02, function() + local newPos = hs.mouse.absolutePosition() + local dx = newPos.x - lastPos.x + local dy = newPos.y - lastPos.y + + -- If current direction (dx) is opposite of last direction (lastDx) + -- That indicates a shake reversal. + local reversedX = (dx * lastDx < 0) + local reversedY = (dy * lastDy < 0) + + if reversedX or reversedY then + local speed = math.sqrt(dx*dx + dy*dy) + if speed > 15 then + energy = energy + (speed * 2) + end + else + -- Constant decay (15 units per cycle) + energy = math.max(0, energy - 15) + end + + if energy > shakeTrigger then + Monocle.toggle() + energy = 0 + end + + lastDx, lastDy = dx, dy + lastPos = newPos + end) +end + +return Monocle \ No newline at end of file diff --git a/productivity.lua b/productivity.lua new file mode 100644 index 0000000..dfce642 --- /dev/null +++ b/productivity.lua @@ -0,0 +1,85 @@ +local prod = {} + +-- ========================================== +-- 1. SMART APP SWITCHER (Focus & Mouse Center) +-- ========================================== +function prod.focusApp(appName) + local app = hs.application.get(appName) + if app then + local window = app:mainWindow() + if window then + window:unminimize() + window:focus() + local f = window:frame() + -- Centers mouse on the focused window + hs.mouse.absolutePosition({x = f.x + f.w/2, y = f.y + f.h/2}) + end + else + hs.application.launchOrFocus(appName) + end +end + +-- KEY BINDINGS for Item 1 +hs.hotkey.bind({"alt"}, "T", function() prod.focusApp("Terminal") end) +hs.hotkey.bind({"alt"}, "G", function() prod.focusApp("Gemini") end) +hs.hotkey.bind({"alt"}, "V", function() prod.focusApp("Visual Studio Code") end) + +-- ========================================== +-- 2. GHOST HUNTER (Cleanup & Panic Reset) +-- ========================================== +function prod.ghostHunter() + local windows = hs.window.allWindows() + local zombies = 0 + local rescued = 0 + + for _, win in ipairs(windows) do + local app = win:application() + local frame = win:frame() + + -- Identify Zombie (No app or no title) + if not app or (win:title() == "" and not win:isStandard()) then + zombies = zombies + 1 + end + + -- Rescuing windows stuck in "Deep Space" (off-screen coordinates) + if frame.x < -5000 or frame.y < -5000 or frame.x > 10000 or frame.y > 10000 then + win:moveToScreen(hs.screen.primaryScreen()) + rescued = rescued + 1 + end + end + + -- FIX: Replaced forceRefresh() with the default window filter refresh + hs.window.filter.default:getWindows() + + hs.alert.show(string.format("Hunter: %d Ghosts | %d Rescued", zombies, rescued), 2) +end + +-- KEY BINDING for Item 2 (Panic Key) +hs.hotkey.bind({"shift", "alt"}, "G", prod.ghostHunter) + +-- ========================================== +-- 3. CONTEXTUAL MODES (Workspaces) +-- ========================================== +function prod.modeArchitecture() + hs.alert.show("Mode: Architecture (Deep Work)", 2) + hs.application.launchOrFocus("Visual Studio Code") + hs.application.launchOrFocus("Terminal") + -- Hide distractions + local teams = hs.application.get("Microsoft Teams") + if teams then teams:hide() end +end + +function prod.modeCommunication() + hs.alert.show("Mode: Communication (Email/Teams)", 2) + hs.application.launchOrFocus("Microsoft Outlook") + hs.application.launchOrFocus("Microsoft Teams") +end + +-- KEY BINDINGS for Item 3 +hs.hotkey.bind({"shift", "alt"}, "A", prod.modeArchitecture) +hs.hotkey.bind({"shift", "alt"}, "C", prod.modeCommunication) + +-- Loading notification +hs.alert.show("🚀 Productivity Tools Loaded", 1.5) + +return prod \ No newline at end of file diff --git a/saved_layout.json b/saved_layout.json new file mode 100644 index 0000000..aea5d1a --- /dev/null +++ b/saved_layout.json @@ -0,0 +1,51 @@ +{ + "windows" : [ + { + "x" : 0, + "bundleID" : "pro.affine.app", + "y" : 33, + "appName" : "AFFiNE", + "h" : 855, + "w" : 756, + "winTitle" : "AFFiNE" + }, + { + "x" : 0, + "bundleID" : "com.google.Chrome", + "y" : 33, + "appName" : "Google Chrome", + "h" : 856, + "w" : 1512, + "winTitle" : "gitea vscode - Google Search - Google Chrome" + }, + { + "x" : 483, + "bundleID" : "com.apple.Terminal", + "y" : 293, + "appName" : "Terminal", + "h" : 514, + "w" : 909, + "winTitle" : "francop — -zsh — 127×31" + }, + { + "x" : 45, + "bundleID" : "com.apple.Terminal", + "y" : 62, + "appName" : "Terminal", + "h" : 514, + "w" : 909, + "winTitle" : "francop — francop@mediabox3: \/mnt\/gdrive\/Media\/Audiobooks — ssh francop@192.168.1.101 — 127×31" + }, + { + "x" : 0, + "bundleID" : "com.microsoft.VSCode", + "y" : 33, + "appName" : "Code", + "h" : 855, + "w" : 1512, + "winTitle" : "Extension: Gitea for VS Code — .hammerspoon" + } + ], + "screenCount" : 1, + "saveTime" : "15:35:58" +} \ No newline at end of file diff --git a/scripts/.DS_Store b/scripts/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/scripts/.DS_Store differ diff --git a/scripts/auth.json b/scripts/auth.json new file mode 100644 index 0000000..aa5630c --- /dev/null +++ b/scripts/auth.json @@ -0,0 +1,113 @@ +{ + "cookies": [ + { + "name": "affine_session", + "value": "63cc1db4-6544-499c-858b-e3a82a1855f5", + "domain": "myaff.duckdns.org", + "path": "/", + "expires": 1779294796.897129, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "affine_csrf_token", + "value": "e4ecf66b-b50c-4726-b73a-79375f912853", + "domain": "myaff.duckdns.org", + "path": "/", + "expires": 1779294796.897768, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "affine_user_id", + "value": "402ce565-c2bf-499c-94ee-c5e89f3cbb9c", + "domain": "myaff.duckdns.org", + "path": "/", + "expires": -1, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + } + ], + "origins": [ + { + "origin": "http://myaff.duckdns.org:3010", + "localStorage": [ + { + "name": "global-cache:workspace-information:fc2caf2b-3e3e-48cc-9533-4eb63b4753cf", + "value": "{\"name\":\"Franco's Workspace\",\"isOwner\":true,\"isAdmin\":false,\"isTeam\":false,\"isEmpty\":false}" + }, + { + "name": "global-state:workspace-state:aG5wUrCkEh2MGImWPbHBp:recent-pages", + "value": "[\"yOPjnOSCGj\"]" + }, + { + "name": "global-state:cloud-workspace:402ce565-c2bf-499c-94ee-c5e89f3cbb9c", + "value": "[{\"id\":\"fc2caf2b-3e3e-48cc-9533-4eb63b4753cf\",\"flavour\":\"affine-cloud\",\"initialized\":true}]" + }, + { + "name": "affine_telemetry_session_number", + "value": "1" + }, + { + "name": "global-cache:i18n_lng", + "value": "\"en\"" + }, + { + "name": "global-cache:subscription:402ce565-c2bf-499c-94ee-c5e89f3cbb9c", + "value": "[]" + }, + { + "name": "global-cache:workspace-information:aG5wUrCkEh2MGImWPbHBp", + "value": "{\"name\":\"Demo Workspace\",\"isOwner\":true}" + }, + { + "name": "global-state:workspace-state:fc2caf2b-3e3e-48cc-9533-4eb63b4753cf:recent-pages", + "value": "[\"M5HIySO_xVmitXRc3ZjdQ\"]" + }, + { + "name": "global-state:affine-cloud-auth", + "value": "{\"account\":{\"id\":\"402ce565-c2bf-499c-94ee-c5e89f3cbb9c\",\"email\":\"franco.pellicciotti@gmail.com\",\"label\":\"Franco\",\"avatar\":null,\"info\":{\"id\":\"402ce565-c2bf-499c-94ee-c5e89f3cbb9c\",\"email\":\"franco.pellicciotti@gmail.com\",\"avatarUrl\":null,\"name\":\"Franco\",\"disabled\":false,\"hasPassword\":true,\"emailVerified\":false}}}" + }, + { + "name": "global-state:workspace-state:aG5wUrCkEh2MGImWPbHBp:permission", + "value": "{\"isOwner\":true,\"isAdmin\":false,\"isTeam\":false}" + }, + { + "name": "last_page_id", + "value": "M5HIySO_xVmitXRc3ZjdQ" + }, + { + "name": "is-first-open", + "value": "false" + }, + { + "name": "affine_telemetry_client_id", + "value": "hZf8xS8AYmKB4fDZVibca" + }, + { + "name": "global-state:workspace-state:fc2caf2b-3e3e-48cc-9533-4eb63b4753cf:permission", + "value": "{\"isOwner\":true,\"isAdmin\":false,\"isTeam\":false}" + }, + { + "name": "global-state:serverConfig:affine-cloud", + "value": "{\"credentialsRequirement\":{\"password\":{\"minLength\":8,\"maxLength\":32}},\"features\":[\"Comment\",\"LocalWorkspace\",\"CopilotEmbedding\",\"Copilot\"],\"oauthProviders\":[],\"serverName\":\"AFFiNE SelfHosted Cloud\",\"type\":\"Selfhosted\",\"version\":\"0.26.6\",\"initialized\":true}" + }, + { + "name": "last_workspace_id", + "value": "fc2caf2b-3e3e-48cc-9533-4eb63b4753cf" + }, + { + "name": "affine-local-workspace", + "value": "[\"aG5wUrCkEh2MGImWPbHBp\"]" + }, + { + "name": "global-state:workspaceSelectorOpen", + "value": "false" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/error_debug.png b/scripts/error_debug.png new file mode 100644 index 0000000..98ec30a Binary files /dev/null and b/scripts/error_debug.png differ diff --git a/scripts/final_error.png b/scripts/final_error.png new file mode 100644 index 0000000..9956717 Binary files /dev/null and b/scripts/final_error.png differ diff --git a/scripts/login_error.png b/scripts/login_error.png new file mode 100644 index 0000000..3d77a4b Binary files /dev/null and b/scripts/login_error.png differ diff --git a/scripts/node_modules/.bin/playwright b/scripts/node_modules/.bin/playwright new file mode 120000 index 0000000..50992a7 --- /dev/null +++ b/scripts/node_modules/.bin/playwright @@ -0,0 +1 @@ +../playwright/cli.js \ No newline at end of file diff --git a/scripts/node_modules/.bin/playwright-core b/scripts/node_modules/.bin/playwright-core new file mode 120000 index 0000000..08d6c28 --- /dev/null +++ b/scripts/node_modules/.bin/playwright-core @@ -0,0 +1 @@ +../playwright-core/cli.js \ No newline at end of file diff --git a/scripts/node_modules/.package-lock.json b/scripts/node_modules/.package-lock.json new file mode 100644 index 0000000..d5675ab --- /dev/null +++ b/scripts/node_modules/.package-lock.json @@ -0,0 +1,38 @@ +{ + "name": "scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/scripts/node_modules/playwright-core/LICENSE b/scripts/node_modules/playwright-core/LICENSE new file mode 100644 index 0000000..df11237 --- /dev/null +++ b/scripts/node_modules/playwright-core/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Portions Copyright (c) Microsoft Corporation. + Portions Copyright 2017 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/scripts/node_modules/playwright-core/NOTICE b/scripts/node_modules/playwright-core/NOTICE new file mode 100644 index 0000000..814ec16 --- /dev/null +++ b/scripts/node_modules/playwright-core/NOTICE @@ -0,0 +1,5 @@ +Playwright +Copyright (c) Microsoft Corporation + +This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer), +available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE). diff --git a/scripts/node_modules/playwright-core/README.md b/scripts/node_modules/playwright-core/README.md new file mode 100644 index 0000000..422b373 --- /dev/null +++ b/scripts/node_modules/playwright-core/README.md @@ -0,0 +1,3 @@ +# playwright-core + +This package contains the no-browser flavor of [Playwright](http://github.com/microsoft/playwright). diff --git a/scripts/node_modules/playwright-core/ThirdPartyNotices.txt b/scripts/node_modules/playwright-core/ThirdPartyNotices.txt new file mode 100644 index 0000000..8804482 --- /dev/null +++ b/scripts/node_modules/playwright-core/ThirdPartyNotices.txt @@ -0,0 +1,3552 @@ +microsoft/playwright-core + +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION + +This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. + +- @hono/node-server@1.19.11 (https://github.com/honojs/node-server) +- @modelcontextprotocol/sdk@1.28.0 (https://github.com/modelcontextprotocol/typescript-sdk) +- accepts@2.0.0 (https://github.com/jshttp/accepts) +- agent-base@7.1.4 (https://github.com/TooTallNate/proxy-agents) +- ajv-formats@3.0.1 (https://github.com/ajv-validator/ajv-formats) +- ajv@8.18.0 (https://github.com/ajv-validator/ajv) +- balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match) +- body-parser@2.2.2 (https://github.com/expressjs/body-parser) +- brace-expansion@1.1.12 (https://github.com/juliangruber/brace-expansion) +- buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32) +- bytes@3.1.2 (https://github.com/visionmedia/bytes.js) +- call-bind-apply-helpers@1.0.2 (https://github.com/ljharb/call-bind-apply-helpers) +- call-bound@1.0.4 (https://github.com/ljharb/call-bound) +- codemirror@5.65.18 (https://github.com/codemirror/CodeMirror) +- colors@1.4.0 (https://github.com/Marak/colors.js) +- commander@13.1.0 (https://github.com/tj/commander.js) +- concat-map@0.0.1 (https://github.com/substack/node-concat-map) +- content-disposition@1.0.1 (https://github.com/jshttp/content-disposition) +- content-type@1.0.5 (https://github.com/jshttp/content-type) +- cookie-signature@1.2.2 (https://github.com/visionmedia/node-cookie-signature) +- cookie@0.7.2 (https://github.com/jshttp/cookie) +- cors@2.8.5 (https://github.com/expressjs/cors) +- cross-spawn@7.0.6 (https://github.com/moxystudio/node-cross-spawn) +- debug@4.3.4 (https://github.com/debug-js/debug) +- debug@4.4.0 (https://github.com/debug-js/debug) +- debug@4.4.3 (https://github.com/debug-js/debug) +- define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop) +- depd@2.0.0 (https://github.com/dougwilson/nodejs-depd) +- diff@8.0.4 (https://github.com/kpdecker/jsdiff) +- dotenv@16.4.5 (https://github.com/motdotla/dotenv) +- dunder-proto@1.0.1 (https://github.com/es-shims/dunder-proto) +- ee-first@1.1.1 (https://github.com/jonathanong/ee-first) +- encodeurl@2.0.0 (https://github.com/pillarjs/encodeurl) +- end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream) +- es-define-property@1.0.1 (https://github.com/ljharb/es-define-property) +- es-errors@1.3.0 (https://github.com/ljharb/es-errors) +- es-object-atoms@1.1.1 (https://github.com/ljharb/es-object-atoms) +- escape-html@1.0.3 (https://github.com/component/escape-html) +- etag@1.8.1 (https://github.com/jshttp/etag) +- eventsource-parser@3.0.3 (https://github.com/rexxars/eventsource-parser) +- eventsource@3.0.7 (git://git@github.com/EventSource/eventsource) +- express-rate-limit@8.3.1 (https://github.com/express-rate-limit/express-rate-limit) +- express@5.2.1 (https://github.com/expressjs/express) +- fast-deep-equal@3.1.3 (https://github.com/epoberezkin/fast-deep-equal) +- fast-uri@3.1.0 (https://github.com/fastify/fast-uri) +- finalhandler@2.1.1 (https://github.com/pillarjs/finalhandler) +- forwarded@0.2.0 (https://github.com/jshttp/forwarded) +- fresh@2.0.0 (https://github.com/jshttp/fresh) +- function-bind@1.1.2 (https://github.com/Raynos/function-bind) +- get-intrinsic@1.3.0 (https://github.com/ljharb/get-intrinsic) +- get-proto@1.0.1 (https://github.com/ljharb/get-proto) +- get-stream@5.2.0 (https://github.com/sindresorhus/get-stream) +- gopd@1.2.0 (https://github.com/ljharb/gopd) +- graceful-fs@4.2.10 (https://github.com/isaacs/node-graceful-fs) +- has-symbols@1.1.0 (https://github.com/inspect-js/has-symbols) +- hasown@2.0.2 (https://github.com/inspect-js/hasOwn) +- hono@4.12.7 (https://github.com/honojs/hono) +- http-errors@2.0.1 (https://github.com/jshttp/http-errors) +- https-proxy-agent@7.0.6 (https://github.com/TooTallNate/proxy-agents) +- iconv-lite@0.7.2 (https://github.com/pillarjs/iconv-lite) +- inherits@2.0.4 (https://github.com/isaacs/inherits) +- ini@6.0.0 (https://github.com/npm/ini) +- ip-address@10.1.0 (https://github.com/beaugunderson/ip-address) +- ip-address@9.0.5 (https://github.com/beaugunderson/ip-address) +- ipaddr.js@1.9.1 (https://github.com/whitequark/ipaddr.js) +- is-docker@2.2.1 (https://github.com/sindresorhus/is-docker) +- is-promise@4.0.0 (https://github.com/then/is-promise) +- is-wsl@2.2.0 (https://github.com/sindresorhus/is-wsl) +- isexe@2.0.0 (https://github.com/isaacs/isexe) +- jose@6.1.3 (https://github.com/panva/jose) +- jpeg-js@0.4.4 (https://github.com/eugeneware/jpeg-js) +- jsbn@1.1.0 (https://github.com/andyperlitch/jsbn) +- json-schema-traverse@1.0.0 (https://github.com/epoberezkin/json-schema-traverse) +- json-schema-typed@8.0.2 (https://github.com/RemyRylan/json-schema-typed) +- math-intrinsics@1.1.0 (https://github.com/es-shims/math-intrinsics) +- media-typer@1.1.0 (https://github.com/jshttp/media-typer) +- merge-descriptors@2.0.0 (https://github.com/sindresorhus/merge-descriptors) +- mime-db@1.54.0 (https://github.com/jshttp/mime-db) +- mime-types@3.0.2 (https://github.com/jshttp/mime-types) +- mime@3.0.0 (https://github.com/broofa/mime) +- minimatch@3.1.4 (https://github.com/isaacs/minimatch) +- ms@2.1.2 (https://github.com/zeit/ms) +- ms@2.1.3 (https://github.com/vercel/ms) +- negotiator@1.0.0 (https://github.com/jshttp/negotiator) +- object-assign@4.1.1 (https://github.com/sindresorhus/object-assign) +- object-inspect@1.13.4 (https://github.com/inspect-js/object-inspect) +- on-finished@2.4.1 (https://github.com/jshttp/on-finished) +- once@1.4.0 (https://github.com/isaacs/once) +- open@8.4.0 (https://github.com/sindresorhus/open) +- parseurl@1.3.3 (https://github.com/pillarjs/parseurl) +- path-key@3.1.1 (https://github.com/sindresorhus/path-key) +- path-to-regexp@8.3.0 (https://github.com/pillarjs/path-to-regexp) +- pend@1.2.0 (https://github.com/andrewrk/node-pend) +- pkce-challenge@5.0.0 (https://github.com/crouchcd/pkce-challenge) +- pngjs@6.0.0 (https://github.com/lukeapage/pngjs) +- progress@2.0.3 (https://github.com/visionmedia/node-progress) +- proxy-addr@2.0.7 (https://github.com/jshttp/proxy-addr) +- proxy-from-env@2.0.0 (https://github.com/Rob--W/proxy-from-env) +- pump@3.0.2 (https://github.com/mafintosh/pump) +- qs@6.15.0 (https://github.com/ljharb/qs) +- range-parser@1.2.1 (https://github.com/jshttp/range-parser) +- raw-body@3.0.2 (https://github.com/stream-utils/raw-body) +- require-from-string@2.0.2 (https://github.com/floatdrop/require-from-string) +- retry@0.12.0 (https://github.com/tim-kos/node-retry) +- router@2.2.0 (https://github.com/pillarjs/router) +- safer-buffer@2.1.2 (https://github.com/ChALkeR/safer-buffer) +- send@1.2.1 (https://github.com/pillarjs/send) +- serve-static@2.2.1 (https://github.com/expressjs/serve-static) +- setprototypeof@1.2.0 (https://github.com/wesleytodd/setprototypeof) +- shebang-command@2.0.0 (https://github.com/kevva/shebang-command) +- shebang-regex@3.0.0 (https://github.com/sindresorhus/shebang-regex) +- side-channel-list@1.0.0 (https://github.com/ljharb/side-channel-list) +- side-channel-map@1.0.1 (https://github.com/ljharb/side-channel-map) +- side-channel-weakmap@1.0.2 (https://github.com/ljharb/side-channel-weakmap) +- side-channel@1.1.0 (https://github.com/ljharb/side-channel) +- signal-exit@3.0.7 (https://github.com/tapjs/signal-exit) +- smart-buffer@4.2.0 (https://github.com/JoshGlazebrook/smart-buffer) +- socks-proxy-agent@8.0.5 (https://github.com/TooTallNate/proxy-agents) +- socks@2.8.3 (https://github.com/JoshGlazebrook/socks) +- sprintf-js@1.1.3 (https://github.com/alexei/sprintf.js) +- statuses@2.0.2 (https://github.com/jshttp/statuses) +- toidentifier@1.0.1 (https://github.com/component/toidentifier) +- type-is@2.0.1 (https://github.com/jshttp/type-is) +- unpipe@1.0.0 (https://github.com/stream-utils/unpipe) +- vary@1.1.2 (https://github.com/jshttp/vary) +- which@2.0.2 (https://github.com/isaacs/node-which) +- wrappy@1.0.2 (https://github.com/npm/wrappy) +- ws@8.17.1 (https://github.com/websockets/ws) +- yaml@2.8.3 (https://github.com/eemeli/yaml) +- yauzl@3.2.1 (https://github.com/thejoshwolfe/yauzl) +- yazl@2.5.1 (https://github.com/thejoshwolfe/yazl) +- zod-to-json-schema@3.25.1 (https://github.com/StefanTerdell/zod-to-json-schema) +- zod@4.3.6 (https://github.com/colinhacks/zod) + +%% @hono/node-server@1.19.11 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2022 - present, Yusuke Wada and Hono contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF @hono/node-server@1.19.11 AND INFORMATION + +%% @modelcontextprotocol/sdk@1.28.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Anthropic, PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF @modelcontextprotocol/sdk@1.28.0 AND INFORMATION + +%% accepts@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF accepts@2.0.0 AND INFORMATION + +%% agent-base@7.1.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF agent-base@7.1.4 AND INFORMATION + +%% ajv-formats@3.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2020 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF ajv-formats@3.0.1 AND INFORMATION + +%% ajv@8.18.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2015-2021 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF ajv@8.18.0 AND INFORMATION + +%% balanced-match@1.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(MIT) + +Copyright (c) 2013 Julian Gruber <julian@juliangruber.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF balanced-match@1.0.2 AND INFORMATION + +%% body-parser@2.2.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF body-parser@2.2.2 AND INFORMATION + +%% brace-expansion@1.1.12 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF brace-expansion@1.1.12 AND INFORMATION + +%% buffer-crc32@0.2.13 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License + +Copyright (c) 2013 Brian J. Brennan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF buffer-crc32@0.2.13 AND INFORMATION + +%% bytes@3.1.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF bytes@3.1.2 AND INFORMATION + +%% call-bind-apply-helpers@1.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF call-bind-apply-helpers@1.0.2 AND INFORMATION + +%% call-bound@1.0.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF call-bound@1.0.4 AND INFORMATION + +%% codemirror@5.65.18 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (C) 2017 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF codemirror@5.65.18 AND INFORMATION + +%% colors@1.4.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Original Library + - Copyright (c) Marak Squires + +Additional Functionality + - Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF colors@1.4.0 AND INFORMATION + +%% commander@13.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2011 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF commander@13.1.0 AND INFORMATION + +%% concat-map@0.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +This software is released under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF concat-map@0.0.1 AND INFORMATION + +%% content-disposition@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF content-disposition@1.0.1 AND INFORMATION + +%% content-type@1.0.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF content-type@1.0.5 AND INFORMATION + +%% cookie-signature@1.2.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012–2024 LearnBoost and other contributors; + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF cookie-signature@1.2.2 AND INFORMATION + +%% cookie@0.7.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF cookie@0.7.2 AND INFORMATION + +%% cors@2.8.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF cors@2.8.5 AND INFORMATION + +%% cross-spawn@7.0.6 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2018 Made With MOXY Lda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF cross-spawn@7.0.6 AND INFORMATION + +%% debug@4.3.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF debug@4.3.4 AND INFORMATION + +%% debug@4.4.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF debug@4.4.0 AND INFORMATION + +%% debug@4.4.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF debug@4.4.3 AND INFORMATION + +%% define-lazy-prop@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF define-lazy-prop@2.0.0 AND INFORMATION + +%% depd@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2018 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF depd@2.0.0 AND INFORMATION + +%% diff@8.0.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +BSD 3-Clause License + +Copyright (c) 2009-2015, Kevin Decker +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF diff@8.0.4 AND INFORMATION + +%% dotenv@16.4.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015, Scott Motte +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF dotenv@16.4.5 AND INFORMATION + +%% dunder-proto@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 ECMAScript Shims + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF dunder-proto@1.0.1 AND INFORMATION + +%% ee-first@1.1.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF ee-first@1.1.1 AND INFORMATION + +%% encodeurl@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF encodeurl@2.0.0 AND INFORMATION + +%% end-of-stream@1.4.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF end-of-stream@1.4.4 AND INFORMATION + +%% es-define-property@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF es-define-property@1.0.1 AND INFORMATION + +%% es-errors@1.3.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF es-errors@1.3.0 AND INFORMATION + +%% es-object-atoms@1.1.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF es-object-atoms@1.1.1 AND INFORMATION + +%% escape-html@1.0.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012-2013 TJ Holowaychuk +Copyright (c) 2015 Andreas Lubbe +Copyright (c) 2015 Tiancheng "Timothy" Gu + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF escape-html@1.0.3 AND INFORMATION + +%% etag@1.8.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF etag@1.8.1 AND INFORMATION + +%% eventsource-parser@3.0.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2025 Espen Hovlandsdal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF eventsource-parser@3.0.3 AND INFORMATION + +%% eventsource@3.0.7 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License + +Copyright (c) EventSource GitHub organisation + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF eventsource@3.0.7 AND INFORMATION + +%% express-rate-limit@8.3.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +# MIT License + +Copyright 2023 Nathan Friedly, Vedant K + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF express-rate-limit@8.3.1 AND INFORMATION + +%% express@5.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2009-2014 TJ Holowaychuk +Copyright (c) 2013-2014 Roman Shtylman +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF express@5.2.1 AND INFORMATION + +%% fast-deep-equal@3.1.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF fast-deep-equal@3.1.3 AND INFORMATION + +%% fast-uri@3.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2011-2021, Gary Court until https://github.com/garycourt/uri-js/commit/a1acf730b4bba3f1097c9f52e7d9d3aba8cdcaae +Copyright (c) 2021-present The Fastify team +All rights reserved. + +The Fastify team members are listed at https://github.com/fastify/fastify#team. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of any contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * * * + +The complete list of contributors can be found at: +- https://github.com/garycourt/uri-js/graphs/contributors +========================================= +END OF fast-uri@3.1.0 AND INFORMATION + +%% finalhandler@2.1.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF finalhandler@2.1.1 AND INFORMATION + +%% forwarded@0.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF forwarded@0.2.0 AND INFORMATION + +%% fresh@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF fresh@2.0.0 AND INFORMATION + +%% function-bind@1.1.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2013 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF function-bind@1.1.2 AND INFORMATION + +%% get-intrinsic@1.3.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2020 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF get-intrinsic@1.3.0 AND INFORMATION + +%% get-proto@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2025 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF get-proto@1.0.1 AND INFORMATION + +%% get-stream@5.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF get-stream@5.2.0 AND INFORMATION + +%% gopd@1.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2022 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF gopd@1.2.0 AND INFORMATION + +%% graceful-fs@4.2.10 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF graceful-fs@4.2.10 AND INFORMATION + +%% has-symbols@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2016 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF has-symbols@1.1.0 AND INFORMATION + +%% hasown@2.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Jordan Harband and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF hasown@2.0.2 AND INFORMATION + +%% hono@4.12.7 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2021 - present, Yusuke Wada and Hono contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF hono@4.12.7 AND INFORMATION + +%% http-errors@2.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF http-errors@2.0.1 AND INFORMATION + +%% https-proxy-agent@7.0.6 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF https-proxy-agent@7.0.6 AND INFORMATION + +%% iconv-lite@0.7.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2011 Alexander Shtuchkin + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF iconv-lite@0.7.2 AND INFORMATION + +%% inherits@2.0.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF inherits@2.0.4 AND INFORMATION + +%% ini@6.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF ini@6.0.0 AND INFORMATION + +%% ip-address@10.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (C) 2011 by Beau Gunderson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF ip-address@10.1.0 AND INFORMATION + +%% ip-address@9.0.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (C) 2011 by Beau Gunderson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF ip-address@9.0.5 AND INFORMATION + +%% ipaddr.js@1.9.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (C) 2011-2017 whitequark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF ipaddr.js@1.9.1 AND INFORMATION + +%% is-docker@2.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF is-docker@2.2.1 AND INFORMATION + +%% is-promise@4.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF is-promise@4.0.0 AND INFORMATION + +%% is-wsl@2.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF is-wsl@2.2.0 AND INFORMATION + +%% isexe@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF isexe@2.0.0 AND INFORMATION + +%% jose@6.1.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF jose@6.1.3 AND INFORMATION + +%% jpeg-js@0.4.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2014, Eugene Ware +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Eugene Ware nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY EUGENE WARE ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL EUGENE WARE BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF jpeg-js@0.4.4 AND INFORMATION + +%% jsbn@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Licensing +--------- + +This software is covered under the following copyright: + +/* + * Copyright (c) 2003-2005 Tom Wu + * All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, + * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY + * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + * + * IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, + * INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER + * RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF + * THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT + * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * In addition, the following condition applies: + * + * All redistributions must retain an intact copy of this copyright notice + * and disclaimer. + */ + +Address all questions regarding this license to: + + Tom Wu + tjw@cs.Stanford.EDU +========================================= +END OF jsbn@1.1.0 AND INFORMATION + +%% json-schema-traverse@1.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF json-schema-traverse@1.0.0 AND INFORMATION + +%% json-schema-typed@8.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +BSD 2-Clause License + +Original source code is copyright (c) 2019-2025 Remy Rylan + + +All JSON Schema documentation and descriptions are copyright (c): + +2009 [draft-0] IETF Trust , Kris Zyp , +and SitePen (USA) . + +2009 [draft-1] IETF Trust , Kris Zyp , +and SitePen (USA) . + +2010 [draft-2] IETF Trust , Kris Zyp , +and SitePen (USA) . + +2010 [draft-3] IETF Trust , Kris Zyp , +Gary Court , and SitePen (USA) . + +2013 [draft-4] IETF Trust ), Francis Galiegue +, Kris Zyp , Gary Court +, and SitePen (USA) . + +2018 [draft-7] IETF Trust , Austin Wright , +Henry Andrews , Geraint Luff , and +Cloudflare, Inc. . + +2019 [draft-2019-09] IETF Trust , Austin Wright +, Henry Andrews , Ben Hutton +, and Greg Dennis . + +2020 [draft-2020-12] IETF Trust , Austin Wright +, Henry Andrews , Ben Hutton +, and Greg Dennis . + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF json-schema-typed@8.0.2 AND INFORMATION + +%% math-intrinsics@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 ECMAScript Shims + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF math-intrinsics@1.1.0 AND INFORMATION + +%% media-typer@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF media-typer@1.1.0 AND INFORMATION + +%% merge-descriptors@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Jonathan Ong +Copyright (c) Douglas Christopher Wilson +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF merge-descriptors@2.0.0 AND INFORMATION + +%% mime-db@1.54.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF mime-db@1.54.0 AND INFORMATION + +%% mime-types@3.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF mime-types@3.0.2 AND INFORMATION + +%% mime@3.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2010 Benjamin Thomas, Robert Kieffer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF mime@3.0.0 AND INFORMATION + +%% minimatch@3.1.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF minimatch@3.1.4 AND INFORMATION + +%% ms@2.1.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2016 Zeit, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF ms@2.1.2 AND INFORMATION + +%% ms@2.1.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2020 Vercel, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF ms@2.1.3 AND INFORMATION + +%% negotiator@1.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012-2014 Federico Romero +Copyright (c) 2012-2014 Isaac Z. Schlueter +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF negotiator@1.0.0 AND INFORMATION + +%% object-assign@4.1.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF object-assign@4.1.1 AND INFORMATION + +%% object-inspect@1.13.4 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2013 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF object-inspect@1.13.4 AND INFORMATION + +%% on-finished@2.4.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF on-finished@2.4.1 AND INFORMATION + +%% once@1.4.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF once@1.4.0 AND INFORMATION + +%% open@8.4.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF open@8.4.0 AND INFORMATION + +%% parseurl@1.3.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF parseurl@1.3.3 AND INFORMATION + +%% path-key@3.1.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF path-key@3.1.1 AND INFORMATION + +%% path-to-regexp@8.3.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF path-to-regexp@8.3.0 AND INFORMATION + +%% pend@1.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (Expat) + +Copyright (c) 2014 Andrew Kelley + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files +(the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF pend@1.2.0 AND INFORMATION + +%% pkce-challenge@5.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2019 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF pkce-challenge@5.0.0 AND INFORMATION + +%% pngjs@6.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +pngjs2 original work Copyright (c) 2015 Luke Page & Original Contributors +pngjs derived work Copyright (c) 2012 Kuba Niegowski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF pngjs@6.0.0 AND INFORMATION + +%% progress@2.0.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2017 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF progress@2.0.3 AND INFORMATION + +%% proxy-addr@2.0.7 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF proxy-addr@2.0.7 AND INFORMATION + +%% proxy-from-env@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License + +Copyright (C) 2016-2018 Rob Wu + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF proxy-from-env@2.0.0 AND INFORMATION + +%% pump@3.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Mathias Buus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF pump@3.0.2 AND INFORMATION + +%% qs@6.15.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF qs@6.15.0 AND INFORMATION + +%% range-parser@1.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF raw-body@3.0.2 AND INFORMATION + +%% require-from-string@2.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) Vsevolod Strukchinsky (github.com/floatdrop) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF require-from-string@2.0.2 AND INFORMATION + +%% retry@0.12.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2011: +Tim Koschützki (tim@debuggable.com) +Felix Geisendörfer (felix@debuggable.com) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +========================================= +END OF retry@0.12.0 AND INFORMATION + +%% router@2.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Roman Shtylman +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF router@2.2.0 AND INFORMATION + +%% safer-buffer@2.1.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2018 Nikita Skovoroda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF safer-buffer@2.1.2 AND INFORMATION + +%% send@1.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF send@1.2.1 AND INFORMATION + +%% serve-static@2.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2010 Sencha Inc. +Copyright (c) 2011 LearnBoost +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF serve-static@2.2.1 AND INFORMATION + +%% setprototypeof@1.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015, Wes Todd + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF setprototypeof@1.2.0 AND INFORMATION + +%% shebang-command@2.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Kevin Mårtensson (github.com/kevva) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF shebang-command@2.0.0 AND INFORMATION + +%% shebang-regex@3.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF shebang-regex@3.0.0 AND INFORMATION + +%% side-channel-list@1.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF side-channel-list@1.0.0 AND INFORMATION + +%% side-channel-map@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF side-channel-map@1.0.1 AND INFORMATION + +%% side-channel-weakmap@1.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2019 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF side-channel-weakmap@1.0.2 AND INFORMATION + +%% side-channel@1.1.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2019 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF side-channel@1.1.0 AND INFORMATION + +%% signal-exit@3.0.7 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) 2015, Contributors + +Permission to use, copy, modify, and/or distribute this software +for any purpose with or without fee is hereby granted, provided +that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF signal-exit@3.0.7 AND INFORMATION + +%% smart-buffer@4.2.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2013-2017 Josh Glazebrook + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF smart-buffer@4.2.0 AND INFORMATION + +%% socks-proxy-agent@8.0.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2013 Nathan Rajlich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF socks-proxy-agent@8.0.5 AND INFORMATION + +%% socks@2.8.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2013 Josh Glazebrook + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF socks@2.8.3 AND INFORMATION + +%% sprintf-js@1.1.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2007-present, Alexandru Mărășteanu +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of this software nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF sprintf-js@1.1.3 AND INFORMATION + +%% statuses@2.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +========================================= +END OF statuses@2.0.2 AND INFORMATION + +%% toidentifier@1.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF toidentifier@1.0.1 AND INFORMATION + +%% type-is@2.0.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF type-is@2.0.1 AND INFORMATION + +%% unpipe@1.0.0 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF unpipe@1.0.0 AND INFORMATION + +%% vary@1.1.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF vary@1.1.2 AND INFORMATION + +%% which@2.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF which@2.0.2 AND INFORMATION + +%% wrappy@1.0.2 NOTICES AND INFORMATION BEGIN HERE +========================================= +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF wrappy@1.0.2 AND INFORMATION + +%% ws@8.17.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2011 Einar Otto Stangvik +Copyright (c) 2013 Arnout Kazemier and contributors +Copyright (c) 2016 Luigi Pinca and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF ws@8.17.1 AND INFORMATION + +%% yaml@2.8.3 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright Eemeli Aro + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. +========================================= +END OF yaml@2.8.3 AND INFORMATION + +%% yauzl@3.2.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Josh Wolfe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF yauzl@3.2.1 AND INFORMATION + +%% yazl@2.5.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +The MIT License (MIT) + +Copyright (c) 2014 Josh Wolfe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF yazl@2.5.1 AND INFORMATION + +%% zod-to-json-schema@3.25.1 NOTICES AND INFORMATION BEGIN HERE +========================================= +ISC License + +Copyright (c) 2020, Stefan Terdell + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +========================================= +END OF zod-to-json-schema@3.25.1 AND INFORMATION + +%% zod@4.3.6 NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2025 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF zod@4.3.6 AND INFORMATION + +SUMMARY BEGIN HERE +========================================= +Total Packages: 133 +========================================= +END OF SUMMARY \ No newline at end of file diff --git a/scripts/node_modules/playwright-core/bin/install_media_pack.ps1 b/scripts/node_modules/playwright-core/bin/install_media_pack.ps1 new file mode 100644 index 0000000..6170754 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/install_media_pack.ps1 @@ -0,0 +1,5 @@ +$osInfo = Get-WmiObject -Class Win32_OperatingSystem +# check if running on Windows Server +if ($osInfo.ProductType -eq 3) { + Install-WindowsFeature Server-Media-Foundation +} diff --git a/scripts/node_modules/playwright-core/bin/install_webkit_wsl.ps1 b/scripts/node_modules/playwright-core/bin/install_webkit_wsl.ps1 new file mode 100644 index 0000000..ccaaf15 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/install_webkit_wsl.ps1 @@ -0,0 +1,33 @@ +$ErrorActionPreference = 'Stop' + +# This script sets up a WSL distribution that will be used to run WebKit. + +$Distribution = "playwright" +$Username = "pwuser" + +$distributions = (wsl --list --quiet) -split "\r?\n" +if ($distributions -contains $Distribution) { + Write-Host "WSL distribution '$Distribution' already exists. Skipping installation." +} else { + Write-Host "Installing new WSL distribution '$Distribution'..." + $VhdSize = "10GB" + wsl --install -d Ubuntu-24.04 --name $Distribution --no-launch --vhd-size $VhdSize + wsl -d $Distribution -u root adduser --gecos GECOS --disabled-password $Username +} + +$pwshDirname = (Resolve-Path -Path $PSScriptRoot).Path; +$playwrightCoreRoot = Resolve-Path (Join-Path $pwshDirname "..") + +$initScript = @" +if [ ! -f "/home/$Username/node/bin/node" ]; then + mkdir -p /home/$Username/node + curl -fsSL https://nodejs.org/dist/v22.17.0/node-v22.17.0-linux-x64.tar.xz -o /home/$Username/node/node-v22.17.0-linux-x64.tar.xz + tar -xJf /home/$Username/node/node-v22.17.0-linux-x64.tar.xz -C /home/$Username/node --strip-components=1 + sudo -u $Username echo 'export PATH=/home/$Username/node/bin:\`$PATH' >> /home/$Username/.profile +fi +/home/$Username/node/bin/node cli.js install-deps webkit +sudo -u $Username PLAYWRIGHT_SKIP_BROWSER_GC=1 /home/$Username/node/bin/node cli.js install webkit +"@ -replace "\r\n", "`n" + +wsl -d $Distribution --cd $playwrightCoreRoot -u root -- bash -c "$initScript" +Write-Host "Done!" \ No newline at end of file diff --git a/scripts/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh b/scripts/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh new file mode 100755 index 0000000..0451bda --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old beta if any. +if dpkg --get-selections | grep -q "^google-chrome-beta[[:space:]]*install$" >/dev/null; then + apt-get remove -y google-chrome-beta +fi + +# 2. Update apt lists (needed to install curl and chrome dependencies) +apt-get update + +# 3. Install curl to download chrome +if ! command -v curl >/dev/null; then + apt-get install -y curl +fi + +# 4. download chrome beta from dl.google.com and install it. +cd /tmp +curl -O https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb +apt-get install -y ./google-chrome-beta_current_amd64.deb +rm -rf ./google-chrome-beta_current_amd64.deb +cd - +google-chrome-beta --version diff --git a/scripts/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh b/scripts/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh new file mode 100755 index 0000000..617e3b5 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e +set -x + +rm -rf "/Applications/Google Chrome Beta.app" +cd /tmp +curl --retry 3 -o ./googlechromebeta.dmg https://dl.google.com/chrome/mac/universal/beta/googlechromebeta.dmg +hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechromebeta.dmg ./googlechromebeta.dmg +cp -pR "/Volumes/googlechromebeta.dmg/Google Chrome Beta.app" /Applications +hdiutil detach /Volumes/googlechromebeta.dmg +rm -rf /tmp/googlechromebeta.dmg + +/Applications/Google\ Chrome\ Beta.app/Contents/MacOS/Google\ Chrome\ Beta --version diff --git a/scripts/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 b/scripts/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 new file mode 100644 index 0000000..3fbe551 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = 'Stop' + +$url = 'https://dl.google.com/tag/s/dl/chrome/install/beta/googlechromebetastandaloneenterprise64.msi' + +Write-Host "Downloading Google Chrome Beta" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\google-chrome-beta.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Google Chrome Beta" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Google\\Chrome Beta\\Application\\chrome.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Google Chrome Beta." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/scripts/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh b/scripts/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh new file mode 100755 index 0000000..78f1d41 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old stable if any. +if dpkg --get-selections | grep -q "^google-chrome[[:space:]]*install$" >/dev/null; then + apt-get remove -y google-chrome +fi + +# 2. Update apt lists (needed to install curl and chrome dependencies) +apt-get update + +# 3. Install curl to download chrome +if ! command -v curl >/dev/null; then + apt-get install -y curl +fi + +# 4. download chrome stable from dl.google.com and install it. +cd /tmp +curl -O https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +apt-get install -y ./google-chrome-stable_current_amd64.deb +rm -rf ./google-chrome-stable_current_amd64.deb +cd - +google-chrome --version diff --git a/scripts/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh b/scripts/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh new file mode 100755 index 0000000..6aa650a --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -e +set -x + +rm -rf "/Applications/Google Chrome.app" +cd /tmp +curl --retry 3 -o ./googlechrome.dmg https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg +hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechrome.dmg ./googlechrome.dmg +cp -pR "/Volumes/googlechrome.dmg/Google Chrome.app" /Applications +hdiutil detach /Volumes/googlechrome.dmg +rm -rf /tmp/googlechrome.dmg +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version diff --git a/scripts/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 b/scripts/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 new file mode 100644 index 0000000..7ca2dba --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = 'Stop' +$url = 'https://dl.google.com/tag/s/dl/chrome/install/googlechromestandaloneenterprise64.msi' + +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\google-chrome.msi" +Write-Host "Downloading Google Chrome" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Google Chrome" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + + +$suffix = "\\Google\\Chrome\\Application\\chrome.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Google Chrome." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/scripts/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh b/scripts/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh new file mode 100755 index 0000000..a1531a9 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old beta if any. +if dpkg --get-selections | grep -q "^microsoft-edge-beta[[:space:]]*install$" >/dev/null; then + apt-get remove -y microsoft-edge-beta +fi + +# 2. Install curl to download Microsoft gpg key +if ! command -v curl >/dev/null; then + apt-get update + apt-get install -y curl +fi + +# GnuPG is not preinstalled in slim images +if ! command -v gpg >/dev/null; then + apt-get update + apt-get install -y gpg +fi + +# 3. Add the GPG key, the apt repo, update the apt cache, and install the package +curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg +install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ +sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list' +rm /tmp/microsoft.gpg +apt-get update && apt-get install -y microsoft-edge-beta + +microsoft-edge-beta --version diff --git a/scripts/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh b/scripts/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh new file mode 100755 index 0000000..72ec3e4 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +set -x + +cd /tmp +curl --retry 3 -o ./msedge_beta.pkg "$1" +# Note: there's no way to uninstall previously installed MSEdge. +# However, running PKG again seems to update installation. +sudo installer -pkg /tmp/msedge_beta.pkg -target / +rm -rf /tmp/msedge_beta.pkg +/Applications/Microsoft\ Edge\ Beta.app/Contents/MacOS/Microsoft\ Edge\ Beta --version diff --git a/scripts/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 b/scripts/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 new file mode 100644 index 0000000..cce0d0b --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = 'Stop' +$url = $args[0] + +Write-Host "Downloading Microsoft Edge Beta" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\microsoft-edge-beta.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Microsoft Edge Beta" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Microsoft\\Edge Beta\\Application\\msedge.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Microsoft Edge Beta." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/scripts/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh b/scripts/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh new file mode 100755 index 0000000..7fde34e --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old dev if any. +if dpkg --get-selections | grep -q "^microsoft-edge-dev[[:space:]]*install$" >/dev/null; then + apt-get remove -y microsoft-edge-dev +fi + +# 2. Install curl to download Microsoft gpg key +if ! command -v curl >/dev/null; then + apt-get update + apt-get install -y curl +fi + +# GnuPG is not preinstalled in slim images +if ! command -v gpg >/dev/null; then + apt-get update + apt-get install -y gpg +fi + +# 3. Add the GPG key, the apt repo, update the apt cache, and install the package +curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg +install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ +sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list' +rm /tmp/microsoft.gpg +apt-get update && apt-get install -y microsoft-edge-dev + +microsoft-edge-dev --version diff --git a/scripts/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh b/scripts/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh new file mode 100755 index 0000000..3376e86 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +set -x + +cd /tmp +curl --retry 3 -o ./msedge_dev.pkg "$1" +# Note: there's no way to uninstall previously installed MSEdge. +# However, running PKG again seems to update installation. +sudo installer -pkg /tmp/msedge_dev.pkg -target / +rm -rf /tmp/msedge_dev.pkg +/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev --version diff --git a/scripts/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 b/scripts/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 new file mode 100644 index 0000000..22e6db8 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1 @@ -0,0 +1,23 @@ +$ErrorActionPreference = 'Stop' +$url = $args[0] + +Write-Host "Downloading Microsoft Edge Dev" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\microsoft-edge-dev.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Microsoft Edge Dev" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Microsoft\\Edge Dev\\Application\\msedge.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Microsoft Edge Dev." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} diff --git a/scripts/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh b/scripts/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh new file mode 100755 index 0000000..4acb1db --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -e +set -x + +if [[ $(arch) == "aarch64" ]]; then + echo "ERROR: not supported on Linux Arm64" + exit 1 +fi + +if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then + if [[ ! -f "/etc/os-release" ]]; then + echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)" + exit 1 + fi + + ID=$(bash -c 'source /etc/os-release && echo $ID') + if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then + echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported" + exit 1 + fi +fi + +# 1. make sure to remove old stable if any. +if dpkg --get-selections | grep -q "^microsoft-edge-stable[[:space:]]*install$" >/dev/null; then + apt-get remove -y microsoft-edge-stable +fi + +# 2. Install curl to download Microsoft gpg key +if ! command -v curl >/dev/null; then + apt-get update + apt-get install -y curl +fi + +# GnuPG is not preinstalled in slim images +if ! command -v gpg >/dev/null; then + apt-get update + apt-get install -y gpg +fi + +# 3. Add the GPG key, the apt repo, update the apt cache, and install the package +curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg +install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/ +sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-stable.list' +rm /tmp/microsoft.gpg +apt-get update && apt-get install -y microsoft-edge-stable + +microsoft-edge-stable --version diff --git a/scripts/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh b/scripts/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh new file mode 100755 index 0000000..afcd2f5 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -e +set -x + +cd /tmp +curl --retry 3 -o ./msedge_stable.pkg "$1" +# Note: there's no way to uninstall previously installed MSEdge. +# However, running PKG again seems to update installation. +sudo installer -pkg /tmp/msedge_stable.pkg -target / +rm -rf /tmp/msedge_stable.pkg +/Applications/Microsoft\ Edge.app/Contents/MacOS/Microsoft\ Edge --version diff --git a/scripts/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 b/scripts/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 new file mode 100644 index 0000000..31fdf51 --- /dev/null +++ b/scripts/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1 @@ -0,0 +1,24 @@ +$ErrorActionPreference = 'Stop' + +$url = $args[0] + +Write-Host "Downloading Microsoft Edge" +$wc = New-Object net.webclient +$msiInstaller = "$env:temp\microsoft-edge-stable.msi" +$wc.Downloadfile($url, $msiInstaller) + +Write-Host "Installing Microsoft Edge" +$arguments = "/i `"$msiInstaller`" /quiet" +Start-Process msiexec.exe -ArgumentList $arguments -Wait +Remove-Item $msiInstaller + +$suffix = "\\Microsoft\\Edge\\Application\\msedge.exe" +if (Test-Path "${env:ProgramFiles(x86)}$suffix") { + (Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo +} elseif (Test-Path "${env:ProgramFiles}$suffix") { + (Get-Item "${env:ProgramFiles}$suffix").VersionInfo +} else { + Write-Host "ERROR: Failed to install Microsoft Edge." + Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help." + exit 1 +} \ No newline at end of file diff --git a/scripts/node_modules/playwright-core/browsers.json b/scripts/node_modules/playwright-core/browsers.json new file mode 100644 index 0000000..aff3102 --- /dev/null +++ b/scripts/node_modules/playwright-core/browsers.json @@ -0,0 +1,81 @@ +{ + "comment": "Do not edit this file, use utils/roll_browser.js", + "browsers": [ + { + "name": "chromium", + "revision": "1217", + "installByDefault": true, + "browserVersion": "147.0.7727.15", + "title": "Chrome for Testing" + }, + { + "name": "chromium-headless-shell", + "revision": "1217", + "installByDefault": true, + "browserVersion": "147.0.7727.15", + "title": "Chrome Headless Shell" + }, + { + "name": "chromium-tip-of-tree", + "revision": "1417", + "installByDefault": false, + "browserVersion": "148.0.7755.0", + "title": "Chrome Canary for Testing" + }, + { + "name": "chromium-tip-of-tree-headless-shell", + "revision": "1417", + "installByDefault": false, + "browserVersion": "148.0.7755.0", + "title": "Chrome Canary Headless Shell" + }, + { + "name": "firefox", + "revision": "1511", + "installByDefault": true, + "browserVersion": "148.0.2", + "title": "Firefox" + }, + { + "name": "firefox-beta", + "revision": "1505", + "installByDefault": false, + "browserVersion": "148.0b9", + "title": "Firefox Beta" + }, + { + "name": "webkit", + "revision": "2272", + "installByDefault": true, + "revisionOverrides": { + "mac14": "2251", + "mac14-arm64": "2251", + "debian11-x64": "2105", + "debian11-arm64": "2105", + "ubuntu20.04-x64": "2092", + "ubuntu20.04-arm64": "2092" + }, + "browserVersion": "26.4", + "title": "WebKit" + }, + { + "name": "ffmpeg", + "revision": "1011", + "installByDefault": true, + "revisionOverrides": { + "mac12": "1010", + "mac12-arm64": "1010" + } + }, + { + "name": "winldd", + "revision": "1007", + "installByDefault": false + }, + { + "name": "android", + "revision": "1001", + "installByDefault": false + } + ] +} diff --git a/scripts/node_modules/playwright-core/cli.js b/scripts/node_modules/playwright-core/cli.js new file mode 100755 index 0000000..fb309ea --- /dev/null +++ b/scripts/node_modules/playwright-core/cli.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const { program } = require('./lib/cli/programWithTestStub'); +program.parse(process.argv); diff --git a/scripts/node_modules/playwright-core/index.d.ts b/scripts/node_modules/playwright-core/index.d.ts new file mode 100644 index 0000000..97c1493 --- /dev/null +++ b/scripts/node_modules/playwright-core/index.d.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types/types'; diff --git a/scripts/node_modules/playwright-core/index.js b/scripts/node_modules/playwright-core/index.js new file mode 100644 index 0000000..d4991d0 --- /dev/null +++ b/scripts/node_modules/playwright-core/index.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const minimumMajorNodeVersion = 18; +const currentNodeVersion = process.versions.node; +const semver = currentNodeVersion.split('.'); +const [major] = [+semver[0]]; + +if (major < minimumMajorNodeVersion) { + console.error( + 'You are running Node.js ' + + currentNodeVersion + + '.\n' + + `Playwright requires Node.js ${minimumMajorNodeVersion} or higher. \n` + + 'Please update your version of Node.js.' + ); + process.exit(1); +} + +module.exports = require('./lib/inprocess'); diff --git a/scripts/node_modules/playwright-core/index.mjs b/scripts/node_modules/playwright-core/index.mjs new file mode 100644 index 0000000..3b3c75b --- /dev/null +++ b/scripts/node_modules/playwright-core/index.mjs @@ -0,0 +1,28 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import playwright from './index.js'; + +export const chromium = playwright.chromium; +export const firefox = playwright.firefox; +export const webkit = playwright.webkit; +export const selectors = playwright.selectors; +export const devices = playwright.devices; +export const errors = playwright.errors; +export const request = playwright.request; +export const _electron = playwright._electron; +export const _android = playwright._android; +export default playwright; diff --git a/scripts/node_modules/playwright-core/lib/androidServerImpl.js b/scripts/node_modules/playwright-core/lib/androidServerImpl.js new file mode 100644 index 0000000..568548b --- /dev/null +++ b/scripts/node_modules/playwright-core/lib/androidServerImpl.js @@ -0,0 +1,65 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var androidServerImpl_exports = {}; +__export(androidServerImpl_exports, { + AndroidServerLauncherImpl: () => AndroidServerLauncherImpl +}); +module.exports = __toCommonJS(androidServerImpl_exports); +var import_playwrightServer = require("./remote/playwrightServer"); +var import_playwright = require("./server/playwright"); +var import_crypto = require("./server/utils/crypto"); +var import_utilsBundle = require("./utilsBundle"); +var import_progress = require("./server/progress"); +class AndroidServerLauncherImpl { + async launchServer(options = {}) { + const playwright = (0, import_playwright.createPlaywright)({ sdkLanguage: "javascript", isServer: true }); + const controller = new import_progress.ProgressController(); + let devices = await controller.run((progress) => playwright.android.devices(progress, { + host: options.adbHost, + port: options.adbPort, + omitDriverInstall: options.omitDriverInstall + })); + if (devices.length === 0) + throw new Error("No devices found"); + if (options.deviceSerialNumber) { + devices = devices.filter((d) => d.serial === options.deviceSerialNumber); + if (devices.length === 0) + throw new Error(`No device with serial number '${options.deviceSerialNumber}' was found`); + } + if (devices.length > 1) + throw new Error(`More than one device found. Please specify deviceSerialNumber`); + const device = devices[0]; + const path = options.wsPath ? options.wsPath.startsWith("/") ? options.wsPath : `/${options.wsPath}` : `/${(0, import_crypto.createGuid)()}`; + const server = new import_playwrightServer.PlaywrightServer({ mode: "launchServer", path, maxConnections: 1, preLaunchedAndroidDevice: device }); + const wsEndpoint = await server.listen(options.port, options.host); + const browserServer = new import_utilsBundle.ws.EventEmitter(); + browserServer.wsEndpoint = () => wsEndpoint; + browserServer.close = () => device.close(); + browserServer.kill = () => device.close(); + device.on("close", () => { + server.close(); + browserServer.emit("close"); + }); + return browserServer; + } +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + AndroidServerLauncherImpl +}); diff --git a/scripts/node_modules/playwright-core/lib/bootstrap.js b/scripts/node_modules/playwright-core/lib/bootstrap.js new file mode 100644 index 0000000..f00db60 --- /dev/null +++ b/scripts/node_modules/playwright-core/lib/bootstrap.js @@ -0,0 +1,77 @@ +"use strict"; +if (process.env.PW_INSTRUMENT_MODULES) { + const Module = require("module"); + const originalLoad = Module._load; + const root = { name: "", selfMs: 0, totalMs: 0, childrenMs: 0, children: [] }; + let current = root; + const stack = []; + Module._load = function(request, _parent, _isMain) { + const node = { name: request, selfMs: 0, totalMs: 0, childrenMs: 0, children: [] }; + current.children.push(node); + stack.push(current); + current = node; + const start = performance.now(); + let result; + try { + result = originalLoad.apply(this, arguments); + } catch (e) { + current = stack.pop(); + current.children.pop(); + throw e; + } + const duration = performance.now() - start; + node.totalMs = duration; + node.selfMs = Math.max(0, duration - node.childrenMs); + current = stack.pop(); + current.childrenMs += duration; + return result; + }; + process.on("exit", () => { + function printTree(node, prefix, isLast, lines2, depth) { + if (node.totalMs < 1 && depth > 0) + return; + const connector = depth === 0 ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "; + const time = `${node.totalMs.toFixed(1).padStart(8)}ms`; + const self = node.children.length ? ` (self: ${node.selfMs.toFixed(1)}ms)` : ""; + lines2.push(`${time} ${prefix}${connector}${node.name}${self}`); + const childPrefix = prefix + (depth === 0 ? "" : isLast ? " " : "\u2502 "); + const sorted2 = node.children.slice().sort((a, b) => b.totalMs - a.totalMs); + for (let i = 0; i < sorted2.length; i++) + printTree(sorted2[i], childPrefix, i === sorted2.length - 1, lines2, depth + 1); + } + let totalModules = 0; + function count(n) { + totalModules++; + n.children.forEach(count); + } + root.children.forEach(count); + const lines = []; + const sorted = root.children.slice().sort((a, b) => b.totalMs - a.totalMs); + for (let i = 0; i < sorted.length; i++) + printTree(sorted[i], "", i === sorted.length - 1, lines, 0); + const totalMs = root.children.reduce((s, c) => s + c.totalMs, 0); + process.stderr.write(` +--- Module load tree: ${totalModules} modules, ${totalMs.toFixed(0)}ms total --- +` + lines.join("\n") + "\n"); + const flat = /* @__PURE__ */ new Map(); + function gather(n) { + const existing = flat.get(n.name); + if (existing) { + existing.selfMs += n.selfMs; + existing.totalMs += n.totalMs; + existing.count++; + } else { + flat.set(n.name, { selfMs: n.selfMs, totalMs: n.totalMs, count: 1 }); + } + n.children.forEach(gather); + } + root.children.forEach(gather); + const top50 = [...flat.entries()].sort((a, b) => b[1].selfMs - a[1].selfMs).slice(0, 50); + const flatLines = top50.map( + ([mod, { selfMs, totalMs: totalMs2, count: count2 }]) => `${selfMs.toFixed(1).padStart(8)}ms self ${totalMs2.toFixed(1).padStart(8)}ms total (x${String(count2).padStart(3)}) ${mod}` + ); + process.stderr.write(` +--- Top 50 modules by self time --- +` + flatLines.join("\n") + "\n"); + }); +} diff --git a/scripts/node_modules/playwright-core/lib/browserServerImpl.js b/scripts/node_modules/playwright-core/lib/browserServerImpl.js new file mode 100644 index 0000000..ac2b25d --- /dev/null +++ b/scripts/node_modules/playwright-core/lib/browserServerImpl.js @@ -0,0 +1,120 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var browserServerImpl_exports = {}; +__export(browserServerImpl_exports, { + BrowserServerLauncherImpl: () => BrowserServerLauncherImpl +}); +module.exports = __toCommonJS(browserServerImpl_exports); +var import_playwrightServer = require("./remote/playwrightServer"); +var import_helper = require("./server/helper"); +var import_playwright = require("./server/playwright"); +var import_crypto = require("./server/utils/crypto"); +var import_debug = require("./server/utils/debug"); +var import_stackTrace = require("./utils/isomorphic/stackTrace"); +var import_time = require("./utils/isomorphic/time"); +var import_utilsBundle = require("./utilsBundle"); +var validatorPrimitives = __toESM(require("./protocol/validatorPrimitives")); +var import_progress = require("./server/progress"); +class BrowserServerLauncherImpl { + constructor(browserName) { + this._browserName = browserName; + } + async launchServer(options = {}) { + const playwright = (0, import_playwright.createPlaywright)({ sdkLanguage: "javascript", isServer: true }); + const metadata = { id: "", startTime: 0, endTime: 0, type: "Internal", method: "", params: {}, log: [], internal: true }; + const validatorContext = { + tChannelImpl: (names, arg, path2) => { + throw new validatorPrimitives.ValidationError(`${path2}: channels are not expected in launchServer`); + }, + binary: "buffer", + isUnderTest: import_debug.isUnderTest + }; + let launchOptions = { + ...options, + ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : void 0, + ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs), + env: options.env ? envObjectToArray(options.env) : void 0, + timeout: options.timeout ?? import_time.DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT + }; + let browser; + try { + const controller = new import_progress.ProgressController(metadata); + browser = await controller.run(async (progress) => { + if (options._userDataDir !== void 0) { + const validator = validatorPrimitives.scheme["BrowserTypeLaunchPersistentContextParams"]; + launchOptions = validator({ ...launchOptions, userDataDir: options._userDataDir }, "", validatorContext); + const context = await playwright[this._browserName].launchPersistentContext(progress, options._userDataDir, launchOptions); + return context._browser; + } else { + const validator = validatorPrimitives.scheme["BrowserTypeLaunchParams"]; + launchOptions = validator(launchOptions, "", validatorContext); + return await playwright[this._browserName].launch(progress, launchOptions, toProtocolLogger(options.logger)); + } + }); + } catch (e) { + const log = import_helper.helper.formatBrowserLogs(metadata.log); + (0, import_stackTrace.rewriteErrorMessage)(e, `${e.message} Failed to launch browser.${log}`); + throw e; + } + const path = options.wsPath ? options.wsPath.startsWith("/") ? options.wsPath : `/${options.wsPath}` : `/${(0, import_crypto.createGuid)()}`; + const server = new import_playwrightServer.PlaywrightServer({ mode: options._sharedBrowser ? "launchServerShared" : "launchServer", path, maxConnections: Infinity, preLaunchedBrowser: browser }); + const wsEndpoint = await server.listen(options.port, options.host); + const browserServer = new import_utilsBundle.ws.EventEmitter(); + browserServer.process = () => browser.options.browserProcess.process; + browserServer.wsEndpoint = () => wsEndpoint; + browserServer.close = () => browser.options.browserProcess.close(); + browserServer[Symbol.asyncDispose] = browserServer.close; + browserServer.kill = () => browser.options.browserProcess.kill(); + browserServer._disconnectForTest = () => server.close(); + browserServer._userDataDirForTest = browser._userDataDirForTest; + browser.options.browserProcess.onclose = (exitCode, signal) => { + server.close(); + browserServer.emit("close", exitCode, signal); + }; + return browserServer; + } +} +function toProtocolLogger(logger) { + return logger ? (direction, message) => { + if (logger.isEnabled("protocol", "verbose")) + logger.log("protocol", "verbose", (direction === "send" ? "SEND \u25BA " : "\u25C0 RECV ") + JSON.stringify(message), [], {}); + } : void 0; +} +function envObjectToArray(env) { + const result = []; + for (const name in env) { + if (!Object.is(env[name], void 0)) + result.push({ name, value: String(env[name]) }); + } + return result; +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + BrowserServerLauncherImpl +}); diff --git a/scripts/node_modules/playwright-core/lib/cli/browserActions.js b/scripts/node_modules/playwright-core/lib/cli/browserActions.js new file mode 100644 index 0000000..2a00914 --- /dev/null +++ b/scripts/node_modules/playwright-core/lib/cli/browserActions.js @@ -0,0 +1,308 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var browserActions_exports = {}; +__export(browserActions_exports, { + codegen: () => codegen, + open: () => open, + pdf: () => pdf, + screenshot: () => screenshot +}); +module.exports = __toCommonJS(browserActions_exports); +var import_fs = __toESM(require("fs")); +var import_os = __toESM(require("os")); +var import_path = __toESM(require("path")); +var playwright = __toESM(require("../..")); +var import_utils = require("../utils"); +var import_utilsBundle = require("../utilsBundle"); +async function launchContext(options, extraOptions) { + validateOptions(options); + const browserType = lookupBrowserType(options); + const launchOptions = extraOptions; + if (options.channel) + launchOptions.channel = options.channel; + launchOptions.handleSIGINT = false; + const contextOptions = ( + // Copy the device descriptor since we have to compare and modify the options. + options.device ? { ...playwright.devices[options.device] } : {} + ); + if (!extraOptions.headless) + contextOptions.deviceScaleFactor = import_os.default.platform() === "darwin" ? 2 : 1; + if (browserType.name() === "webkit" && process.platform === "linux") { + delete contextOptions.hasTouch; + delete contextOptions.isMobile; + } + if (contextOptions.isMobile && browserType.name() === "firefox") + contextOptions.isMobile = void 0; + if (options.blockServiceWorkers) + contextOptions.serviceWorkers = "block"; + if (options.proxyServer) { + launchOptions.proxy = { + server: options.proxyServer + }; + if (options.proxyBypass) + launchOptions.proxy.bypass = options.proxyBypass; + } + if (options.viewportSize) { + try { + const [width, height] = options.viewportSize.split(",").map((n) => +n); + if (isNaN(width) || isNaN(height)) + throw new Error("bad values"); + contextOptions.viewport = { width, height }; + } catch (e) { + throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"'); + } + } + if (options.geolocation) { + try { + const [latitude, longitude] = options.geolocation.split(",").map((n) => parseFloat(n.trim())); + contextOptions.geolocation = { + latitude, + longitude + }; + } catch (e) { + throw new Error('Invalid geolocation format, should be "lat,long". For example --geolocation="37.819722,-122.478611"'); + } + contextOptions.permissions = ["geolocation"]; + } + if (options.userAgent) + contextOptions.userAgent = options.userAgent; + if (options.lang) + contextOptions.locale = options.lang; + if (options.colorScheme) + contextOptions.colorScheme = options.colorScheme; + if (options.timezone) + contextOptions.timezoneId = options.timezone; + if (options.loadStorage) + contextOptions.storageState = options.loadStorage; + if (options.ignoreHttpsErrors) + contextOptions.ignoreHTTPSErrors = true; + if (options.saveHar) { + contextOptions.recordHar = { path: import_path.default.resolve(process.cwd(), options.saveHar), mode: "minimal" }; + if (options.saveHarGlob) + contextOptions.recordHar.urlFilter = options.saveHarGlob; + contextOptions.serviceWorkers = "block"; + } + let browser; + let context; + if (options.userDataDir) { + context = await browserType.launchPersistentContext(options.userDataDir, { ...launchOptions, ...contextOptions }); + browser = context.browser(); + } else { + browser = await browserType.launch(launchOptions); + context = await browser.newContext(contextOptions); + } + let closingBrowser = false; + async function closeBrowser() { + if (closingBrowser) + return; + closingBrowser = true; + if (options.saveStorage) + await context.storageState({ path: options.saveStorage }).catch((e) => null); + if (options.saveHar) + await context.close(); + await browser.close(); + } + context.on("page", (page) => { + page.on("dialog", () => { + }); + page.on("close", () => { + const hasPage = browser.contexts().some((context2) => context2.pages().length > 0); + if (hasPage) + return; + closeBrowser().catch(() => { + }); + }); + }); + process.on("SIGINT", async () => { + await closeBrowser(); + (0, import_utils.gracefullyProcessExitDoNotHang)(130); + }); + const timeout = options.timeout ? parseInt(options.timeout, 10) : 0; + context.setDefaultTimeout(timeout); + context.setDefaultNavigationTimeout(timeout); + delete launchOptions.headless; + delete launchOptions.executablePath; + delete launchOptions.handleSIGINT; + delete contextOptions.deviceScaleFactor; + return { browser, browserName: browserType.name(), context, contextOptions, launchOptions, closeBrowser }; +} +async function openPage(context, url) { + let page = context.pages()[0]; + if (!page) + page = await context.newPage(); + if (url) { + if (import_fs.default.existsSync(url)) + url = "file://" + import_path.default.resolve(url); + else if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:") && !url.startsWith("data:")) + url = "http://" + url; + await page.goto(url); + } + return page; +} +async function open(options, url) { + const { context } = await launchContext(options, { headless: !!process.env.PWTEST_CLI_HEADLESS, executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH }); + await context._exposeConsoleApi(); + await openPage(context, url); +} +async function codegen(options, url) { + const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options; + const tracesDir = import_path.default.join(import_os.default.tmpdir(), `playwright-recorder-trace-${Date.now()}`); + const { context, browser, launchOptions, contextOptions, closeBrowser } = await launchContext(options, { + headless: !!process.env.PWTEST_CLI_HEADLESS, + executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH, + tracesDir + }); + const donePromise = new import_utils.ManualPromise(); + maybeSetupTestHooks(browser, closeBrowser, donePromise); + import_utilsBundle.dotenv.config({ path: "playwright.env" }); + await context._enableRecorder({ + language, + launchOptions, + contextOptions, + device: options.device, + saveStorage: options.saveStorage, + mode: "recording", + testIdAttributeName, + outputFile: outputFile ? import_path.default.resolve(outputFile) : void 0, + handleSIGINT: false + }); + await openPage(context, url); + donePromise.resolve(); +} +async function maybeSetupTestHooks(browser, closeBrowser, donePromise) { + if (!process.env.PWTEST_CLI_IS_UNDER_TEST) + return; + const logs = []; + require("playwright-core/lib/utilsBundle").debug.log = (...args) => { + const line = require("util").format(...args) + "\n"; + logs.push(line); + process.stderr.write(line); + }; + browser.on("disconnected", () => { + const hasCrashLine = logs.some((line) => line.includes("process did exit:") && !line.includes("process did exit: exitCode=0, signal=null")); + if (hasCrashLine) { + process.stderr.write("Detected browser crash.\n"); + (0, import_utils.gracefullyProcessExitDoNotHang)(1); + } + }); + const close = async () => { + await donePromise; + await closeBrowser(); + }; + if (process.env.PWTEST_CLI_EXIT_AFTER_TIMEOUT) { + setTimeout(close, +process.env.PWTEST_CLI_EXIT_AFTER_TIMEOUT); + return; + } + let stdin = ""; + process.stdin.on("data", (data) => { + stdin += data.toString(); + if (stdin.startsWith("exit")) { + process.stdin.destroy(); + close(); + } + }); +} +async function waitForPage(page, captureOptions) { + if (captureOptions.waitForSelector) { + console.log(`Waiting for selector ${captureOptions.waitForSelector}...`); + await page.waitForSelector(captureOptions.waitForSelector); + } + if (captureOptions.waitForTimeout) { + console.log(`Waiting for timeout ${captureOptions.waitForTimeout}...`); + await page.waitForTimeout(parseInt(captureOptions.waitForTimeout, 10)); + } +} +async function screenshot(options, captureOptions, url, path2) { + const { context } = await launchContext(options, { headless: true }); + console.log("Navigating to " + url); + const page = await openPage(context, url); + await waitForPage(page, captureOptions); + console.log("Capturing screenshot into " + path2); + await page.screenshot({ path: path2, fullPage: !!captureOptions.fullPage }); + await page.close(); +} +async function pdf(options, captureOptions, url, path2) { + if (options.browser !== "chromium") + throw new Error("PDF creation is only working with Chromium"); + const { context } = await launchContext({ ...options, browser: "chromium" }, { headless: true }); + console.log("Navigating to " + url); + const page = await openPage(context, url); + await waitForPage(page, captureOptions); + console.log("Saving as pdf into " + path2); + await page.pdf({ path: path2, format: captureOptions.paperFormat }); + await page.close(); +} +function lookupBrowserType(options) { + let name = options.browser; + if (options.device) { + const device = playwright.devices[options.device]; + name = device.defaultBrowserType; + } + let browserType; + switch (name) { + case "chromium": + browserType = playwright.chromium; + break; + case "webkit": + browserType = playwright.webkit; + break; + case "firefox": + browserType = playwright.firefox; + break; + case "cr": + browserType = playwright.chromium; + break; + case "wk": + browserType = playwright.webkit; + break; + case "ff": + browserType = playwright.firefox; + break; + } + if (browserType) + return browserType; + import_utilsBundle.program.help(); +} +function validateOptions(options) { + if (options.device && !(options.device in playwright.devices)) { + const lines = [`Device descriptor not found: '${options.device}', available devices are:`]; + for (const name in playwright.devices) + lines.push(` "${name}"`); + throw new Error(lines.join("\n")); + } + if (options.colorScheme && !["light", "dark"].includes(options.colorScheme)) + throw new Error('Invalid color scheme, should be one of "light", "dark"'); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + codegen, + open, + pdf, + screenshot +}); diff --git a/scripts/node_modules/playwright-core/lib/cli/driver.js b/scripts/node_modules/playwright-core/lib/cli/driver.js new file mode 100644 index 0000000..c96c3f6 --- /dev/null +++ b/scripts/node_modules/playwright-core/lib/cli/driver.js @@ -0,0 +1,98 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var driver_exports = {}; +__export(driver_exports, { + launchBrowserServer: () => launchBrowserServer, + printApiJson: () => printApiJson, + runDriver: () => runDriver, + runServer: () => runServer +}); +module.exports = __toCommonJS(driver_exports); +var import_fs = __toESM(require("fs")); +var playwright = __toESM(require("../..")); +var import_pipeTransport = require("../server/utils/pipeTransport"); +var import_playwrightServer = require("../remote/playwrightServer"); +var import_server = require("../server"); +var import_processLauncher = require("../server/utils/processLauncher"); +function printApiJson() { + console.log(JSON.stringify(require("../../api.json"))); +} +function runDriver() { + const dispatcherConnection = new import_server.DispatcherConnection(); + new import_server.RootDispatcher(dispatcherConnection, async (rootScope, { sdkLanguage }) => { + const playwright2 = (0, import_server.createPlaywright)({ sdkLanguage }); + return new import_server.PlaywrightDispatcher(rootScope, playwright2); + }); + const transport = new import_pipeTransport.PipeTransport(process.stdout, process.stdin); + transport.onmessage = (message) => dispatcherConnection.dispatch(JSON.parse(message)); + const isJavaScriptLanguageBinding = !process.env.PW_LANG_NAME || process.env.PW_LANG_NAME === "javascript"; + const replacer = !isJavaScriptLanguageBinding && String.prototype.toWellFormed ? (key, value) => { + if (typeof value === "string") + return value.toWellFormed(); + return value; + } : void 0; + dispatcherConnection.onmessage = (message) => transport.send(JSON.stringify(message, replacer)); + transport.onclose = () => { + dispatcherConnection.onmessage = () => { + }; + (0, import_processLauncher.gracefullyProcessExitDoNotHang)(0); + }; + process.on("SIGINT", () => { + }); +} +async function runServer(options) { + const { + port, + host, + path = "/", + maxConnections = Infinity, + extension, + artifactsDir + } = options; + const server = new import_playwrightServer.PlaywrightServer({ mode: extension ? "extension" : "default", path, maxConnections, artifactsDir }); + const wsEndpoint = await server.listen(port, host); + process.on("exit", () => server.close().catch(console.error)); + console.log("Listening on " + wsEndpoint); + process.stdin.on("close", () => (0, import_processLauncher.gracefullyProcessExitDoNotHang)(0)); +} +async function launchBrowserServer(browserName, configFile) { + let options = {}; + if (configFile) + options = JSON.parse(import_fs.default.readFileSync(configFile).toString()); + const browserType = playwright[browserName]; + const server = await browserType.launchServer(options); + console.log(server.wsEndpoint()); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + launchBrowserServer, + printApiJson, + runDriver, + runServer +}); diff --git a/scripts/node_modules/playwright-core/lib/cli/installActions.js b/scripts/node_modules/playwright-core/lib/cli/installActions.js new file mode 100644 index 0000000..eddcf4a --- /dev/null +++ b/scripts/node_modules/playwright-core/lib/cli/installActions.js @@ -0,0 +1,171 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var installActions_exports = {}; +__export(installActions_exports, { + installBrowsers: () => installBrowsers, + installDeps: () => installDeps, + markDockerImage: () => markDockerImage, + registry: () => import_server.registry, + uninstallBrowsers: () => uninstallBrowsers +}); +module.exports = __toCommonJS(installActions_exports); +var import_path = __toESM(require("path")); +var import_server = require("../server"); +var import_utils = require("../utils"); +var import_utils2 = require("../utils"); +var import_ascii = require("../server/utils/ascii"); +function printInstalledBrowsers(browsers) { + const browserPaths = /* @__PURE__ */ new Set(); + for (const browser of browsers) + browserPaths.add(browser.browserPath); + console.log(` Browsers:`); + for (const browserPath of [...browserPaths].sort()) + console.log(` ${browserPath}`); + console.log(` References:`); + const references = /* @__PURE__ */ new Set(); + for (const browser of browsers) + references.add(browser.referenceDir); + for (const reference of [...references].sort()) + console.log(` ${reference}`); +} +function printGroupedByPlaywrightVersion(browsers) { + const dirToVersion = /* @__PURE__ */ new Map(); + for (const browser of browsers) { + if (dirToVersion.has(browser.referenceDir)) + continue; + const packageJSON = require(import_path.default.join(browser.referenceDir, "package.json")); + const version = packageJSON.version; + dirToVersion.set(browser.referenceDir, version); + } + const groupedByPlaywrightMinorVersion = /* @__PURE__ */ new Map(); + for (const browser of browsers) { + const version = dirToVersion.get(browser.referenceDir); + let entries = groupedByPlaywrightMinorVersion.get(version); + if (!entries) { + entries = []; + groupedByPlaywrightMinorVersion.set(version, entries); + } + entries.push(browser); + } + const sortedVersions = [...groupedByPlaywrightMinorVersion.keys()].sort((a, b) => { + const aComponents = a.split("."); + const bComponents = b.split("."); + const aMajor = parseInt(aComponents[0], 10); + const bMajor = parseInt(bComponents[0], 10); + if (aMajor !== bMajor) + return aMajor - bMajor; + const aMinor = parseInt(aComponents[1], 10); + const bMinor = parseInt(bComponents[1], 10); + if (aMinor !== bMinor) + return aMinor - bMinor; + return aComponents.slice(2).join(".").localeCompare(bComponents.slice(2).join(".")); + }); + for (const version of sortedVersions) { + console.log(` +Playwright version: ${version}`); + printInstalledBrowsers(groupedByPlaywrightMinorVersion.get(version)); + } +} +async function markDockerImage(dockerImageNameTemplate) { + (0, import_utils2.assert)(dockerImageNameTemplate, "dockerImageNameTemplate is required"); + await (0, import_server.writeDockerVersion)(dockerImageNameTemplate); +} +async function installBrowsers(args, options) { + if ((0, import_utils.isLikelyNpxGlobal)()) { + console.error((0, import_ascii.wrapInASCIIBox)([ + `WARNING: It looks like you are running 'npx playwright install' without first`, + `installing your project's dependencies.`, + ``, + `To avoid unexpected behavior, please install your dependencies first, and`, + `then run Playwright's install command:`, + ``, + ` npm install`, + ` npx playwright install`, + ``, + `If your project does not yet depend on Playwright, first install the`, + `applicable npm package (most commonly @playwright/test), and`, + `then run Playwright's install command to download the browsers:`, + ``, + ` npm install @playwright/test`, + ` npx playwright install`, + `` + ].join("\n"), 1)); + } + if (options.shell === false && options.onlyShell) + throw new Error(`Only one of --no-shell and --only-shell can be specified`); + const shell = options.shell === false ? "no" : options.onlyShell ? "only" : void 0; + const executables = import_server.registry.resolveBrowsers(args, { shell }); + if (options.withDeps) + await import_server.registry.installDeps(executables, !!options.dryRun); + if (options.dryRun && options.list) + throw new Error(`Only one of --dry-run and --list can be specified`); + if (options.dryRun) { + for (const executable of executables) { + console.log(import_server.registry.calculateDownloadTitle(executable)); + console.log(` Install location: ${executable.directory ?? ""}`); + if (executable.downloadURLs?.length) { + const [url, ...fallbacks] = executable.downloadURLs; + console.log(` Download url: ${url}`); + for (let i = 0; i < fallbacks.length; ++i) + console.log(` Download fallback ${i + 1}: ${fallbacks[i]}`); + } + console.log(``); + } + } else if (options.list) { + const browsers = await import_server.registry.listInstalledBrowsers(); + printGroupedByPlaywrightVersion(browsers); + } else { + await import_server.registry.install(executables, { force: options.force }); + await import_server.registry.validateHostRequirementsForExecutablesIfNeeded(executables, process.env.PW_LANG_NAME || "javascript").catch((e) => { + e.name = "Playwright Host validation warning"; + console.error(e); + }); + } +} +async function uninstallBrowsers(options) { + delete process.env.PLAYWRIGHT_SKIP_BROWSER_GC; + await import_server.registry.uninstall(!!options.all).then(({ numberOfBrowsersLeft }) => { + if (!options.all && numberOfBrowsersLeft > 0) { + console.log("Successfully uninstalled Playwright browsers for the current Playwright installation."); + console.log(`There are still ${numberOfBrowsersLeft} browsers left, used by other Playwright installations. +To uninstall Playwright browsers for all installations, re-run with --all flag.`); + } + }); +} +async function installDeps(args, options) { + await import_server.registry.installDeps(import_server.registry.resolveBrowsers(args, {}), !!options.dryRun); +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + installBrowsers, + installDeps, + markDockerImage, + registry, + uninstallBrowsers +}); diff --git a/scripts/node_modules/playwright-core/lib/cli/program.js b/scripts/node_modules/playwright-core/lib/cli/program.js new file mode 100644 index 0000000..39b59dd --- /dev/null +++ b/scripts/node_modules/playwright-core/lib/cli/program.js @@ -0,0 +1,225 @@ +"use strict"; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var program_exports = {}; +__export(program_exports, { + program: () => import_utilsBundle2.program +}); +module.exports = __toCommonJS(program_exports); +var import_bootstrap = require("../bootstrap"); +var import_utils = require("../utils"); +var import_traceCli = require("../tools/trace/traceCli"); +var import_utilsBundle = require("../utilsBundle"); +var import_utilsBundle2 = require("../utilsBundle"); +const packageJSON = require("../../package.json"); +import_utilsBundle.program.version("Version " + (process.env.PW_CLI_DISPLAY_VERSION || packageJSON.version)).name(buildBasePlaywrightCLICommand(process.env.PW_LANG_NAME)); +import_utilsBundle.program.command("mark-docker-image [dockerImageNameTemplate]", { hidden: true }).description("mark docker image").allowUnknownOption(true).action(async function(dockerImageNameTemplate) { + const { markDockerImage } = require("./installActions"); + markDockerImage(dockerImageNameTemplate).catch(logErrorAndExit); +}); +commandWithOpenOptions("open [url]", "open page in browser specified via -b, --browser", []).action(async function(url, options) { + const { open } = require("./browserActions"); + open(options, url).catch(logErrorAndExit); +}).addHelpText("afterAll", ` +Examples: + + $ open + $ open -b webkit https://example.com`); +commandWithOpenOptions( + "codegen [url]", + "open page and generate code for user actions", + [ + ["-o, --output ", "saves the generated script to a file"], + ["--target ", `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()], + ["--test-id-attribute ", "use the specified attribute to generate data test ID selectors"] + ] +).action(async function(url, options) { + const { codegen } = require("./browserActions"); + await codegen(options, url); +}).addHelpText("afterAll", ` +Examples: + + $ codegen + $ codegen --target=python + $ codegen -b webkit https://example.com`); +import_utilsBundle.program.command("install [browser...]").description("ensure browsers necessary for this version of Playwright are installed").option("--with-deps", "install system dependencies for browsers").option("--dry-run", "do not execute installation, only print information").option("--list", "prints list of browsers from all playwright installations").option("--force", "force reinstall of already installed browsers").option("--only-shell", "only install headless shell when installing chromium").option("--no-shell", "do not install chromium headless shell").action(async function(args, options) { + try { + const { installBrowsers } = require("./installActions"); + await installBrowsers(args, options); + } catch (e) { + console.log(`Failed to install browsers +${e}`); + (0, import_utils.gracefullyProcessExitDoNotHang)(1); + } +}).addHelpText("afterAll", ` + +Examples: + - $ install + Install default browsers. + + - $ install chrome firefox + Install custom browsers, supports chromium, firefox, webkit, chromium-headless-shell.`); +import_utilsBundle.program.command("uninstall").description("Removes browsers used by this installation of Playwright from the system (chromium, firefox, webkit, ffmpeg). This does not include branded channels.").option("--all", "Removes all browsers used by any Playwright installation from the system.").action(async (options) => { + const { uninstallBrowsers } = require("./installActions"); + uninstallBrowsers(options).catch(logErrorAndExit); +}); +import_utilsBundle.program.command("install-deps [browser...]").description("install dependencies necessary to run browsers (will ask for sudo permissions)").option("--dry-run", "Do not execute installation commands, only print them").action(async function(args, options) { + try { + const { installDeps } = require("./installActions"); + await installDeps(args, options); + } catch (e) { + console.log(`Failed to install browser dependencies +${e}`); + (0, import_utils.gracefullyProcessExitDoNotHang)(1); + } +}).addHelpText("afterAll", ` +Examples: + - $ install-deps + Install dependencies for default browsers. + + - $ install-deps chrome firefox + Install dependencies for specific browsers, supports chromium, firefox, webkit, chromium-headless-shell.`); +const browsers = [ + { alias: "cr", name: "Chromium", type: "chromium" }, + { alias: "ff", name: "Firefox", type: "firefox" }, + { alias: "wk", name: "WebKit", type: "webkit" } +]; +for (const { alias, name, type } of browsers) { + commandWithOpenOptions(`${alias} [url]`, `open page in ${name}`, []).action(async function(url, options) { + const { open } = require("./browserActions"); + open({ ...options, browser: type }, url).catch(logErrorAndExit); + }).addHelpText("afterAll", ` +Examples: + + $ ${alias} https://example.com`); +} +commandWithOpenOptions( + "screenshot ", + "capture a page screenshot", + [ + ["--wait-for-selector ", "wait for selector before taking a screenshot"], + ["--wait-for-timeout ", "wait for timeout in milliseconds before taking a screenshot"], + ["--full-page", "whether to take a full page screenshot (entire scrollable area)"] + ] +).action(async function(url, filename, command) { + const { screenshot } = require("./browserActions"); + screenshot(command, command, url, filename).catch(logErrorAndExit); +}).addHelpText("afterAll", ` +Examples: + + $ screenshot -b webkit https://example.com example.png`); +commandWithOpenOptions( + "pdf ", + "save page as pdf", + [ + ["--paper-format ", "paper format: Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5, A6"], + ["--wait-for-selector ", "wait for given selector before saving as pdf"], + ["--wait-for-timeout ", "wait for given timeout in milliseconds before saving as pdf"] + ] +).action(async function(url, filename, options) { + const { pdf } = require("./browserActions"); + pdf(options, options, url, filename).catch(logErrorAndExit); +}).addHelpText("afterAll", ` +Examples: + + $ pdf https://example.com example.pdf`); +import_utilsBundle.program.command("run-driver", { hidden: true }).action(async function(options) { + const { runDriver } = require("./driver"); + runDriver(); +}); +import_utilsBundle.program.command("run-server", { hidden: true }).option("--port ", "Server port").option("--host ", "Server host").option("--path ", "Endpoint Path", "/").option("--max-clients ", "Maximum clients").option("--mode ", 'Server mode, either "default" or "extension"').option("--artifacts-dir ", "Artifacts directory").action(async function(options) { + const { runServer } = require("./driver"); + runServer({ + port: options.port ? +options.port : void 0, + host: options.host, + path: options.path, + maxConnections: options.maxClients ? +options.maxClients : Infinity, + extension: options.mode === "extension" || !!process.env.PW_EXTENSION_MODE, + artifactsDir: options.artifactsDir + }).catch(logErrorAndExit); +}); +import_utilsBundle.program.command("print-api-json", { hidden: true }).action(async function(options) { + const { printApiJson } = require("./driver"); + printApiJson(); +}); +import_utilsBundle.program.command("launch-server", { hidden: true }).requiredOption("--browser ", 'Browser name, one of "chromium", "firefox" or "webkit"').option("--config ", "JSON file with launchServer options").action(async function(options) { + const { launchBrowserServer } = require("./driver"); + launchBrowserServer(options.browser, options.config); +}); +import_utilsBundle.program.command("show-trace [trace]").option("-b, --browser ", "browser to use, one of cr, chromium, ff, firefox, wk, webkit", "chromium").option("-h, --host ", "Host to serve trace on; specifying this option opens trace in a browser tab").option("-p, --port ", "Port to serve trace on, 0 for any free port; specifying this option opens trace in a browser tab").option("--stdin", "Accept trace URLs over stdin to update the viewer").description("show trace viewer").action(async function(trace, options) { + if (options.browser === "cr") + options.browser = "chromium"; + if (options.browser === "ff") + options.browser = "firefox"; + if (options.browser === "wk") + options.browser = "webkit"; + const openOptions = { + host: options.host, + port: +options.port, + isServer: !!options.stdin + }; + const { runTraceInBrowser, runTraceViewerApp } = require("../server/trace/viewer/traceViewer"); + if (options.port !== void 0 || options.host !== void 0) + runTraceInBrowser(trace, openOptions).catch(logErrorAndExit); + else + runTraceViewerApp(trace, options.browser, openOptions).catch(logErrorAndExit); +}).addHelpText("afterAll", ` +Examples: + + $ show-trace + $ show-trace https://example.com/trace.zip`); +(0, import_traceCli.addTraceCommands)(import_utilsBundle.program, logErrorAndExit); +import_utilsBundle.program.command("cli", { hidden: true }).allowExcessArguments(true).allowUnknownOption(true).action(async (options) => { + const { program: cliProgram } = require("../tools/cli-client/program"); + process.argv.splice(process.argv.indexOf("cli"), 1); + cliProgram().catch(logErrorAndExit); +}); +function logErrorAndExit(e) { + if (process.env.PWDEBUGIMPL) + console.error(e); + else + console.error(e.name + ": " + e.message); + (0, import_utils.gracefullyProcessExitDoNotHang)(1); +} +function codegenId() { + return process.env.PW_LANG_NAME || "playwright-test"; +} +function commandWithOpenOptions(command, description, options) { + let result = import_utilsBundle.program.command(command).description(description); + for (const option of options) + result = result.option(option[0], ...option.slice(1)); + return result.option("-b, --browser ", "browser to use, one of cr, chromium, ff, firefox, wk, webkit", "chromium").option("--block-service-workers", "block service workers").option("--channel ", 'Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc').option("--color-scheme ", 'emulate preferred color scheme, "light" or "dark"').option("--device ", 'emulate device, for example "iPhone 11"').option("--geolocation ", 'specify geolocation coordinates, for example "37.819722,-122.478611"').option("--ignore-https-errors", "ignore https errors").option("--load-storage ", "load context storage state from the file, previously saved with --save-storage").option("--lang ", 'specify language / locale, for example "en-GB"').option("--proxy-server ", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--proxy-bypass ", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--save-har ", "save HAR file with all network activity at the end").option("--save-har-glob ", "filter entries in the HAR by matching url against this glob pattern").option("--save-storage ", "save context storage state at the end, for later use with --load-storage").option("--timezone