-
Notifications
You must be signed in to change notification settings - Fork 0
/
nimwin.nim
459 lines (358 loc) · 15.2 KB
/
nimwin.nim
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
import x11/xlib, x11/xutil, x11/x, x11/keysym
import threadpool, osproc, tables, sequtils, posix, strformat, os, sugar, options, strutils
var root : TWindow
proc handleBadWindow(display : PDisplay, ev : PXErrorEvent) : cint {.cdecl.} =
# resourceID maps to the Window's XID
# ev.resourceID
echo "Bad window", ": ", ev.resourceid
0
proc handleIOError(display : PDisplay) : cint {.cdecl.} =
0
template HandleKey(key : TKeySym, body : untyped) : untyped =
block:
if (XLookupKeySym(cast[PXKeyEvent](ev.xkey.addr), 0) == key.cuint):
body
type
WinPropKind = enum pkString, pkCardinal
WinProp = ref object of RootObj
name : string
case kind: WinPropKind
of pkString: strProp : string
of pkCardinal: cardinalProp : seq[uint]
type Window = ref object of RootObj
x : cint
y : cint
width : cint
height : cint
win : TWindow
screen : PScreen
props : seq[WinProp]
proc unpackCardinal(typeFormat : int,
nItems : int,
buf : ptr cuchar) : seq[uint] =
# See https://www.x.org/releases/current/doc/man/man3/XGetWindowProperty.3.xhtml
var byte_stride : int
case typeFormat
of 8:
byte_stride = (ptr cuchar).sizeof.int
of 16:
byte_stride = (ptr cshort).sizeof.int
of 32:
byte_stride = (ptr clong).sizeof.int
else:
return @[]
for i in 0..(nItems - 1):
let currentItem = cast[int](buf) + cast[int](i * byte_stride)
case typeFormat
of 8:
result &= cast[ptr cuchar](currentItem)[].uint
of 16:
result &= cast[ptr cshort](currentItem)[].uint
of 32:
result &= cast[ptr clong](currentItem)[].uint
else:
continue
proc getPropertyValue(display : PDisplay, window : TWindow, property : TAtom) : Option[WinProp] =
let longOffset : clong = 0.clong
let longLength : clong = high(int) # max length of the data to be returned
var actualType : TAtom
var actualTypeFormat : cint
var nitemsReturn : culong
var bytesAfterReturn : culong
var propValue : ptr cuchar
var currentAtomName = display.XGetAtomName(property)
var atomName = newString(currentAtomName.len)
copyMem(addr(atomName[0]), currentAtomName, currentAtomName.len)
discard currentAtomName.XFree
discard display.XGetWindowProperty(window,
property,
longOffset,
longLength,
false.cint,
AnyPropertyType.TAtom,
actualType.addr,
actualTypeFormat.addr,
nitemsReturn.addr,
bytesAfterReturn.addr,
propValue.addr)
let typeName = display.XGetAtomName(actualType)
if typeName == "STRING":
var propStrValue = newString(propValue.len)
if propStrValue.len > 0:
copyMem(addr(propStrValue[0]), propValue, propValue.len)
result = some(WinProp(name: atomName, kind: pkString, strProp: propStrValue))
else:
result = none(WinProp)
elif typeName == "CARDINAL":
result = some(
WinProp(
name: atomName,
kind: pkCardinal,
cardinalProp: unpackCardinal(actualTypeFormat.int, nitemsReturn.int, propValue)
)
)
else:
result = none(WinProp)
discard propValue.XFree
return
iterator getProperties(display : PDisplay, window : TWindow) : Option[WinProp] =
# Get property names/values of a given window on a display
var nPropsReturn : cint
# pointer to a list of word32
var atoms : PAtom = display.XListProperties(window, nPropsReturn.addr)
var currentAtom : PAtom
# Iterate over the list of atom names
for i in 0..(nPropsReturn.int - 1):
currentAtom = cast[PAtom](
cast[int](atoms) + cast[int](i * currentAtom[].sizeof)
)
yield display.getPropertyValue(window, currentAtom[])
discard atoms.XFree
proc getAttributes(display : PDisplay, window : PWindow) : Option[TXWindowAttributes] =
var attrs : TXWindowAttributes
if display.XGetWindowAttributes(window[], attrs.addr) == BadWindow:
return none(TXWindowAttributes)
return some(attrs)
proc changeEvMask(display : PDisplay, window : PWindow, eventMask : clong) =
var attributes : TXSetWindowAttributes
attributes.eventMask = eventMask
discard display.XChangeWindowAttributes(window[], CWEventMask, attributes.addr)
iterator getChildren(display : PDisplay) : Window =
var currentWindow : PWindow
var rootReturn : TWindow
var parentReturn : TWindow
var childrenReturn : PWindow
var nChildrenReturn : cuint
discard XQueryTree(display,
root,
rootReturn.addr,
parentReturn.addr,
childrenReturn.addr,
nChildrenReturn.addr)
for i in 0..(nChildrenReturn.int - 1):
currentWindow = cast[PWindow](
cast[int](childrenReturn) + cast[int](i * currentWindow.sizeof)
)
let attr : Option[TXWindowAttributes] = getAttributes(display, currentWindow)
if attr.isNone:
continue
if attr.get.map_state == IsUnmapped or attr.get.map_state == IsUnviewable:
continue
if attr.get.override_redirect == 1:
continue
let props = map(toSeq(getProperties(display, currentWindow[])).filterIt(it.isSome), (p) => p.get)
let win = Window(
x: attr.get.x.cint,
y: attr.get.y.cint,
width: attr.get.width,
height: attr.get.height,
win: currentWindow[],
screen: attr.get.screen,
props: props
)
for prop in props:
if prop.kind == pkCardinal:
if prop.name.startsWith("_NET_WM_STRUT"):
echo prop.name, ": ", prop.cardinalProp
elif prop.name.startsWith("_NET_WM_OPAQUE"):
echo prop.name, ": ", prop.cardinalProp
else:
echo prop.name, prop.kind
yield win
discard XFree(childrenReturn)
proc getDisplay : PDisplay =
result = XOpenDisplay(nil)
if result == nil:
quit("Failed to open display")
proc grabMouse(display : PDisplay, button : int) =
discard XGrabButton(display,
button.cuint,
Mod1Mask.cuint,
DefaultRootWindow(display),
1.cint,
ButtonPressMask or ButtonReleaseMask or PointerMotionMask,
GrabModeAsync,
GrabModeAsync,
None,
None)
proc grabKeyCombo(display : PDisplay,
key : TKeySym,
masks : seq[cuint] = @[]) =
# The reason we have 4 XGrabKey calls here is that
# the user might have num lock on
# and we still want to be able to grab these key combos
discard XGrabKey(display,
XKeySymToKeyCode(display, key).cint,
foldr(@[Mod1Mask.cuint] & masks, a or b),
DefaultRootWindow(display),
1.cint,
GrabModeAsync.cint,
GrabModeAsync.cint)
discard XGrabKey(display,
XKeySymToKeyCode(display, key).cint,
foldr(@[Mod1Mask.cuint, Mod2Mask.cuint] & masks, a or b),
DefaultRootWindow(display),
1.cint,
GrabModeAsync.cint,
GrabModeAsync.cint)
discard XGrabKey(display,
XKeySymToKeyCode(display, key).cint,
foldr(@[Mod1Mask.cuint, LockMask.cuint] & masks, a or b),
DefaultRootWindow(display),
1.cint,
GrabModeAsync.cint,
GrabModeAsync.cint)
discard XGrabKey(display,
XKeySymToKeyCode(display, key).cint,
foldr(@[Mod1Mask.cuint, LockMask.cuint, Mod2Mask.cuint] & masks, a or b),
DefaultRootWindow(display),
1.cint,
GrabModeAsync.cint,
GrabModeAsync.cint)
# When spawning a new process:
# Create an entry in a table or set with the PID
# Create a thread that simple waits for it to exit
# Send a message via channel to the main thread when it's done waiting for it to exit
# Check for events on the current iteration, close the process, remove it from the set of open processes
# Used to signal when a process has exited
# Obviously only used for processes nimwin manages
var processChan : Channel[int]
processChan.open(0)
proc startTerminal() : Process =
let terminal_path = getEnv("NIMWIN_TERMINAL", "/usr/bin/urxvt")
startProcess(terminal_path, "", ["-e", "tmux"])
proc launcher() : Process =
let launcher_path = getEnv("NIMWIN_LAUNCHER", "/usr/bin/dmenu_run")
startProcess(launcher_path)
proc handleProcess(p : Process) =
discard p.waitForExit
processChan.send(p.processID)
proc calculateStruts(display : PDisplay) : tuple[top: uint, bottom: uint]=
for win in getChildren(display):
for prop in win.props:
if prop.kind == pkCardinal and prop.name.startsWith("_NET_WM_STRUT"):
result.top = max(result.top, prop.cardinalProp[2])
result.bottom = max(result.bottom, prop.cardinalProp[3])
when isMainModule:
discard "~/.nimwin".expandTilde.existsOrCreateDir
var logFile : File = expandTilde("~/.nimwin/nimwin_log").open(fmWrite)
logFile.writeLine("Starting Nimwin")
var start : TXButtonEvent
var ev : TXEvent
var attr : TXWindowAttributes
let display = getDisplay()
let displayNum = display.DisplayString
logFile.writeLine(fmt"Opened display {displayNum}")
root = DefaultRootWindow(display)
display.changeEvMask(root.addr, SubstructureNotifyMask or StructureNotifyMask)
display.grabKeyCombo(XK_Return, @[ShiftMask.cuint])
display.grabKeyCombo(XK_T, @[ShiftMask.cuint])
display.grabKeyCombo(XK_Tab) # Cycle through windows
display.grabKeyCombo(XK_Q) # Restart window manager
display.grabKeyCombo(XK_P) # Launcher
display.grabKeyCombo(XK_T) # Full screen
display.grabKeyCombo(XK_C, @[ShiftMask.cuint]) # CLose a window
display.grabMouse(1)
display.grabMouse(3)
start.subWindow = None
var openProcesses = initTable[int, Process]() # hashset of processes
discard XSetErrorHandler(handleBadWindow)
discard XSetIOErrorHandler(handleIOError)
while true:
let processExited = processChan.tryRecv()
if processExited.dataAvailable:
openProcesses[processExited.msg].close
openProcesses.del(processExited.msg)
# TODO refactor using XPending or XCB?
discard XNextEvent(display, ev.addr)
# The reason we look at the subwindow is because we grabbed the root window
# and we want events in its children
# For spawning, e.g. a terminal we also want events for the root window
if ev.theType == KeyPress:
HandleKey(XK_Return):
let p = startTerminal()
openProcesses[p.processID] = p
spawn handleProcess(p)
HandleKey(XK_C):
let windowStack = toSeq(getChildren(display))
if windowStack.len > 0:
discard display.XDestroyWindow(windowStack[^1].win)
HandleKey(XK_Tab):
if ev.xKey.subWindow != None:
# Cycle through subwindows of the root window
#discard XCirculateSubwindows(display, root, RaiseLowest)
#discard display.XFlush()
let ignored = @["_NET_WM_STRUT_PARTIAL", "_NET_WM_STRUT"]
let windowStack = filter(toSeq(getChildren(display)), (w) => not w.props.anyIt(it.name.in(ignored)))
if windowStack.len > 0:
echo "Tab cycling shit, raising this window: ", windowStack[0].win
discard display.XSetInputFocus(windowStack[0].win, RevertToPointerRoot, CurrentTime)
discard display.XRaiseWindow(windowStack[0].win)
HandleKey(XK_P):
let p = launcher()
openProcesses[p.processID] = p
spawn handleProcess(p)
HandleKey(XK_Q):
let currentPath = getAppDir()
if fmt"{currentPath}/nimwin".existsFile:
logFile.writeLine("Trying to restart Nimwin")
logFile.writeLine(fmt"Restarting: executing {currentPath}/nimwin on display={displayNum}")
logFile.flushFile
discard display.XCloseDisplay
let restartResult = execvp(fmt"{currentPath}/nimwin".cstring, nil)
if restartResult == -1:
quit("Failed to restart Nimwin")
HandleKey(XK_T):
# Get all of the struts with offsets from the top
# Get all of the struts with offsets from the bottomm
# and the left and the right
#
# then subtract the max of the offsets from the top from the screenHeight
#
if ev.xKey.subWindow != None:
let rootAttrs = getAttributes(display, root.addr)
if rootAttrs.isSome:
let struts = display.calculateStruts
let screenHeight = rootAttrs.get.height
let screenWidth = rootAttrs.get.width
let winAttrs : Option[TXWindowAttributes] = getAttributes(display, ev.xKey.subWindow.addr)
let depth = winAttrs.get.borderWidth.cuint
let borderWidth = winAttrs.get.depth.cuint
discard XMoveResizeWindow(display,
ev.xKey.subWindow,
0, struts.top.cint,
screenWidth.cuint, screenHeight.cuint - struts.bottom.cuint - borderWidth.cuint)
elif (ev.theType == ButtonPress) and (ev.xButton.subWindow != None):
discard XGetWindowAttributes(display, ev.xButton.subWindow, attr.addr)
start = ev.xButton
elif (ev.theType == MapNotify) and (ev.xmap.overrideRedirect == 0):
let rootAttrs = getAttributes(display, root.addr)
if rootAttrs.isSome:
let struts = display.calculateStruts
let screenHeight = rootAttrs.get.height
let screenWidth = rootAttrs.get.width
let winAttrs : Option[TXWindowAttributes] = getAttributes(display, ev.xmap.window.addr)
if winAttrs.isSome and winAttrs.get.overrideRedirect == 0:
discard XMoveResizeWindow(display,
ev.xmap.window,
0, struts.top.cint,
screenWidth.cuint, screenHeight.cuint)
discard display.XSetInputFocus(ev.xmap.window, RevertToPointerRoot, CurrentTime)
elif (ev.theType == MotionNotify) and (start.subWindow != None):
# Discard any following MotionNotify events
# This avoids "movement lag"
while display.XCheckTypedEvent(MotionNotify, ev.addr) != 0:
continue
discard display.XFlush()
var xDiff : int = ev.xButton.xRoot - start.xRoot
var yDiff : int = ev.xButton.yRoot - start.yRoot
discard XMoveResizeWindow(display,
start.subWindow,
attr.x + (if start.button == 1: xDiff else: 0).cint,
attr.y + (if start.button == 1: yDiff else: 0).cint,
max(1, attr.width + (if start.button == 3: xDiff else: 0)).cuint,
max(1, attr.height + (if start.button == 3: yDiff else: 0)).cuint)
elif ev.theType == ButtonRelease:
start.subWindow = None
else:
continue