-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwindow_manager.lua
More file actions
183 lines (155 loc) · 5.01 KB
/
Copy pathwindow_manager.lua
File metadata and controls
183 lines (155 loc) · 5.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
-- Window Manager Module for Hammerspoon
-- Rectangle-style window management with keyboard shortcuts
-- Supports cycling through 1/2 → 1/3 → 2/3 fractions
local M = {}
-- Private state
local hotkeys = {}
local cycleState = {} -- { [windowId] = { action = "left", index = 1 } }
local fractions = {1/2, 1/3, 2/3}
-- Get focused window, skip fullscreen
local function getFocusedWindow()
local win = hs.window.focusedWindow()
if not win or win:isFullScreen() then
return nil
end
return win
end
-- Set window frame with instant snapping
local function setFrame(win, frame)
local prevDuration = hs.window.animationDuration
hs.window.animationDuration = 0
win:setFrame(frame)
hs.window.animationDuration = prevDuration
end
-- Get next cycle index for a window/action combination
local function getNextCycleIndex(windowId, action)
local state = cycleState[windowId]
if state and state.action == action then
-- Same action on same window, advance cycle
local nextIndex = (state.index % #fractions) + 1
cycleState[windowId] = { action = action, index = nextIndex }
return nextIndex
else
-- Different action or window, start at 1
cycleState[windowId] = { action = action, index = 1 }
return 1
end
end
-- Position window with cycling support
local function positionWindow(action)
local win = getFocusedWindow()
if not win then return end
local windowId = win:id()
local screenFrame = win:screen():frame()
local cycleIndex = getNextCycleIndex(windowId, action)
local fraction = fractions[cycleIndex]
local frame
if action == "left" then
frame = {
x = screenFrame.x,
y = screenFrame.y,
w = screenFrame.w * fraction,
h = screenFrame.h
}
elseif action == "right" then
frame = {
x = screenFrame.x + screenFrame.w * (1 - fraction),
y = screenFrame.y,
w = screenFrame.w * fraction,
h = screenFrame.h
}
elseif action == "top" then
frame = {
x = screenFrame.x,
y = screenFrame.y,
w = screenFrame.w,
h = screenFrame.h * fraction
}
elseif action == "bottom" then
frame = {
x = screenFrame.x,
y = screenFrame.y + screenFrame.h * (1 - fraction),
w = screenFrame.w,
h = screenFrame.h * fraction
}
end
setFrame(win, frame)
end
-- Non-cycling position functions
local function maximize()
local win = getFocusedWindow()
if not win then return end
local screenFrame = win:screen():frame()
setFrame(win, screenFrame)
-- Clear cycle state for this window
cycleState[win:id()] = nil
end
local function nextDisplay()
local win = getFocusedWindow()
if not win then return end
local currentScreen = win:screen()
local currentFrame = currentScreen:frame()
local screens = hs.screen.allScreens()
-- Find the screen to the right (smallest x that is > current x)
local targetScreen = nil
local minX = math.huge
for _, screen in ipairs(screens) do
local frame = screen:frame()
if frame.x > currentFrame.x and frame.x < minX then
minX = frame.x
targetScreen = screen
end
end
if targetScreen then
win:moveToScreen(targetScreen, false, true, 0)
end
end
local function prevDisplay()
local win = getFocusedWindow()
if not win then return end
local currentScreen = win:screen()
local currentFrame = currentScreen:frame()
local screens = hs.screen.allScreens()
-- Find the screen to the left (largest x that is < current x)
local targetScreen = nil
local maxX = -math.huge
for _, screen in ipairs(screens) do
local frame = screen:frame()
if frame.x < currentFrame.x and frame.x > maxX then
maxX = frame.x
targetScreen = screen
end
end
if targetScreen then
win:moveToScreen(targetScreen, false, true, 0)
end
end
-- Public API
function M.init(cfg)
cfg = cfg or {}
local mods = {"ctrl", "alt", "cmd"}
local bindings = {
{ mods, "left", function() positionWindow("left") end },
{ mods, "right", function() positionWindow("right") end },
{ mods, "up", function() positionWindow("top") end },
{ mods, "down", function() positionWindow("bottom") end },
{ mods, "f", maximize },
{ mods, "end", nextDisplay }, -- fn+right produces "end"
{ mods, "home", prevDisplay }, -- fn+left produces "home"
}
for _, binding in ipairs(bindings) do
local hk = hs.hotkey.bind(binding[1], binding[2], binding[3])
table.insert(hotkeys, hk)
end
print("Window Manager loaded (Rectangle-style shortcuts with cycling)")
return M
end
function M.stop()
for _, hk in ipairs(hotkeys) do
hk:delete()
end
hotkeys = {}
cycleState = {}
print("Window Manager stopped")
end
return M