Integration with Raycast
This commit is contained in:
+146
-101
@@ -5,15 +5,19 @@ local json = require("hs.json")
|
|||||||
local styledtext = require("hs.styledtext")
|
local styledtext = require("hs.styledtext")
|
||||||
|
|
||||||
-- ==========================================
|
-- ==========================================
|
||||||
-- CONFIGURATION & CENTRAL CONFIG LOADING
|
-- CONFIGURATION & CENTRAL JSON CONFIG LOADING
|
||||||
-- ==========================================
|
-- ==========================================
|
||||||
selector.storageDir = os.getenv("HOME") .. "/.hammerspoon/layouts/"
|
local homeDir = os.getenv("HOME") or "/Users/francop"
|
||||||
local configFile = os.getenv("HOME") .. "/.hammerspoon/config.json"
|
selector.storageDir = homeDir .. "/.hammerspoon/layouts/"
|
||||||
|
local configFile = homeDir .. "/.hammerspoon/config.json"
|
||||||
selector.layoutHotkeys = {}
|
selector.layoutHotkeys = {}
|
||||||
selector.staticHotkeys = {}
|
selector.staticHotkeys = {}
|
||||||
|
selector.instanceChooser = nil
|
||||||
|
selector.actionChooser = nil -- Secondary chooser cache for profile actions
|
||||||
|
|
||||||
if not hs.fs.attributes(selector.storageDir) then hs.fs.mkdir(selector.storageDir) end
|
if not hs.fs.attributes(selector.storageDir) then hs.fs.mkdir(selector.storageDir) end
|
||||||
|
|
||||||
|
-- Baseline fallbacks
|
||||||
local stubbornAppsList = {
|
local stubbornAppsList = {
|
||||||
["Gemini"] = true, ["AFFiNE"] = true, ["Terminal"] = true,
|
["Gemini"] = true, ["AFFiNE"] = true, ["Terminal"] = true,
|
||||||
["System Settings"] = true, ["Hammerspoon"] = true
|
["System Settings"] = true, ["Hammerspoon"] = true
|
||||||
@@ -26,11 +30,14 @@ local ignoreListItems = {
|
|||||||
["com.typewhisper.mac"] = true
|
["com.typewhisper.mac"] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Load Central Config if available
|
-- Load Central Config JSON if available
|
||||||
local cfgData = hs.json.read(configFile)
|
local cfgData = hs.json.read(configFile)
|
||||||
if cfgData then
|
if cfgData then
|
||||||
if cfgData.stubbornAppsList then stubbornAppsList = cfgData.stubbornAppsList end
|
if cfgData.stubbornAppsList then stubbornAppsList = cfgData.stubbornAppsList end
|
||||||
if cfgData.ignoreListItems then ignoreListItems = cfgData.ignoreListItems end
|
if cfgData.ignoreListItems then ignoreListItems = cfgData.ignoreListItems end
|
||||||
|
print("LayoutSelector: Successfully loaded rules from central config.json")
|
||||||
|
else
|
||||||
|
print("LayoutSelector: Central config.json not found, utilizing default fallbacks")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ==========================================
|
-- ==========================================
|
||||||
@@ -77,10 +84,11 @@ local function captureCurrentLayout()
|
|||||||
if app and win:isVisible() and win:frame().w > 0 then
|
if app and win:isVisible() and win:frame().w > 0 then
|
||||||
local appName = app:name() or ""
|
local appName = app:name() or ""
|
||||||
local bundleID = app:bundleID() or ""
|
local bundleID = app:bundleID() or ""
|
||||||
|
local winTitle = win:title() or "Untitled Window"
|
||||||
-- Hybrid check for Capture
|
-- Hybrid check for Capture
|
||||||
if not ignoreListItems[appName] and not ignoreListItems[bundleID] then
|
if not ignoreListItems[appName] and not ignoreListItems[bundleID] then
|
||||||
table.insert(layout.windows, {
|
table.insert(layout.windows, {
|
||||||
appName = appName, bundleID = bundleID, winTitle = win:title(),
|
appName = appName, bundleID = bundleID, winTitle = winTitle,
|
||||||
x = math.floor(win:frame().x), y = math.floor(win:frame().y),
|
x = math.floor(win:frame().x), y = math.floor(win:frame().y),
|
||||||
w = math.floor(win:frame().w), h = math.floor(win:frame().h)
|
w = math.floor(win:frame().w), h = math.floor(win:frame().h)
|
||||||
})
|
})
|
||||||
@@ -106,7 +114,6 @@ local function executeRestore(filePath, layoutName)
|
|||||||
local function moveWindows()
|
local function moveWindows()
|
||||||
local savedByApp = {}
|
local savedByApp = {}
|
||||||
for _, winData in ipairs(data.windows) do
|
for _, winData in ipairs(data.windows) do
|
||||||
-- Hybrid check for Restore processing
|
|
||||||
if not ignoreListItems[winData.appName] and not ignoreListItems[winData.bundleID] then
|
if not ignoreListItems[winData.appName] and not ignoreListItems[winData.bundleID] then
|
||||||
savedByApp[winData.appName] = savedByApp[winData.appName] or {}
|
savedByApp[winData.appName] = savedByApp[winData.appName] or {}
|
||||||
table.insert(savedByApp[winData.appName], winData)
|
table.insert(savedByApp[winData.appName], winData)
|
||||||
@@ -178,11 +185,138 @@ local function executeRestore(filePath, layoutName)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- ==========================================
|
-- ==========================================
|
||||||
-- HOTKEY & MENU MANAGEMENT
|
-- RAYCAST EXTERNAL IPC HOOKS & MANAGEMENT
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
function selector.save(name)
|
||||||
|
if not name or name == "" then
|
||||||
|
hs.alert.show("Error: Invalid Layout Name", 2)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local path = selector.storageDir .. name .. ".json"
|
||||||
|
local layoutData = captureCurrentLayout()
|
||||||
|
hs.json.write(layoutData, path, true, true)
|
||||||
|
selector.rebindLayoutKeys()
|
||||||
|
hs.alert.show("Saved Layout: " .. name, 2)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function selector.load(name)
|
||||||
|
if not name or name == "" then
|
||||||
|
hs.alert.show("Error: Specify Layout Name", 2)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local path = selector.storageDir .. name .. ".json"
|
||||||
|
if hs.fs.attributes(path) then
|
||||||
|
executeRestore(path, name)
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
hs.alert.show("Layout Not Found: " .. name, 2)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Secondary Selector Options Window (Handles single profile rules)
|
||||||
|
function selector.showLayoutActions(layoutName)
|
||||||
|
local path = selector.storageDir .. layoutName .. ".json"
|
||||||
|
|
||||||
|
local actionChoices = {
|
||||||
|
{
|
||||||
|
text = "🚀 Restore Layout Profile",
|
||||||
|
subText = "Snap open apps back into position for: " .. layoutName,
|
||||||
|
action = "restore"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "🔄 Update / Overwrite Profile",
|
||||||
|
subText = "Replace layout coordinates with your current window setup",
|
||||||
|
action = "update"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "🗑️ Delete Layout Profile",
|
||||||
|
subText = "Permanently remove the profile config file from disk",
|
||||||
|
action = "delete"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if selector.actionChooser then selector.actionChooser:hide() end
|
||||||
|
|
||||||
|
selector.actionChooser = hs.chooser.new(function(choice)
|
||||||
|
if choice then
|
||||||
|
if choice.action == "restore" then
|
||||||
|
selector.load(layoutName)
|
||||||
|
elseif choice.action == "update" then
|
||||||
|
hs.json.write(captureCurrentLayout(), path, true, true)
|
||||||
|
hs.alert.show("Updated: " .. layoutName, 2)
|
||||||
|
elseif choice.action == "delete" then
|
||||||
|
os.remove(path)
|
||||||
|
selector.rebindLayoutKeys()
|
||||||
|
hs.alert.show("Deleted Profile: " .. layoutName, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
selector.actionChooser:placeholderText("Manage Layout Context [" .. layoutName .. "]...")
|
||||||
|
selector.actionChooser:choices(actionChoices)
|
||||||
|
selector.actionChooser:show()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Primary Selection Interface Window
|
||||||
|
function selector.showMenu()
|
||||||
|
local choices = {
|
||||||
|
{
|
||||||
|
text = "📸 Create / Save New Layout...",
|
||||||
|
subText = "Take a snapshot snapshot of your active desktop workspace configuration",
|
||||||
|
action = "create"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
local files = getLayoutFiles()
|
||||||
|
for _, file in ipairs(files) do
|
||||||
|
local label = file:gsub("%.json$", "")
|
||||||
|
local path = selector.storageDir .. file
|
||||||
|
local data = hs.json.read(path)
|
||||||
|
|
||||||
|
local subTextStr = "Layout Profile"
|
||||||
|
if data then
|
||||||
|
local modeStr = (data.mode == "Docked") and "🖥️ Docked" or "💻 Laptop"
|
||||||
|
subTextStr = string.format("%s Mode | Screens: %d | Saved: %s", modeStr, data.screenCount or 1, data.saveTime or "Unknown")
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(choices, {
|
||||||
|
text = "📁 " .. label,
|
||||||
|
subText = subTextStr,
|
||||||
|
action = "manage",
|
||||||
|
layoutName = label
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if selector.instanceChooser then selector.instanceChooser:hide() end
|
||||||
|
|
||||||
|
selector.instanceChooser = hs.chooser.new(function(choice)
|
||||||
|
if choice then
|
||||||
|
if choice.action == "create" then
|
||||||
|
-- Fallback input trigger to type profile key
|
||||||
|
local _, name = hs.dialog.textPrompt("Create Window Layout", "Enter a unique profile label name:", "", "Save", "Cancel")
|
||||||
|
if name and name ~= "" then
|
||||||
|
selector.save(name)
|
||||||
|
end
|
||||||
|
elseif choice.action == "manage" then
|
||||||
|
-- Hand context frame mapping loop to the action menu selector block
|
||||||
|
selector.showLayoutActions(choice.layoutName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
selector.instanceChooser:placeholderText("Select a layout workspace profile or register a new one...")
|
||||||
|
selector.instanceChooser:choices(choices)
|
||||||
|
selector.instanceChooser:show()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- HOTKEY MANAGEMENT
|
||||||
-- ==========================================
|
-- ==========================================
|
||||||
|
|
||||||
function selector.rebindLayoutKeys()
|
function selector.rebindLayoutKeys()
|
||||||
-- Only rebind when called explicitly (on file changes)
|
|
||||||
for _, hk in pairs(selector.layoutHotkeys) do hk:delete() end
|
for _, hk in pairs(selector.layoutHotkeys) do hk:delete() end
|
||||||
selector.layoutHotkeys = {}
|
selector.layoutHotkeys = {}
|
||||||
|
|
||||||
@@ -196,108 +330,19 @@ function selector.rebindLayoutKeys()
|
|||||||
print("LayoutSelector: Dynamic Hotkeys Rebuilt")
|
print("LayoutSelector: Dynamic Hotkeys Rebuilt")
|
||||||
end
|
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
|
-- INITIALIZATION & GLOBAL EXPORT
|
||||||
-- ==========================================
|
-- ==========================================
|
||||||
|
|
||||||
-- Static Save Hotkey (Defined once)
|
|
||||||
selector.staticHotkeys["save"] = hs.hotkey.bind({"shift", "ctrl", "alt", "cmd"}, "S", function()
|
selector.staticHotkeys["save"] = hs.hotkey.bind({"shift", "ctrl", "alt", "cmd"}, "S", function()
|
||||||
local _, name = hs.dialog.textPrompt("New Layout", "Enter name:", "", "Save", "Cancel")
|
local _, name = hs.dialog.textPrompt("New Layout", "Enter name:", "", "Save", "Cancel")
|
||||||
if name and name ~= "" then
|
if name and name ~= "" then
|
||||||
hs.json.write(captureCurrentLayout(), selector.storageDir .. name .. ".json", true, true)
|
selector.save(name)
|
||||||
selector.rebindLayoutKeys() -- Rebuild keys only when file added
|
|
||||||
selector.refreshMenu()
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- Screen watcher only refreshes the VISUAL menu, not the keys
|
selector.rebindLayoutKeys()
|
||||||
selector.screenWatcher = hs.screen.watcher.new(function()
|
|
||||||
hs.timer.doAfter(2, selector.refreshMenu)
|
|
||||||
end):start()
|
|
||||||
|
|
||||||
selector.rebindLayoutKeys() -- Run once at load
|
LayoutSelector = selector
|
||||||
selector.refreshMenu() -- Run once at load
|
|
||||||
|
|
||||||
return selector
|
return selector
|
||||||
+46
-2
@@ -209,7 +209,6 @@ function obj.rescueWindowsToLaptop()
|
|||||||
if f.w > maxFrame.w then f.w = maxFrame.w - 100 end
|
if f.w > maxFrame.w then f.w = maxFrame.w - 100 end
|
||||||
if f.h > maxFrame.h then f.h = maxFrame.h - 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.x = maxFrame.x + 50 + staggerOffset
|
||||||
f.y = maxFrame.y + 50 + staggerOffset
|
f.y = maxFrame.y + 50 + staggerOffset
|
||||||
|
|
||||||
@@ -302,7 +301,6 @@ obj.screenWatcher = hs.screen.watcher.new(function()
|
|||||||
|
|
||||||
local currentScreens = #hs.screen.allScreens()
|
local currentScreens = #hs.screen.allScreens()
|
||||||
|
|
||||||
-- NEW GUARD: If count hasn't changed, ignore ghost/handshake events
|
|
||||||
if currentScreens == obj.lastScreenCount then
|
if currentScreens == obj.lastScreenCount then
|
||||||
log("DOCK EVENT: Ignored (Screen count unchanged).")
|
log("DOCK EVENT: Ignored (Screen count unchanged).")
|
||||||
return
|
return
|
||||||
@@ -338,4 +336,50 @@ obj.clockTimer = hs.timer.doEvery(1, function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
updateMenu()
|
updateMenu()
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- RAYCAST INTERACTIVE CHOOSER MENU
|
||||||
|
-- ==========================================
|
||||||
|
function obj.showMenu()
|
||||||
|
local choices = {
|
||||||
|
{
|
||||||
|
text = "📸 Save State Layout",
|
||||||
|
subText = "Snapshot active positions to saved_layout.json (Resets auto-timer)",
|
||||||
|
action = "save"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "🔄 Restore State Layout",
|
||||||
|
subText = "Force apps and window sizes back to your saved profile state",
|
||||||
|
action = "restore"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text = "🚀 Rescue Windows",
|
||||||
|
subText = "Cascade active window threads onto primary laptop screen space",
|
||||||
|
action = "rescue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.instanceChooser then obj.instanceChooser:hide() end
|
||||||
|
|
||||||
|
obj.instanceChooser = hs.chooser.new(function(choice)
|
||||||
|
if choice then
|
||||||
|
if choice.action == "save" then
|
||||||
|
obj.saveLayout(false)
|
||||||
|
saveCountdown = obj.saveInterval
|
||||||
|
elseif choice.action == "restore" then
|
||||||
|
obj.restoreLayout()
|
||||||
|
elseif choice.action == "rescue" then
|
||||||
|
obj.rescueWindowsToLaptop()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
obj.instanceChooser:placeholderText("Workspace Manager Actions...")
|
||||||
|
obj.instanceChooser:choices(choices)
|
||||||
|
obj.instanceChooser:show()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Export module instance globally for direct IPC command routing
|
||||||
|
WindowManager = obj
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
@@ -6,14 +6,6 @@ hs.dockIcon(false)
|
|||||||
hs.ipc.cliInstall("/opt/homebrew")
|
hs.ipc.cliInstall("/opt/homebrew")
|
||||||
hs.alert.show("Hammerspoon Headless Daemon Active", 2)
|
hs.alert.show("Hammerspoon Headless Daemon Active", 2)
|
||||||
|
|
||||||
-- ========================================================================
|
|
||||||
-- HEADLESS WORKSPACE BACKGROUND ENGINE (FORCED AT BOOT)
|
|
||||||
-- ========================================================================
|
|
||||||
hs.menuIcon(false)
|
|
||||||
hs.dockIcon(false)
|
|
||||||
hs.ipc.cliInstall("/opt/homebrew")
|
|
||||||
hs.alert.show("Hammerspoon Headless Daemon Active", 2)
|
|
||||||
|
|
||||||
-- ~/.hammerspoon/init.lua
|
-- ~/.hammerspoon/init.lua
|
||||||
-- Load Config First
|
-- Load Config First
|
||||||
-- config = require("Config")
|
-- config = require("Config")
|
||||||
@@ -24,7 +16,10 @@ require("HyperKey")
|
|||||||
require('SearchWindows')
|
require('SearchWindows')
|
||||||
require('Caffeine')
|
require('Caffeine')
|
||||||
require('AppBorders')
|
require('AppBorders')
|
||||||
require('LayoutSelector')
|
|
||||||
|
-- CRITICAL FOR RAYCAST INTERACTION: Bind the return value to a global variable
|
||||||
|
LayoutSelector = require('LayoutSelector')
|
||||||
|
|
||||||
require('System_Tweaks') -- Used for Time Machine Throttle Disable
|
require('System_Tweaks') -- Used for Time Machine Throttle Disable
|
||||||
-- require("Focus") -- Does not work with layout saver - Not needed if using Monocle
|
-- require("Focus") -- Does not work with layout saver - Not needed if using Monocle
|
||||||
Network = require("NetworkCenter")
|
Network = require("NetworkCenter")
|
||||||
@@ -37,23 +32,19 @@ local productivity = require("productivity")
|
|||||||
local quickNote = require("affine_quick_note")
|
local quickNote = require("affine_quick_note")
|
||||||
quickNote.init()
|
quickNote.init()
|
||||||
require("affine_clipper"):init()
|
require("affine_clipper"):init()
|
||||||
---
|
|
||||||
-- Load Google API Monitor
|
|
||||||
-- local googleMonitor = require("google_monitor")
|
|
||||||
-- googleMonitor.init()
|
|
||||||
--
|
|
||||||
|
|
||||||
-- Load Spoon Files
|
-- Load Spoon Files
|
||||||
hs.loadSpoon('SpoonInstall')
|
hs.loadSpoon('SpoonInstall')
|
||||||
hs.loadSpoon('SpeedMenu')
|
hs.loadSpoon('SpeedMenu')
|
||||||
hs.loadSpoon('BrewInfo')
|
hs.loadSpoon('BrewInfo')
|
||||||
-- hs.loadSpoon('Seal')
|
|
||||||
|
|
||||||
--- 3. Run/Configure Spoons
|
-- ==========================================
|
||||||
|
-- SPOON CONFIGURATION
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
---- SpeedMenu Config
|
---- SpeedMenu Config
|
||||||
if spoon.SpeedMenu then
|
if spoon.SpeedMenu then
|
||||||
-- 1. Define the Fix Function (includes MAC and IPv6)
|
-- Define the Fix Function (includes MAC and IPv6)
|
||||||
local function applyFullMenuFix()
|
local function applyFullMenuFix()
|
||||||
local interface = spoon.SpeedMenu.interface or "en0"
|
local interface = spoon.SpeedMenu.interface or "en0"
|
||||||
local ssid = hs.wifi.currentNetwork() or "Disconnected"
|
local ssid = hs.wifi.currentNetwork() or "Disconnected"
|
||||||
@@ -76,14 +67,14 @@ if spoon.SpeedMenu then
|
|||||||
spoon.SpeedMenu.menubar:setMenu(menuitems)
|
spoon.SpeedMenu.menubar:setMenu(menuitems)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 2. Hook the rescan method
|
-- Hook the rescan method
|
||||||
local oldRescan = spoon.SpeedMenu.rescan
|
local oldRescan = spoon.SpeedMenu.rescan
|
||||||
spoon.SpeedMenu.rescan = function(self)
|
spoon.SpeedMenu.rescan = function(self)
|
||||||
oldRescan(self)
|
oldRescan(self)
|
||||||
applyFullMenuFix()
|
applyFullMenuFix()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- 3. Toggle Logic (Starts as OFF)
|
-- Toggle Logic (Starts as OFF)
|
||||||
local speedMenuRunning = false
|
local speedMenuRunning = false
|
||||||
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function()
|
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function()
|
||||||
if speedMenuRunning then
|
if speedMenuRunning then
|
||||||
@@ -110,6 +101,4 @@ if spoon.BrewInfo then
|
|||||||
end
|
end
|
||||||
---- BrewInfo END
|
---- BrewInfo END
|
||||||
|
|
||||||
-- Add this line to the module section of your init.lua
|
|
||||||
|
|
||||||
hs.alert.show("Hammerspoon Config Reloaded")
|
hs.alert.show("Hammerspoon Config Reloaded")
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"screenCount" : 3,
|
||||||
|
"windows" : [
|
||||||
|
{
|
||||||
|
"x" : -952,
|
||||||
|
"bundleID" : "com.apple.finder",
|
||||||
|
"y" : 159,
|
||||||
|
"appName" : "Finder",
|
||||||
|
"h" : 492,
|
||||||
|
"w" : 920,
|
||||||
|
"winTitle" : "raycast-scripts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x" : 2070,
|
||||||
|
"bundleID" : "com.raycast.macos",
|
||||||
|
"y" : 121,
|
||||||
|
"appName" : "Raycast",
|
||||||
|
"h" : 319,
|
||||||
|
"w" : 1000,
|
||||||
|
"winTitle" : "Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x" : 1986,
|
||||||
|
"bundleID" : "com.google.GeminiMacOS",
|
||||||
|
"y" : 110,
|
||||||
|
"appName" : "Gemini",
|
||||||
|
"h" : 536,
|
||||||
|
"w" : 1002,
|
||||||
|
"winTitle" : "Gemini — Hammerspoon Menu Bar Icon Troubleshooting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x" : 0,
|
||||||
|
"bundleID" : "com.google.Chrome",
|
||||||
|
"y" : 39,
|
||||||
|
"appName" : "Google Chrome",
|
||||||
|
"h" : 957,
|
||||||
|
"w" : 1920,
|
||||||
|
"winTitle" : "francop - Dashboard - Gitea: Git with a cup of tea - Google Chrome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x" : 2794,
|
||||||
|
"bundleID" : "com.apple.Terminal",
|
||||||
|
"y" : 131,
|
||||||
|
"appName" : "Terminal",
|
||||||
|
"h" : 499,
|
||||||
|
"w" : 860,
|
||||||
|
"winTitle" : "francop — -zsh — 120×30"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x" : 0,
|
||||||
|
"bundleID" : "com.microsoft.VSCode",
|
||||||
|
"y" : 30,
|
||||||
|
"appName" : "Code",
|
||||||
|
"h" : 957,
|
||||||
|
"w" : 1920,
|
||||||
|
"winTitle" : "init.lua — .hammerspoon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"saveTime" : "2026-05-15 22:26:48",
|
||||||
|
"mode" : "Docked"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user