├── config.nims ├── .gitignore ├── assets └── worm.desktop ├── src ├── log.nim ├── events │ ├── expose.nim │ ├── buttonrelease.nim │ ├── configurenotify.nim │ ├── leavenotify.nim │ ├── configurerequest.nim │ ├── unmapnotify.nim │ ├── destroynotify.nim │ ├── propertynotify.nim │ ├── enternotify.nim │ ├── motionnotify.nim │ ├── buttonpress.nim │ ├── maprequest.nim │ └── clientmessage.nim ├── worm.nim ├── events.nim ├── types.nim ├── atoms.nim ├── wormc.nim └── wm.nim ├── worm.nimble ├── .github └── workflows │ └── build.yml ├── examples ├── sxhkdrc ├── rc └── jgmenu_run ├── LICENSE ├── README.md ├── index.html ├── docs └── wormc.md └── logo.svg /config.nims: -------------------------------------------------------------------------------- 1 | switch("gc", "arc") 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | worm 2 | wormc 3 | a.out 4 | -------------------------------------------------------------------------------- /assets/worm.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Worm 3 | Comment=A floating, tag-based window manager written in Nim 4 | Exec=worm 5 | DesktopNames=Worm 6 | X-LightDM-DesktopName=worm 7 | Type=Application 8 | -------------------------------------------------------------------------------- /src/log.nim: -------------------------------------------------------------------------------- 1 | import std/logging 2 | 3 | var logger = newConsoleLogger(fmtStr = "$time | $levelname | ") 4 | logger.addHandler 5 | 6 | template log*(lvl: Level, data: string): untyped = 7 | let pos = instantiationInfo() 8 | let addition = "" & pos.filename & ":" & $pos.line & " | " # % [pos.filename, $pos.line] 9 | logger.log(lvl, addition & data) 10 | 11 | template log*(data: string): untyped = log(lvlInfo, data) 12 | -------------------------------------------------------------------------------- /worm.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.2.5" 4 | author = "codic12" 5 | description = "Window manager " 6 | license = "MIT" 7 | srcDir = "src" 8 | bin = @["worm", "wormc"] 9 | 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.4.8" 14 | requires "x11" 15 | # requires "cairo" # disgusting 16 | requires "pixie" # chad native Nim drawing library. lets go! 17 | requires "regex" 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | nim: [ 'stable' ] 11 | name: Nim ${{ matrix.nim }} worm build 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup nim 15 | uses: jiro4989/setup-nim-action@v1 16 | with: 17 | nim-version: ${{ matrix.nim }} 18 | - run: nimble -y build -d:release --gc:arc 19 | -------------------------------------------------------------------------------- /src/events/expose.nim: -------------------------------------------------------------------------------- 1 | import ../wm, ../types 2 | import x11/[xlib, x] 3 | import std/options 4 | 5 | proc handleExpose*(self: var Wm; ev: XExposeEvent): void = 6 | let clientOpt = self.findClient do (client: Client) -> bool: client.window == ev.window 7 | if clientOpt.isNone: return 8 | let client = clientOpt.get[0] 9 | discard self.dpy.XMapWindow client.frame.window 10 | discard self.dpy.XSetInputFocus(client.window, RevertToPointerRoot, CurrentTime) 11 | discard self.dpy.XRaiseWindow client.frame.window 12 | 13 | -------------------------------------------------------------------------------- /examples/sxhkdrc: -------------------------------------------------------------------------------- 1 | # Restart worm 2 | super + ctrl + r 3 | worm 4 | 5 | # Quit worm 6 | ctrl + alt + q 7 | pkill worm 8 | 9 | # Close app 10 | super + q 11 | wormc close-active-client 12 | 13 | # Maximize app 14 | super + f 15 | wormc maximize-active-client 16 | 17 | # Minimize app 18 | super + h 19 | wormc minimize-active-client 20 | 21 | # Tags 22 | super + {_, shift + } {1-9} 23 | wormc {switch,move-active}-tag {1-9} 24 | 25 | # Set master windows 26 | super + m 27 | wormc master-active 28 | 29 | # Float a window 30 | super + shift + space 31 | wormc float-active 32 | -------------------------------------------------------------------------------- /src/events/buttonrelease.nim: -------------------------------------------------------------------------------- 1 | import ../wm 2 | import x11/[xlib,x] 3 | import std/options 4 | import ../types 5 | 6 | proc handleButtonRelease*(self: var Wm; ev: XButtonEvent): void = 7 | #if ev.subwindow == None or ev.window == self.root: return 8 | if self.motionInfo.isSome: discard self.dpy.XUngrabPointer CurrentTime 9 | self.motionInfo = none MotionInfo 10 | # let clientOpt = self.findClient do (client: Client) -> 11 | # bool: client.frame.window == ev.window 12 | # if clientOpt.isNone: return 13 | # let client = clientOpt.get[0] 14 | # self.renderTop client[] 15 | for client in self.clients.mitems: self.renderTop client 16 | -------------------------------------------------------------------------------- /src/worm.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/[os, osproc, parseopt], 3 | wm, 4 | events, 5 | log 6 | 7 | when isMainModule: 8 | var instance = initWm() 9 | 10 | var rcFile = "~/.config/worm/rc" 11 | 12 | var p = initOptParser("") 13 | while true: 14 | p.next() 15 | case p.kind 16 | of cmdEnd: break 17 | of cmdLongOption: 18 | case p.key 19 | of "rcfile": 20 | rcFile = p.val 21 | else: discard 22 | else: discard 23 | 24 | if fileExists expandTilde rcFile: 25 | log "config file found, loading..." 26 | discard startProcess expandTilde rcFile 27 | log "config file loaded!" 28 | 29 | instance.eventLoop() 30 | -------------------------------------------------------------------------------- /src/events/configurenotify.nim: -------------------------------------------------------------------------------- 1 | import ../wm, ../types 2 | import x11/xlib 3 | import std/options 4 | 5 | proc handleConfigureNotify*(self: var Wm; ev: XConfigureEvent): void = 6 | let clientOpt = self.findClient do (client: Client) -> bool: client.window == ev.window 7 | if clientOpt.isNone: return 8 | let client = clientOpt.get[0] 9 | if not client.fullscreen: # and not (ev.x == 0 and ev.y == cint self.config.frameHeight): 10 | discard self.dpy.XResizeWindow(client.frame.window, cuint ev.width, 11 | cuint ev.height + cint client.frameHeight) 12 | discard self.dpy.XMoveWindow(client.window, 0, cint client.frameHeight) 13 | # if self.layout == lyTiling: self.tileWindows 14 | self.renderTop client[] 15 | -------------------------------------------------------------------------------- /src/events/leavenotify.nim: -------------------------------------------------------------------------------- 1 | import ../wm, ../types 2 | import x11/xlib 3 | # import std/options 4 | 5 | proc handleLeaveNotify*(self: var Wm; ev: XLeaveWindowEvent): void = 6 | for client in self.clients.mitems: 7 | if client.frame.close == ev.subwindow or client.frame.close == ev.window: 8 | client.frame.closeHovered = false 9 | self.renderTop client 10 | break 11 | elif client.frame.maximize == ev.subwindow or client.frame.maximize == ev.window: 12 | client.frame.maximizeHovered = false 13 | self.renderTop client 14 | break 15 | elif client.frame.minimize == ev.subwindow or client.frame.minimize == ev.window: 16 | client.frame.minimizeHovered = false 17 | self.renderTop client 18 | break -------------------------------------------------------------------------------- /src/events/configurerequest.nim: -------------------------------------------------------------------------------- 1 | import ../wm 2 | import ../types 3 | import x11/[xlib] 4 | import std/options 5 | 6 | proc handleConfigureRequest*(self: var Wm; ev: XConfigureRequestEvent): void = 7 | var changes = XWindowChanges(x: ev.x, y: ev.y, width: ev.width, 8 | height: ev.height, borderWidth: ev.borderWidth, sibling: ev.above, 9 | stackMode: ev.detail) 10 | discard self.dpy.XConfigureWindow(ev.window, cuint ev.valueMask, addr changes) 11 | let clientOpt = self.findClient do (client: Client) -> bool: client.window == ev.window 12 | if clientOpt.isNone: return 13 | let client = clientOpt.get[0] 14 | if ev.x != 0 and ev.y != 0: discard self.dpy.XMoveWindow(client.frame.window, ev.x, ev.y) 15 | if self.layout == lyTiling: self.tileWindows 16 | self.renderTop client[] -------------------------------------------------------------------------------- /src/events/unmapnotify.nim: -------------------------------------------------------------------------------- 1 | import ../wm, ../types 2 | import x11/[xlib,x] 3 | import std/options 4 | 5 | proc handleUnmapNotify*(self: var Wm; ev: XUnmapEvent): void = 6 | let clientOpt = self.findClient do (client: Client) -> bool: client.window == ev.window 7 | if clientOpt.isNone: return 8 | let client = clientOpt.get[0] 9 | discard self.dpy.XUnmapWindow client.frame.window 10 | self.clients.del clientOpt.get[1] 11 | self.updateClientList 12 | discard self.dpy.XSetInputFocus(self.root, RevertToPointerRoot, CurrentTime) 13 | self.focused.reset # TODO: focus last window 14 | for i, locClient in self.clients: 15 | discard self.dpy.XSetWindowBorder(locClient.frame.window, 16 | self.config.borderInactivePixel) 17 | if self.layout == lyTiling: self.tileWindows 18 | -------------------------------------------------------------------------------- /src/events/destroynotify.nim: -------------------------------------------------------------------------------- 1 | import ../wm, ../types 2 | import x11/[xlib,x] 3 | import std/options 4 | 5 | proc handleDestroyNotify*(self: var Wm; ev: XDestroyWindowEvent): void = 6 | let clientOpt = self.findClient do (client: Client) -> bool: client.window == ev.window 7 | if clientOpt.isNone: return 8 | let client = clientOpt.get[0] 9 | discard self.dpy.XDestroyWindow client.frame.window 10 | self.clients.del clientOpt.get[1] 11 | self.updateClientList 12 | discard self.dpy.XSetInputFocus(self.root, RevertToPointerRoot, CurrentTime) 13 | self.focused.reset # TODO: focus last window 14 | for i, locClient in self.clients: 15 | discard self.dpy.XSetWindowBorder(locClient.frame.window, 16 | self.config.borderInactivePixel) 17 | if self.layout == lyTiling: self.tileWindows 18 | -------------------------------------------------------------------------------- /src/events/propertynotify.nim: -------------------------------------------------------------------------------- 1 | import ../wm, ../types, ../atoms 2 | import x11/[xlib, x] 3 | import std/options 4 | 5 | proc handlePropertyNotify*(self: var Wm; ev: XPropertyEvent): void = 6 | let clientOpt = self.findClient do (client: Client) -> bool: client.window == ev.window 7 | if clientOpt.isNone: return 8 | let client = clientOpt.get[0] 9 | let title = block: 10 | var atr: Atom 11 | var afr: cint 12 | var nr: culong 13 | var bar: culong 14 | var prop_return: ptr char 15 | discard self.dpy.XGetWindowProperty(ev.window, self.netAtoms[NetWMName], 16 | 0, high clong, false, self.dpy.XInternAtom("UTF8_STRING", false), 17 | addr atr, addr afr, addr nr, addr bar, addr prop_return) 18 | if prop_return == nil: discard self.dpy.XFetchName(ev.window, cast[ 19 | ptr cstring](addr prop_return)) 20 | $cast[cstring](prop_return) 21 | if client.title == title: return 22 | client.title = title 23 | self.renderTop client[] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright codic12 2021 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | 9 | -------------------------------------------------------------------------------- /src/events/enternotify.nim: -------------------------------------------------------------------------------- 1 | import ../wm, ../types 2 | import x11/xlib, x11/x 3 | import std/options 4 | 5 | proc handleEnterNotify*(self: var Wm; ev: XEnterWindowEvent): void = 6 | for client in self.clients.mitems: 7 | if client.frame.close == ev.subwindow or client.frame.close == ev.window: 8 | client.frame.closeHovered = true 9 | self.renderTop client 10 | break 11 | elif client.frame.maximize == ev.subwindow or client.frame.maximize == ev.window: 12 | client.frame.maximizeHovered = true 13 | self.renderTop client 14 | break 15 | elif client.frame.minimize == ev.subwindow or client.frame.minimize == ev.window: 16 | client.frame.minimizeHovered = true 17 | self.renderTop client 18 | break 19 | if self.focusMode == FocusFollowsMouse: 20 | let clientOpt = self.findClient do (client: Client) -> 21 | bool: client.frame.window == ev.window or client.frame.window == ev.subwindow 22 | if clientOpt.isNone: return 23 | let client = clientOpt.get[0] 24 | discard self.dpy.XRaiseWindow client.frame.window 25 | discard self.dpy.XSetInputFocus(client.window, RevertToPointerRoot, CurrentTime) 26 | discard self.dpy.XMapWindow client.frame.window 27 | self.focused = some clientOpt.get[1] 28 | self.raiseClient clientOpt.get[0][] 29 | -------------------------------------------------------------------------------- /src/events.nim: -------------------------------------------------------------------------------- 1 | import 2 | x11/[x, xlib], 3 | wm, 4 | events/[ 5 | buttonpress, 6 | buttonrelease, 7 | clientmessage, 8 | configurenotify, 9 | configurerequest, 10 | destroynotify, 11 | expose, 12 | maprequest, 13 | motionnotify, 14 | propertynotify, 15 | unmapnotify, 16 | enternotify, 17 | leavenotify 18 | ] 19 | 20 | proc dispatchEvent*(self: var Wm; ev: XEvent) = 21 | case ev.theType: 22 | of ButtonPress: self.handleButtonPress ev.xbutton 23 | of ButtonRelease: self.handleButtonRelease ev.xbutton 24 | of MotionNotify: self.handleMotionNotify ev.xmotion 25 | of MapRequest: self.handleMapRequest ev.xmaprequest 26 | of ConfigureRequest: self.handleConfigureRequest ev.xconfigurerequest 27 | of ConfigureNotify: self.handleConfigureNotify ev.xconfigure 28 | of UnmapNotify: self.handleUnmapNotify ev.xunmap 29 | of DestroyNotify: self.handleDestroyNotify ev.xdestroywindow 30 | of ClientMessage: self.handleClientMessage ev.xclient 31 | of Expose: self.handleExpose ev.xexpose 32 | of PropertyNotify: self.handlePropertyNotify ev.xproperty 33 | of EnterNotify: self.handleEnterNotify ev.xcrossing 34 | of LeaveNotify: self.handleLeaveNotify ev.xcrossing 35 | else: discard 36 | 37 | proc eventLoop*(self: var Wm) = 38 | while true: 39 | discard self.dpy.XNextEvent(unsafeAddr self.currEv) 40 | self.dispatchEvent self.currEv 41 | 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Worm 4 | 5 | Worm is a tiny, dynamic, tag-based window manager written in the Nim language. 6 | It supports both a floating mode and master-stack tiling with gaps and struts. 7 | Check out the [screenshots on the wiki](https://github.com/codic12/worm/wiki/Screenshots) for some examples of how it looks. 8 | 9 | ## Build / install 10 | 11 | Install Nim >= 1.6.0, for example through Choosenim. Clone this repo and run 12 | 13 | ``` 14 | $ nimble build -d:release 15 | ``` 16 | And you should end up with two binaries; strip and use! 17 | 18 | Alternatively, for users using Arch, you can use the AUR package worm-git. 19 | 20 | ``` 21 | $ yay -S worm-git 22 | ``` 23 | 24 | ## Configuration 25 | 26 | ### Autostart 27 | 28 | Worm will try to execute the file `~/.config/worm/rc` on startup. 29 | Simply create it as a shell-script to execute your favorite applications with 30 | worm. 31 | (don't forget to make it executable). 32 | 33 | An example can be found [here](/examples/rc). 34 | 35 | ### Keybindings 36 | 37 | Worm does not have a built-in keyboard mapper, so you should use something like 38 | [sxhkd](https://github.com/baskerville/sxhkd). 39 | Please read [the docs](docs/wormc.md) to understand how wormc works before 40 | writing your own sxhkdrc. 41 | 42 | An example sxhkdrc can be found [here](/examples/sxhkdrc). 43 | 44 | ## License 45 | 46 | Licensed under the MIT license. See the LICENSE file for more info. 47 | 48 | ## Credits 49 | 50 | Thanks to [phisch](https://github.com/phisch) for making the logo! 51 | 52 | Thanks to everyone else that's opened an issue, a PR, or just tried out worm. 53 | 54 | -------------------------------------------------------------------------------- /examples/rc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # | NOTE | NOTE | NOTE | NOTE | 3 | # if you use this configuration, make sure that the paths are all right for the configuration file. this is just an example!!! 4 | # you would also need things like nitrogen and polybar installed. 5 | exec nitrogen --restore & 6 | 7 | exec polybar top -q -c ~/.config/worm/polybar/config.ini & 8 | 9 | exec mpd & 10 | 11 | exec sxhkd -c ~/.config/worm/sxhkdrc & 12 | 13 | wormc border-width 6 14 | 15 | ## pywal support 16 | # # CONFIGURE 17 | # pywal_active_index="3" # color3 18 | # pywal_inactive_index="4" # color4 19 | # 20 | # # CODE 21 | # contrast_text_for () { 22 | # # Formula from: 23 | # # https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color 24 | # # https://www.w3.org/TR/AERT/#color-contrast 25 | # if perl -e "exit ((1 - (0.299 * $((16#${1:0:2})) + 0.587 * $((16#${1:2:2})) + 0.114 * $((16#${1:4:2}))) / 255) > 0.5)"; then 26 | # echo $((16#FF000000)) 27 | # else 28 | # echo $((16#FFFFFFFF)) 29 | # fi 30 | # } 31 | # pywal_active="$(sed -n $(($pywal_active_index + 1))p ~/.cache/wal/colors | tail -c +2)" 32 | # pywal_inactive="$(sed -n $(($pywal_inactive_index + 1))p ~/.cache/wal/colors | tail -c +2)" 33 | # wormc border-active-pixel $((16#FF$pywal_active)) 34 | # wormc border-inactive-pixel $((16#FF$pywal_inactive)) 35 | # wormc frame-active-pixel $((16#FF$pywal_active)) 36 | # wormc frame-inactive-pixel $((16#FF$pywal_inactive)) 37 | # wormc text-active-pixel $(contrast_text_for $pywal_active) 38 | # wormc text-inactive-pixel $(contrast_text_for $pywal_inactive) 39 | 40 | wormc layout tiling 41 | wormc struts 75 20 20 20 42 | wormc gaps 20 43 | wormc frame-height 0 44 | -------------------------------------------------------------------------------- /src/types.nim: -------------------------------------------------------------------------------- 1 | import x11/[xlib, x, xft] 2 | import std/options 3 | 4 | type 5 | Layout* = enum 6 | lyFloating, lyTiling 7 | FramePart* = enum 8 | fpTitle, fpClose, fpMaximize, fpMinimize 9 | ButtonState* = enum 10 | bsActive, bsInactive, bsActiveHover, bsInactiveHover 11 | Geometry* = object 12 | x*, y*: int 13 | width*, height*: uint 14 | MotionInfo* = object 15 | start*: XButtonEvent 16 | attr*: XWindowAttributes 17 | Frame* = object 18 | window*, top*, title*: Window 19 | close*, maximize*, minimize*: Window # button parts 20 | closeHovered*, maximizeHovered*, minimizeHovered*: bool # is the button being hovered over? for reading by renderTop, and set by Enter / Leave Notify events. 21 | Client* = object 22 | window*: Window 23 | frame*: Frame 24 | draw*: ptr XftDraw 25 | color*: XftColor 26 | title*: string 27 | beforeGeom*: Option[Geometry] # Previous geometry *of the frame* pre-fullscreen. 28 | fullscreen*: bool # Whether this client is currently fullscreened or not (EWMH, or otherwise ig) 29 | floating*: bool # If tiling is on, whether this window is currently floating or not. If it's floating it won't be included in the tiling. 30 | tags*: TagSet 31 | frameHeight*: uint 32 | csd*: bool 33 | class*: string 34 | beforeGeomMax*: Option[Geometry] 35 | maximized*: bool 36 | minimized*: bool 37 | ButtonPaths = array[ButtonState, string] 38 | Config* = object 39 | borderActivePixel*, borderInactivePixel*, borderWidth*: uint 40 | frameActivePixel*, frameInactivePixel*, frameHeight*: uint 41 | textActivePixel*: uint 42 | textInactivePixel*: uint 43 | textOffset*, buttonOffset*: tuple[x, y: uint] 44 | gaps*: int # TODO: fix the type errors and change this to unsigned integers. 45 | struts*: tuple[top, bottom, left, right: uint] 46 | frameParts*: tuple[left, center, right: seq[FramePart]] 47 | buttonSize*: uint # always square FOR NOW 48 | rootMenu*: string 49 | closePaths*, minimizePaths*, maximizePaths*: ButtonPaths 50 | modifier*: uint32 51 | TagSet* = array[9, bool] # distinct 52 | FocusMode* = enum 53 | FocusFollowsClick, FocusFollowsMouse 54 | 55 | proc defaultTagSet*: TagSet = [true, false, false, false, false, false, false, 56 | false, false] # put the user on tag 1 when the wm starts. 57 | 58 | proc switchTag*(self: var TagSet; tag: uint8): void = 59 | for i, _ in self: self[i] = false 60 | self[tag] = true 61 | 62 | -------------------------------------------------------------------------------- /src/events/motionnotify.nim: -------------------------------------------------------------------------------- 1 | import ../wm, ../types, ../log 2 | import std/options 3 | import x11/[xlib, x] 4 | 5 | proc handleMotionNotify*(self: var Wm; ev: XMotionEvent): void = 6 | #log "MotionNotify" 7 | #if ev.subwindow == None or ev.window == self.root 8 | if self.motionInfo.isNone: return 9 | let clientOpt = self.findClient do (client: Client) -> 10 | bool: 11 | client.frame.window == ev.window or client.frame.title == ev.window 12 | # false 13 | if clientOpt.isNone: return 14 | let client = clientOpt.get[0] 15 | if client.fullscreen: return 16 | let motionInfo = self.motionInfo.get 17 | while self.dpy.XCheckTypedEvent(MotionNotify, addr self.currEv): discard 18 | let 19 | xdiff = ev.x_root - motionInfo.start.x_root 20 | ydiff = ev.y_root - motionInfo.start.y_root 21 | # todo hoist w/h out 22 | discard self.dpy.XMoveResizeWindow(client.frame.window, motionInfo.attr.x + ( 23 | if motionInfo.start.button == 1: xdiff else: 0), motionInfo.attr.y + ( 24 | if motionInfo.start.button == 1: ydiff else: 0), 1.max( 25 | motionInfo.attr.width + (if motionInfo.start.button == 26 | 3: xdiff else: 0)).cuint, 1.max(motionInfo.attr.height + ( 27 | if motionInfo.start.button == 3: ydiff else: 0)).cuint) 28 | discard self.dpy.XResizeWindow(client.window, 1.max( 29 | motionInfo.attr.width + (if motionInfo.start.button == 30 | 3: xdiff else: 0)).cuint, 1.max(motionInfo.attr.height + ( 31 | if motionInfo.start.button == 3: ydiff else: 0) - 32 | cint client.frameHeight).cuint) 33 | let conf = XConfigureEvent(theType: ConfigureNotify, display: self.dpy, 34 | event: client.window, window: client.window, x: motionInfo.attr.x + ( 35 | if motionInfo.start.button == 1: xdiff else: 0), y: motionInfo.attr.y + ( 36 | if motionInfo.start.button == 1: ydiff else: 0) + client.frameHeight.int32, width: cint 1.max( 37 | motionInfo.attr.width + (if motionInfo.start.button == 38 | 3: xdiff else: 0)).cuint, height: cint 1.max( 39 | motionInfo.attr.height + 40 | (if motionInfo.start.button == 3: ydiff else: 0) - 41 | cint client.frameHeight).cuint) 42 | discard self.dpy.XSendEvent(client.window, false, StructureNotifyMask, cast[ 43 | ptr XEvent](unsafeAddr conf)) 44 | discard self.dpy.XResizeWindow(client.frame.top, 1.max( 45 | motionInfo.attr.width + (if motionInfo.start.button == 46 | 3: xdiff else: 0)).cuint, cuint client.frameHeight) 47 | discard self.dpy.XResizeWindow(client.frame.title, 1.max( 48 | motionInfo.attr.width + (if motionInfo.start.button == 49 | 3: xdiff else: 0)).cuint, cuint client.frameHeight) 50 | 51 | -------------------------------------------------------------------------------- /examples/jgmenu_run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck disable=2039,2086 3 | 4 | : ${JGMENU_EXEC_DIR=/usr/lib/jgmenu} 5 | 6 | export PATH="$JGMENU_EXEC_DIR:$PATH" 7 | JGMENU_LOCKFILE=~/.jgmenu-lockfile 8 | 9 | die () { 10 | printf "fatal: %s\n" "$1" 11 | exit 1 12 | } 13 | 14 | usage () { 15 | printf "usage: jgmenu_run\n" 16 | printf " jgmenu_run []\n\n" 17 | printf "Top level convenience wrapper for jgmenu\n\n" 18 | printf "If no 'command' or argument is specified, jgmenu_run does one of the following:\n" 19 | printf " - Shows the menu if an instance of jgmenu is already running\n" 20 | printf " - Starts jgmenu in 'stay-alive' mode (i.e. as a long-running application)\n" 21 | printf " using apps unless otherwise specified by 'csv_cmd' in jgmenurc\n\n" 22 | printf "Commands are only intended for developers when plugging together modules.\n\n" 23 | exit 0 24 | } 25 | 26 | print_exec_path () { 27 | printf "%b\n" "${JGMENU_EXEC_DIR}" 28 | exit 0 29 | } 30 | 31 | # Pipe to UNIX socket if tint2 button was used. 32 | send_tint2_env_vars_to_jgmenu () { 33 | # Check set/unset with ${parameter+word} 34 | test -z "${TINT2_BUTTON_ALIGNED_X1+x}" && return 35 | printf "%b" \ 36 | "TINT2_BUTTON_ALIGNED_X1=${TINT2_BUTTON_ALIGNED_X1}\n" \ 37 | "TINT2_BUTTON_ALIGNED_X2=${TINT2_BUTTON_ALIGNED_X2}\n" \ 38 | "TINT2_BUTTON_ALIGNED_Y1=${TINT2_BUTTON_ALIGNED_Y1}\n" \ 39 | "TINT2_BUTTON_ALIGNED_Y2=${TINT2_BUTTON_ALIGNED_Y2}\n" \ 40 | "TINT2_BUTTON_PANEL_X1=${TINT2_BUTTON_PANEL_X1}\n" \ 41 | "TINT2_BUTTON_PANEL_X2=${TINT2_BUTTON_PANEL_X2}\n" \ 42 | "TINT2_BUTTON_PANEL_Y1=${TINT2_BUTTON_PANEL_Y1}\n" \ 43 | "TINT2_BUTTON_PANEL_Y2=${TINT2_BUTTON_PANEL_Y2}\n" \ 44 | | "${JGMENU_EXEC_DIR}/jgmenu-socket" 45 | } 46 | 47 | remove_lockfile () { 48 | rm -f "${JGMENU_LOCKFILE}" 49 | printf "warn: %b\n" "found old lockfile; lockfile removed" 50 | } 51 | 52 | # "jgmenu_run" with no arguments specified 53 | if test $# -lt 1 54 | then 55 | # A primary objective here is to keep 'awake' quick 56 | # Checking if the lockfile exists is much quicker than 57 | # 'pgrep -x jgmenu' (30ms on my machine) or similar. 58 | if test -e ${JGMENU_LOCKFILE} 59 | then 60 | send_tint2_env_vars_to_jgmenu 61 | if killall -SIGUSR1 jgmenu >/dev/null 2>&1 62 | then 63 | exit 0 64 | else 65 | # We should not get to here unless 66 | # - there has been a crash (e.g. ASAN related) 67 | # - lockfile was left from a jgmenu compiled before 68 | # commit e3cb35 when we started to handle SIGTERM 69 | # and SIGINT 70 | remove_lockfile 71 | fi 72 | fi 73 | 74 | exec jgmenu --at-pointer 75 | exit 0 76 | fi 77 | 78 | test "$1" = "--help" && usage 79 | test "$1" = "--exec-path" && print_exec_path 80 | 81 | cmd="$1" 82 | cmds="jgmenu-${cmd} jgmenu-${cmd}.sh jgmenu-${cmd}.py" 83 | shift 84 | 85 | for c in ${cmds} 86 | do 87 | if type "${c}" >/dev/null 2>&1 88 | then 89 | exec "${c}" "$@" 90 | exit 0 91 | fi 92 | done 93 | 94 | die "'${cmd}' is not a jgmenu_run command" 95 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Worm - Dynamic window manager 9 | 10 | 11 | 78 | 79 | 80 |
81 | 84 | 85 |

Worm

86 |

87 | Worm is a simple, yet functional, dynamic window manager for the X11 88 | platform. It has first-class support for both floating and master-stack 89 | tiling. In addition, Worm has rudimentary support for tags such as 90 | AwesomeWM or DWM. Worm is written in Nim. 91 |

92 | 93 | 94 | 99 |
100 | 101 | 102 | -------------------------------------------------------------------------------- /src/events/buttonpress.nim: -------------------------------------------------------------------------------- 1 | import x11/[xlib, x, xft] 2 | import ../wm, ../log, ../types 3 | import std/[os, options, osproc, strutils] 4 | 5 | proc handleButtonPress*(self: var Wm; ev: XButtonEvent): void = 6 | if ev.subwindow == None and ev.window == self.root and ev.button == 3: 7 | log "Root menu triggered (right click on root window). Attempting to launch root menu" 8 | if self.config.rootMenu != "" and fileExists expandTilde self.config.rootMenu: 9 | discard startProcess expandTilde self.config.rootMenu 10 | discard self.dpy.XAllowEvents(ReplayPointer, ev.time) 11 | return 12 | #if ev.subwindow == None or ev.window == self.root: return 13 | var 14 | close = false 15 | maximize = false 16 | minimize = false 17 | var clientOpt = self.findClient do (client: Client) -> bool: 18 | ( 19 | if ev.window == client.frame.close: 20 | close = true 21 | close 22 | elif ev.window == client.frame.maximize: 23 | maximize = true 24 | maximize 25 | elif ev.window == client.frame.minimize: 26 | minimize = true 27 | minimize 28 | else: 29 | false) or client.frame.window == ev.subwindow or client.frame.title == ev.window 30 | if clientOpt.isNone and ev.button == 1: 31 | clientOpt = self.findClient do (client: Client) -> bool: client.window == ev.window 32 | discard self.dpy.XAllowEvents(ReplayPointer, ev.time) 33 | if clientOpt.isNone: return 34 | let client = clientOpt.get[0] 35 | var 36 | quitClose = false 37 | quitMaximize = false 38 | quitMinimize = false 39 | if close: 40 | # check if closable 41 | if self.config.frameParts.left.find(fpClose) == -1 and 42 | self.config.frameParts.center.find(fpClose) == -1 and 43 | self.config.frameParts.right.find(fpClose) == -1: 44 | quitClose = false 45 | else: 46 | let cm = XEvent(xclient: XClientMessageEvent(format: 32, 47 | theType: ClientMessage, serial: 0, sendEvent: true, display: self.dpy, 48 | window: client.window, messageType: self.dpy.XInternAtom( 49 | "WM_PROTOCOLS", false), 50 | data: XClientMessageData(l: [clong self.dpy.XInternAtom( 51 | "WM_DELETE_WINDOW", false), CurrentTime, 0, 0, 0]))) 52 | discard self.dpy.XSendEvent(client.window, false, NoEventMask, cast[ 53 | ptr XEvent](unsafeAddr cm)) 54 | quitClose = true 55 | if quitClose: return 56 | if maximize: 57 | # check if closable 58 | if self.config.frameParts.left.find(fpMaximize) == -1 and 59 | self.config.frameParts.center.find(fpMaximize) == -1 and 60 | self.config.frameParts.right.find(fpMaximize) == -1: 61 | quitMaximize = false 62 | else: 63 | self.maximizeClient client[] 64 | quitMaximize = true 65 | if quitMaximize: return 66 | # Workaround for https://github.com/codic12/worm/issues/62 67 | if client.window != ev.window and client.frame.title != ev.window: 68 | discard self.dpy.XGrabPointer(client.frame.window, true, PointerMotionMask or 69 | ButtonReleaseMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime) 70 | if minimize: 71 | # check if closable 72 | if self.config.frameParts.left.find(fpMinimize) == -1 and 73 | self.config.frameParts.center.find(fpMinimize) == -1 and 74 | self.config.frameParts.right.find(fpMinimize) == -1: 75 | quitMinimize = false 76 | else: 77 | self.minimizeClient client[] 78 | quitMinimize = true 79 | if quitMaximize: return 80 | var attr: XWindowAttributes 81 | discard self.dpy.XGetWindowAttributes(client.frame.window, addr attr) 82 | self.motionInfo = some MotionInfo(start: ev, attr: attr) 83 | discard self.dpy.XSetInputFocus(client.window, RevertToPointerRoot, CurrentTime) 84 | for window in [client.frame.window, client.window]: discard self.dpy.XRaiseWindow window 85 | self.focused = some clientOpt.get[1] 86 | discard self.dpy.XSetWindowBorder(self.clients[self.focused.get].frame.window, 87 | self.config.borderActivePixel) 88 | for win in [ 89 | self.clients[self.focused.get].frame.window, 90 | self.clients[self.focused.get].frame.top, 91 | self.clients[self.focused.get].frame.title, 92 | self.clients[self.focused.get].frame.close, 93 | self.clients[self.focused.get].frame.maximize 94 | ]: 95 | discard self.dpy.XSetWindowBackground(win, self.config.frameActivePixel) 96 | self.renderTop self.clients[self.focused.get] 97 | discard self.dpy.XSync false 98 | discard self.dpy.XFlush 99 | var fattr: XWindowAttributes 100 | discard self.dpy.XGetWindowAttributes(self.clients[self.focused.get].window, addr fattr) 101 | var color: XftColor 102 | discard self.dpy.XftColorAllocName(fattr.visual, fattr.colormap, cstring( 103 | "#" & self.config.textActivePixel.toHex 6), addr color) 104 | self.clients[self.focused.get].color = color 105 | self.renderTop self.clients[self.focused.get] 106 | for i, client in self.clients.mpairs: 107 | if self.focused.get.int == i: continue 108 | discard self.dpy.XSetWindowBorder(client.frame.window, 109 | self.config.borderInactivePixel) 110 | for window in [client.frame.top,client.frame.title,client.frame.window,client.frame.close,client.frame.maximize]: 111 | discard self.dpy.XSetWindowBackground(window, self.config.frameInactivePixel) 112 | var attr: XWindowAttributes 113 | discard self.dpy.XGetWindowAttributes(client.window, addr attr) 114 | var color: XftColor 115 | discard self.dpy.XftColorAllocName(attr.visual, attr.colormap, cstring( 116 | "#" & self.config.textInactivePixel.toHex 6), addr color) 117 | client.color = color 118 | self.renderTop client 119 | discard self.dpy.XSync false 120 | discard self.dpy.XFlush 121 | -------------------------------------------------------------------------------- /src/atoms.nim: -------------------------------------------------------------------------------- 1 | import 2 | x11/[xlib, x ] 3 | 4 | converter toXBool(x: bool): XBool = x.XBool 5 | # converter toBool(x: XBool): bool = x.bool 6 | 7 | type 8 | NetAtom* = enum 9 | NetActiveWindow = "_NET_ACTIVE_WINDOW", 10 | NetSupported = "_NET_SUPPORTED", 11 | NetSystemTray = "_NET_SYSTEM_TRAY_S0", 12 | NetSystemTrayOP = "_NET_SYSTEM_TRAY_OPCODE", 13 | NetSystemTrayOrientation = "_NET_SYSTEM_TRAY_ORIENTATION", 14 | NetSystemTrayOrientationHorz = "_NET_SYSTEM_TRAY_ORIENTATION_HORZ", 15 | NetWMName = "_NET_WM_NAME", 16 | NetWMState = "_NET_WM_STATE", 17 | NetWMStateAbove = "_NET_WM_STATE_ABOVE", 18 | NetWMStateMaximizedVert = "_NET_WM_STATE_MAXIMIZED_VERT", 19 | NetWMStateMaximizedHorz = "_NET_WM_STATE_MAXIMIZED_HORZ", 20 | NetWMStateSticky = "_NET_WM_STATE_STICKY", 21 | NetWMStateModal = "_NET_WM_STATE_MODAL", 22 | NetSupportingWMCheck = "_NET_SUPPORTING_WM_CHECK", 23 | NetWMStateFullScreen = "_NET_WM_STATE_FULLSCREEN", 24 | NetClientList = "_NET_CLIENT_LIST", 25 | NetClientListStacking = "_NET_CLIENT_LIST_STACKING", 26 | NetWMStrutPartial = "_NET_WM_STRUT_PARTIAL", 27 | NetWMWindowType = "_NET_WM_WINDOW_TYPE", 28 | NetWMWindowTypeNormal = "_NET_WM_WINDOW_TYPE_NORMAL", 29 | NetWMWindowTypeDialog = "_NET_WM_WINDOW_TYPE_DIALOG", 30 | NetWMWindowTypeUtility = "_NET_WM_WINDOW_TYPE_UTILITY", 31 | NetWMWindowTypeToolbar = "_NET_WM_WINDOW_TYPE_TOOLBAR", 32 | NetWMWindowTypeSplash = "_NET_WM_WINDOW_TYPE_SPLASH", 33 | NetWMWindowTypeMenu = "_NET_WM_WINDOW_TYPE_MENU", 34 | NetWMWindowTypeDropdownMenu = "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", 35 | NetWMWindowTypePopupMenu = "_NET_WM_WINDOW_TYPE_POPUP_MENU", 36 | NetWMWindowTypeTooltip = "_NET_WM_WINDOW_TYPE_TOOLTIP", 37 | NetWMWindowTypeNotification = "_NET_WM_WINDOW_TYPE_NOTIFICATION", 38 | NetWMWindowTypeDock = "_NET_WM_WINDOW_TYPE_DOCK", 39 | NetWMWindowTypeDesktop = "_NET_WM_WINDOW_TYPE_DESKTOP", 40 | NetWMDesktop = "_NET_WM_DESKTOP", 41 | NetDesktopViewport = "_NET_DESKTOP_VIEWPORT", 42 | NetNumberOfDesktops = "_NET_NUMBER_OF_DESKTOPS", 43 | NetCurrentDesktop = "_NET_CURRENT_DESKTOP", 44 | NetDesktopNames = "_NET_DESKTOP_NAMES", 45 | NetFrameExtents = "_NET_FRAME_EXTENTS" 46 | 47 | IpcAtom* = enum 48 | IpcClientMessage = "WORM_IPC_CLIENT_MESSAGE", 49 | IpcBorderActivePixel = "WORM_IPC_BORDER_ACTIVE_PIXEL", 50 | IpcBorderInactivePixel = "WORM_IPC_BORDER_INACTIVE_PIXEL", 51 | IpcBorderWidth = "WORM_IPC_BORDER_WIDTH", 52 | IpcFrameActivePixel = "WORM_IPC_FRAME_ACTIVE_PIXEL", 53 | IpcFrameInactivePixel = "WORM_IPC_FRAME_INACTIVE_PIXEL", 54 | IpcFrameHeight = "WORM_IPC_FRAME_HEIGHT", 55 | IpcTextActivePixel = "WORM_IPC_TEXT_ACTIVE_PIXEL", 56 | IpcTextInactivePixel = "WORM_IPC_TEXT_INACTIVE_PIXEL", 57 | IpcTextFont = "WORM_IPC_TEXT_FONT", 58 | IpcTextOffset = "WORM_IPC_TEXT_OFFSET", 59 | IpcKillClient = "WORM_IPC_KILL_CLIENT", 60 | IpcCloseClient = "WORM_IPC_CLOSE_CLIENT", 61 | IpcSwitchTag = "WORM_IPC_SWITCH_TAG", 62 | IpcAddTag = "WORM_IPC_ADD_TAG", 63 | IpcRemoveTag = "WORM_IPC_REMOVE_TAG" 64 | IpcLayout = "WORM_IPC_LAYOUT", 65 | IpcGaps = "WORM_IPC_GAPS", 66 | IpcMaster = "WORM_IPC_MASTER", 67 | IpcStruts = "WORM_IPC_STRUTS", 68 | IpcMoveTag = "WORM_IPC_MOVE_TAG", 69 | IpcFrameLeft = "WORM_IPC_FRAME_LEFT", 70 | IpcFrameCenter = "WORM_IPC_FRAME_CENTER", 71 | IpcFrameRight = "WORM_IPC_FRAME_RIGHT", 72 | IpcFloat = "WORM_IPC_FLOAT", 73 | IpcButtonOffset = "WORM_IPC_BUTTON_OFFSET", 74 | IpcButtonSize = "WORM_IPC_BUTTON_SIZE", 75 | IpcRootMenu = "WORM_IPC_ROOT_MENU", 76 | IpcCloseActivePath = "WORM_IPC_CLOSE_ACTIVE_PATH", 77 | IpcCloseInactivePath = "WORM_IPC_CLOSE_INACTIVE_PATH", 78 | IpcCloseActiveHoveredPath = "WORM_IPC_CLOSE_ACTIVE_HOVERED_PATH" 79 | IpcCloseInactiveHoveredPath = "WORM_IPC_CLOSE_INACTIVE_HOVERED_PATH", 80 | IpcClosePath = "WORM_IPC_CLOSE_PATH", 81 | IpcMaximizeActivePath = "WORM_IPC_MAXIMIZE_ACTIVE_PATH", 82 | IpcMaximizeInactivePath = "WORM_IPC_MAXIMIZE_INACTIVE_PATH", 83 | IpcMaximizeActiveHoveredPath = "WORM_IPC_MAXIMIZE_ACTIVE_HOVERED_PATH", 84 | IpcMaximizeInactiveHoveredPath = "WORM_IPC_MAXIMIZE_INACTIVE_HOVERED_PATH", 85 | IpcMaximizePath = "WORM_IPC_MAXIMIZE_PATH", 86 | IpcMinimizeActivePath = "WORM_IPC_MINIMIZE_ACTIVE_PATH", 87 | IpcMinimizeInactivePath = "WORM_IPC_MINIMIZE_INACTIVE_PATH", 88 | IpcMinimizeActiveHoveredPath = "WORM_IPC_MINIMIZE_ACTIVE_HOVERED_PATH", 89 | IpcMinimizeInactiveHoveredPath = "WORM_IPC_MINIMIZE_INACTIVE_HOVERED_PATH", 90 | IpcMinimizePath = "WORM_IPC_MINIMIZE_PATH", 91 | IpcMaximizeClient = "WORM_IPC_MAXIMIZE_CLIENT", 92 | IpcMinimizeClient = "WORM_IPC_MINIMIZE_CLIENT", 93 | IpcDecorationDisable = "WORM_IPC_DECORATION_DISABLE", 94 | IpcFocusMode = "WORM_IPC_FOCUS_MODE", 95 | IpcModifier = "WORM_IPC_MODIFIER" 96 | 97 | func getNetAtoms*(dpy: ptr Display): array[NetAtom, Atom] = 98 | for atom in NetAtom: 99 | result[atom] = dpy.XInternAtom(($atom).cstring, false) 100 | 101 | func getIpcAtoms*(dpy: ptr Display): array[IpcAtom, Atom] = 102 | for atom in IpcAtom: 103 | result[atom] = dpy.XInternAtom(($atom).cstring, false) 104 | -------------------------------------------------------------------------------- /docs/wormc.md: -------------------------------------------------------------------------------- 1 | # Documentation for `wormc` 2 | 3 | `wormc` provides the primary way to interact with the Worm window manager's inter-process communication system. It also serves as a reference point for people wanting to make their own IPC clients for Worm. 4 | 5 | Here's the basic format that we use for wormc: 6 | ``` 7 | $ wormc command1 parameters... command2 parameters... 8 | ``` 9 | A command can have any number of parameters. In theory, all these parameters are not run through the parser, but in practice they currently are; this is an implementation detail which is rather an edge case but still will be fixed soon. 10 | 11 | Numbers are always in decimal. Always. There are no exceptions to this rule; colors are also in plain decimal. Eg, this is invalid: 12 | ``` 13 | $ wormc frame-pixel #ff00ffff 14 | ``` 15 | Instead, you could use on posix shells `$((16#ff00ffff))` and have the shell expand it for you. For readability purposes, this is recommended and will be used in this sheet for examples; in fish, this is `(math ff00ffff)`. 16 | 17 | Before we begin, finally, a note on X11 colors: While the 3 byte format RRGGBB works in most cases, it's been observed that some compositors assume the 4 byte format AARRGGBB. As a result, we use the AARRGGBB format in the documentation and for all default values and it's recommended you do the same. If you're not using a compositor it's not likely to be an issue, however. 18 | 19 | Here is the full list of commands: 20 |
21 | ### `border-active-pixel(uint)` 22 | Sets the border color for the currently active client. Ex: `wormc border-active-pixel $((16#ff00ffff))` 23 | ### `border-inactive-pixel(uint)` 24 | Sets the border color for all inactive clients. Ex: `wormc border-inactive-pixel $((16#ff000000))` 25 | ### `border-width(uint)` 26 | Sets the border width for all clients, active or not. Ex: `wormc border-width 5` 27 | ### `frame-active-pixel(uint)` 28 | Sets the color of the frame (titlebar) for the active window. Ex: `wormc frame-active-pixel $((16#ff123456))` 29 | ### `frame-inactive-pixel(uint)` 30 | Sets the color of the frame (titlebar) for all windows that are inactive. Ex: `wormc frame-inactive-pixel $((16#ff222222))` 31 | ### `frame-height(uint)` 32 | Sets the height of the frame / titlebar for all clients, active or not. Ex: `wormc frame-height 20` 33 | ### `text-active-pixel(uint)` 34 | Sets the color of the text drawn on the titlebar / frame for active windows. Ex: `wormc text-pixel $((16#ffffffff))` 35 | ### `text-inactive-pixel(uint)` 36 | Sets the color of the text drawn on the titlebar / frame for inactive windows. Ex: `wormc text-pixel $((16#ff000000))` 37 | ### `gaps(uint)` 38 | Sets the gaps to the specified amount. When in tiling mode, this distance is reserved between the inside parts of windows. See struts for the outside. Ex: `wormc gaps 5` 39 | ### `text-font(string)` 40 | Set the font of text in client titlebars. The provided string must be in valid XFT format. While proper documentation can be found elsewhere, you can do `FontName` or `FontName:size=N`, which covers most use cases; but Xft allows doing much more, like disabling anti-aliasing. Ex `wormc text-font Terminus:size=8` 41 | ### `text-offset(uint x, uint y)` 42 | Specifies the offset of text in the titlebar. By default text is positioned at (0,0) which makes it invisible. The Y value needs to be set to something higher, based on the font size. In the future this Y offset will of course be auto-calculated. Ex `wormc text-offset 10 15` 43 | ### `kill-client(window id)` 44 | Kills the client with the provided window ID forcefully using XKillClient. Ex `wormc kill-client 1234567890` 45 | ### `kill-active-client()` 46 | Same as kill-client, but kills the focused window. Eg `wormc kill-active-client` 47 | ### `close-client(window ID)` 48 | Nicely sends a WM_DELETE_WINDOW message from ICCCM to the provided window ID. For example, `wormc close-client 1234567890` 49 | ### `close-active-client()` 50 | Same as close-client, but closes the focused window. Eg, `wormc close-active-client` 51 | ### `switch-tag(uint in 1-9)` 52 | Clears all other tags and enables given tag. Example: `wormc switch-tag 5` 53 | ### `layout(string)` 54 | Changes layout. `floating` for floating, `tiling` for tiling, otherwise wormc exits. Eg `wormc layout tiling` 55 | ### `struts(uint top, uint bottom, uint left, uint right)` 56 | Sets the struts, also known as the 'outer margins. These are used when maximizing windows (currently unimplemented, sorry!) and while tiling. Example: `wormc struts 10 50 10 10`. 57 | ### `move-tag(uint tag, window id)` 58 | Clears the tags of the provided window and turns on the given tag; for example `wormc move-tag 5 123456789`. 59 | ### `move-active-tag(uint tag)` 60 | Like move-tag, but uses the focused window. Eg `wormc move-active-tag 5` 61 | ### `master(window id)` 62 | In a tiling layout, sets the master of the current workspace to the given window ID. Ex `wormc master 123456789` 63 | ### `master-active()` 64 | Like `master`, but uses the active client. Example: `wormc master-active` 65 | ### `float(window id)` 66 | Change the mode of the client indicated by the provided window ID to floating. In a tiling layout, this would indicate that the provided window should not be tiled. TODO: have a way to reverse this affect. As an example: `wormc float 1234567890` 67 | ### `float-active()` 68 | Like float, but floats the active client; eg `wormc float-active`. 69 | ### `maximize-client(window id)` 70 | Maximize the given window, eg `wormc maximize-client 132123` 71 | ### `maximize-active-client()` 72 | `maximize-client`'s equivalent applied on the currently focused/raised/active window. Eg `wormc maximize-active-client`. 73 | ### `frame-left(string)` 74 | Describes the layout of the frame *on the left side*. This is a comma separated list of *parts*. The parts can be any of: 75 | - T for window title 76 | - C for close button 77 | - M for maximize button 78 | - I for iconify/minimize button 79 | eg: `wormc frame-left 'T;C;M'` 80 | ### `frame-center(string)` / `frame-right(string)` 81 | same as frame-left, but for the center and right parts of a frame window. 82 | ### `button-size(int)` 83 | The size (both width and height) of all window buttons. The window buttons don't have to nessecarily be perfect squares. You can use the larger dimension and the rest of the window will just not render. Eg: `wormc button-size 14` 84 | ### `button-offset(x, y)` 85 | The offset at both the x and y positions at which buttons on the titlebar (M, C) are located, for example `wormc button-offset 10 10`. 86 | ### `root-menu(string)` 87 | Sets path to the root menu. If this file is valid, upon right-clicking the root window it's executed (assumed to be an executable file). Eg `wormc root-menu ~/worm/examples/jgmenu_run`. 88 | ### `decoration-disable(string)` 89 | Disable decorations for all windows which have a class that matches the regex given in `string`. For an example: `wormc decoration-disable '(?i).*firefox.*'`. 90 | Note: The regex format we use is described at [the nim-regex docs page](https://nitely.github.io/nim-regex/regex.html) 91 | 92 | ### `{close,minimize,maximize}-path (string)` 93 | Sets X-active-path, X-inactive-path, X-active-hovered-path, AND X-inactive-hovered-path where X is close, minimize, or maximize. Is an alias for compatibility and convinence with older worm versions. 94 | ### `{close,minimize,maximize}-active-path (string)` 95 | Sets the path for the appropriate button for active windows only, when not being hovered. 96 | ### `{close,minimize,maximize}-active-hovered-path (string)` 97 | Sets the path for the appropriate button for active windows only, when being hovered over. 98 | ### `{close,minimize,maximize}-inactive-path (string)` 99 | Sets the path for the appropriate button for inactive windows only, when not being hovered. 100 | ### `{close,minimize,maximize}-inactive-hovered-path (string)` 101 | Sets the path for the appropriate button for inactive windows only, when being hovered over. 102 | ### `modifier(uint32)` 103 | Takes a modifier (defined in, eg, X11/X.h) and sets it as the default for moving/resizing windows. Examples: 104 | 1 << 3 = Mod1Mask = Alt = 8 105 | 1 << 6 = Mod4Mask = Super/Mod/Win = 64 106 | ### `focus-mode(int)` 107 | Sets the focus mode. 108 | 1: is focus-follows-click (like Microsoft Windows). 109 | 2: is the traditional X11 focus-follows-mouse. 110 | -------------------------------------------------------------------------------- /src/wormc.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/[strutils, os], 3 | x11/[x, xlib, xutil], 4 | atoms 5 | 6 | converter toXBool(x: bool): XBool = x.XBool 7 | converter toBool(x: XBool): bool = x.bool 8 | 9 | type Layout = enum 10 | lyFloating, lyTiling 11 | 12 | proc formatMess(a: Atom, params: varargs[string, `$`]): array[5, clong] = 13 | result[0] = a.clong 14 | 15 | for i in 1 ..< result.len: 16 | if i - 1 < params.len: 17 | result[i] = params[i - 1].parseInt().clong 18 | 19 | proc sendStrPrep(dpy: PDisplay, a: Atom, param: string) = 20 | var 21 | fontList = param.cstring 22 | fontProp: XTextProperty 23 | discard dpy.XUtf8TextListToTextProperty( 24 | addr fontList, 25 | 1, 26 | XUTF8StringStyle, 27 | addr fontProp 28 | ) 29 | 30 | dpy.XSetTextProperty( 31 | dpy.XDefaultRootWindow, 32 | addr fontProp, 33 | a 34 | ) 35 | discard XFree fontProp.value 36 | 37 | proc getLayoutOrd(s: string): int = 38 | if s == "floating": 39 | result = lyFloating.ord 40 | elif s == "tiling": 41 | result = lyTiling.ord 42 | else: 43 | quit 1 44 | 45 | proc main() = 46 | let dpy = XOpenDisplay nil 47 | 48 | if dpy == nil: 49 | return 50 | 51 | let 52 | ipcAtoms = dpy.getIpcAtoms 53 | root = dpy.XDefaultRootWindow 54 | params = commandLineParams() 55 | 56 | for i, param in commandLineParams(): 57 | var data: array[5, clong] 58 | case param: 59 | of "border-active-pixel": 60 | data = ipcAtoms[IpcBorderActivePixel].formatMess(params[i+1]) 61 | of "border-inactive-pixel": 62 | data = ipcAtoms[IpcBorderInactivePixel].formatMess(params[i+1]) 63 | of "border-width": 64 | data = ipcAtoms[IpcBorderWidth].formatMess(params[i+1]) 65 | of "frame-active-pixel": 66 | data = ipcAtoms[IpcFrameActivePixel].formatMess(params[i+1]) 67 | of "frame-inactive-pixel": 68 | data = ipcAtoms[IpcFrameInactivePixel].formatMess(params[i+1]) 69 | of "frame-height": 70 | data = ipcAtoms[IpcFrameHeight].formatMess(params[i+1]) 71 | of "text-active-pixel": 72 | data = ipcAtoms[IpcTextActivePixel].formatMess(params[i+1]) 73 | of "text-inactive-pixel": 74 | data = ipcAtoms[IpcTextInactivePixel].formatMess(params[i+1]) 75 | of "gaps": 76 | data = ipcAtoms[IpcGaps].formatMess(params[i+1]) 77 | 78 | # This is not as simple as sending a ClientMessage to the root window, 79 | # because a string is involved; therefore, we must first do a bit of 80 | # preparation and then send a data-less msg. 81 | of "text-font": 82 | dpy.sendStrPrep(ipcAtoms[IpcTextFont], params[i+1]) 83 | data = ipcAtoms[IpcTextFont].formatMess() 84 | of "frame-left": 85 | dpy.sendStrPrep(ipcAtoms[IpcFrameLeft], params[i+1]) 86 | data = ipcAtoms[IpcFrameLeft].formatMess() 87 | of "frame-center": 88 | dpy.sendStrPrep(ipcAtoms[IpcFrameCenter], params[i+1]) 89 | data = ipcAtoms[IpcFrameCenter].formatMess() 90 | of "frame-right": 91 | dpy.sendStrPrep(ipcAtoms[IpcFrameRight], params[i+1]) 92 | data = ipcAtoms[IpcFrameRight].formatMess() 93 | of "root-menu": 94 | dpy.sendStrPrep(ipcAtoms[IpcRootMenu], params[i+1]) 95 | data = formatMess ipcAtoms[IpcRootMenu] 96 | of "close-active-path": 97 | dpy.sendStrPrep(ipcAtoms[IpcCloseActivePath], params[i+1]) 98 | data = ipcAtoms[IpcCloseActivePath].formatMess() 99 | of "close-inactive-path": 100 | dpy.sendStrPrep(ipcAtoms[IpcCloseInactivePath], params[i+1]) 101 | data = ipcAtoms[IpcCloseInactivePath].formatMess() 102 | of "close-active-hovered-path": 103 | dpy.sendStrPrep(ipcAtoms[IpcCloseActiveHoveredPath], params[i+1]) 104 | data = ipcAtoms[IpcCloseActiveHoveredPath].formatMess() 105 | of "close-inactive-hovered-path": 106 | dpy.sendStrPrep(ipcAtoms[IpcCloseInactiveHoveredPath], params[i+1]) 107 | data = ipcAtoms[IpcCloseInactiveHoveredPath].formatMess() 108 | of "close-path": 109 | dpy.sendStrPrep(ipcAtoms[IpcClosePath], params[i+1]) 110 | data = ipcAtoms[IpcClosePath].formatMess() 111 | of "maximize-active-path": 112 | dpy.sendStrPrep(ipcAtoms[IpcMaximizeActivePath], params[i+1]) 113 | data = ipcAtoms[IpcMaximizeActivePath].formatMess() 114 | of "maximize-inactive-path": 115 | dpy.sendStrPrep(ipcAtoms[IpcMaximizeInactivePath], params[i+1]) 116 | data = ipcAtoms[IpcMaximizeInactivePath].formatMess() 117 | of "maximize-active-hovered-path": 118 | dpy.sendStrPrep(ipcAtoms[IpcMaximizeActiveHoveredPath], params[i+1]) 119 | data = ipcAtoms[IpcMaximizeActiveHoveredPath].formatMess() 120 | of "maximize-inactive-hovered-path": 121 | dpy.sendStrPrep(ipcAtoms[IpcMaximizeInactiveHoveredPath], params[i+1]) 122 | data = ipcAtoms[IpcMaximizeInactiveHoveredPath].formatMess() 123 | of "maximize-path": 124 | dpy.sendStrPrep(ipcAtoms[IpcMaximizePath], params[i+1]) 125 | data = ipcAtoms[IpcMaximizePath].formatMess() 126 | of "minimize-active-path": 127 | dpy.sendStrPrep(ipcAtoms[IpcMinimizeActivePath], params[i+1]) 128 | data = ipcAtoms[IpcMinimizeActivePath].formatMess() 129 | of "minimize-inactive-path": 130 | dpy.sendStrPrep(ipcAtoms[IpcMinimizeInactivePath], params[i+1]) 131 | data = ipcAtoms[IpcMinimizeInactivePath].formatMess() 132 | of "minimize-active-hovered-path": 133 | dpy.sendStrPrep(ipcAtoms[IpcMinimizeActiveHoveredPath], params[i+1]) 134 | data = ipcAtoms[IpcMinimizeActiveHoveredPath].formatMess() 135 | of "minimize-inactive-hovered-path": 136 | dpy.sendStrPrep(ipcAtoms[IpcMinimizeInactiveHoveredPath], params[i+1]) 137 | data = ipcAtoms[IpcMinimizeInactiveHoveredPath].formatMess() 138 | of "minimize-path": 139 | dpy.sendStrPrep(ipcAtoms[IpcMinimizePath], params[i+1]) 140 | data = ipcAtoms[IpcMinimizePath].formatMess() 141 | of "decoration-disable": 142 | dpy.sendStrPrep(ipcAtoms[IpcDecorationDisable], params[i+1]) 143 | data = ipcAtoms[IpcDecorationDisable].formatMess() 144 | of "text-offset": 145 | data = ipcAtoms[IpcTextOffset].formatMess(params[i+1], params[i+2]) 146 | of "kill-client": 147 | data = ipcAtoms[IpcKillClient].formatMess(params[i+1]) 148 | of "kill-active-client": 149 | data = ipcAtoms[IpcKillClient].formatMess() 150 | of "close-client": 151 | data = ipcAtoms[IpcCloseClient].formatMess(params[i+1]) 152 | of "close-active-client": 153 | data = ipcAtoms[IpcCloseClient].formatMess() 154 | of "switch-tag": 155 | data = ipcAtoms[IpcSwitchTag].formatMess(params[i+1]) 156 | of "layout": 157 | data = ipcAtoms[IpcLayout].formatMess(getLayoutOrd params[i+1]) 158 | of "struts": 159 | data = ipcAtoms[IpcStruts].formatMess( 160 | params[i+1], params[i+2], params[i+3], params[i+4] 161 | ) 162 | of "move-tag": 163 | data = ipcAtoms[IpcMoveTag].formatMess(params[i+1], params[i+2]) 164 | of "move-active-tag": 165 | data = ipcAtoms[IpcMoveTag].formatMess(params[i+1]) 166 | of "master": 167 | data = ipcAtoms[IpcMaster].formatMess(params[i+1]) 168 | of "master-active": 169 | data = ipcAtoms[IpcMaster].formatMess() 170 | of "float": 171 | data = ipcAtoms[IpcFloat].formatMess(params[i+1]) 172 | of "float-active": 173 | data = ipcAtoms[IpcFloat].formatMess() 174 | of "button-offset": 175 | data = ipcAtoms[IpcButtonOffset].formatMess(params[i+1], params[i+2]) 176 | of "button-size": 177 | data = ipcAtoms[IpcButtonSize].formatMess(params[i+1]) 178 | of "maximize-client": 179 | data = ipcAtoms[IpcMaximizeClient].formatMess(params[i+1]) 180 | of "maximize-active-client": 181 | data = ipcAtoms[IpcMaximizeClient].formatMess() 182 | of "minimize-client": 183 | data = ipcAtoms[IpcMinimizeClient].formatMess(params[i+1]) 184 | of "minimize-active-client": 185 | data = ipcAtoms[IpcMinimizeClient].formatMess() 186 | of "modifier": 187 | data = ipcAtoms[IpcModifier].formatMess(params[i+1]) 188 | of "focus-mode": 189 | data = ipcAtoms[IpcFocusMode].formatMess(params[i+1]) 190 | 191 | else: discard 192 | 193 | let event = XEvent( 194 | xclient: XClientMessageEvent( 195 | format: 32, 196 | theType: ClientMessage, 197 | serial: 0, 198 | sendEvent: true, 199 | display: dpy, 200 | window: root, 201 | messageType: ipcAtoms[IpcClientMessage], 202 | data: XClientMessageData(l: data) 203 | ) 204 | ) 205 | discard dpy.XSendEvent( 206 | root, 207 | false, 208 | SubstructureNotifyMask, 209 | cast[ptr XEvent](unsafeAddr event) 210 | ) 211 | 212 | discard dpy.XFlush 213 | discard dpy.XSync false 214 | 215 | when isMainModule: 216 | main() 217 | -------------------------------------------------------------------------------- /src/events/maprequest.nim: -------------------------------------------------------------------------------- 1 | import ../wm, ../types, ../atoms, ../log 2 | import std/[options, strutils] 3 | import x11/[x, xlib, xft, xatom, xutil] 4 | import regex 5 | 6 | func getProperty[T]( 7 | dpy: ptr Display; 8 | window: Window; 9 | property: Atom 10 | ): Option[T] = 11 | # TODO: proper names when I'm less lazy and can write it all out 12 | var 13 | a: Atom 14 | b: cint 15 | c: culong 16 | d: culong 17 | e: ptr T 18 | discard XGetWindowProperty( 19 | dpy, 20 | window, 21 | property, 22 | 0, 23 | high clong, 24 | false, 25 | AnyPropertyType, 26 | addr a, 27 | addr b, 28 | addr c, 29 | addr d, 30 | cast[ptr ptr char](addr e) 31 | ) 32 | if c > 0: return some e[] else: result.reset 33 | 34 | proc handleMapRequest*(self: var Wm; ev: XMapRequestEvent): void = 35 | var attr: XWindowAttributes 36 | discard self.dpy.XGetWindowAttributes(ev.window, addr attr) 37 | if attr.overrideRedirect: return 38 | let wintype = getProperty[Atom](self.dpy, ev.window, self.netAtoms[ 39 | NetWMWindowType]) 40 | type Hints = object 41 | flags, functions, decorations: culong 42 | inputMode: clong 43 | status: culong 44 | if wintype.isSome and wintype.get in [self.netAtoms[ 45 | NetWMWindowTypeDock], self.netAtoms[NetWMWindowTypeDropdownMenu], 46 | self.netAtoms[NetWMWindowTypePopupMenu], self.netAtoms[ 47 | NetWMWindowTypeTooltip], self.netAtoms[ 48 | NetWMWindowTypeNotification], self.netAtoms[NetWMWindowTypeDesktop]]: 49 | discard self.dpy.XMapWindow ev.window 50 | discard self.dpy.XLowerWindow ev.window 51 | return # Don't manage irregular windows 52 | let hints = getProperty[Hints](self.dpy, ev.window, self.dpy.XInternAtom( 53 | "_MOTIF_WM_HINTS", false)) 54 | var frameHeight = self.config.frameHeight 55 | var csd = false 56 | if hints.isSome and hints.get.flags == 2 and hints.get.decorations == 0: 57 | frameHeight = 0 58 | csd = true 59 | var max = false 60 | var state = block: 61 | var 62 | typ: Atom 63 | fmt: cint 64 | nitem: culong 65 | baf: culong 66 | props: ptr char 67 | discard self.dpy.XGetWindowProperty(ev.window, self.netAtoms[NetWMState], 0, 68 | high clong, false, AnyPropertyType, addr typ, addr fmt, addr nitem, 69 | addr baf, addr props) 70 | props 71 | if state != nil: 72 | if cast[int](state[]) in [int self.netAtoms[NetWMStateMaximizedHorz], 73 | int self.netAtoms[NetWMStateMaximizedVert]]: 74 | max = true 75 | var chr: XClassHint 76 | discard self.dpy.XGetClassHint(ev.window, addr chr) 77 | block: 78 | for thing in self.noDecorList: 79 | var m: RegexMatch2 80 | log $chr.resClass 81 | log $Regex(thing) 82 | if ($chr.resClass).match thing: 83 | csd = true 84 | frameHeight = 0 85 | var frameAttr = XSetWindowAttributes(backgroundPixel: culong self.config.frameActivePixel, 86 | borderPixel: self.config.borderActivePixel, colormap: attr.colormap) 87 | let frame = self.dpy.XCreateWindow(self.root, attr.x + 88 | self.config.struts.left.cint, attr.y + self.config.struts.top.cint, cuint attr.width, cuint attr.height + 89 | cint frameHeight, 90 | cuint self.config.borderWidth, attr.depth, 91 | InputOutput, 92 | attr.visual, CWBackPixel or CWBorderPixel or CWColormap, addr frameAttr) 93 | discard self.dpy.XSelectInput(frame, ExposureMask or SubstructureNotifyMask or 94 | SubstructureRedirectMask or EnterWindowMask or LeaveWindowMask) 95 | discard self.dpy.XSelectInput(ev.window, PropertyChangeMask) 96 | discard self.dpy.XReparentWindow(ev.window, frame, 0, 97 | cint frameHeight) 98 | # WM_STATE must be set for GTK drag&drop and xprop 99 | # https://github.com/i3/i3/blob/dba30fc9879b42e6b89773c81e1067daa2bb6e23/src/x.c#L1065 100 | let wm_state: uint32 = NormalState 101 | discard self.dpy.XChangeProperty( 102 | ev.window, 103 | self.dpy.XInternAtom("WM_STATE".cstring, false), 104 | XaWindow, 105 | 32, 106 | PropModeReplace, 107 | cast[cstring](unsafeAddr wm_state), 108 | 1 109 | ) 110 | let top = self.dpy.XCreateWindow(frame, 0, 0, 111 | cuint attr.width, cuint frameHeight, 0, attr.depth, 112 | InputOutput, 113 | attr.visual, CWBackPixel or CWBorderPixel or CWColormap, addr frameAttr) 114 | let titleWin = self.dpy.XCreateWindow(top, 0, 0, 115 | cuint attr.width, cuint frameHeight, 0, attr.depth, 116 | InputOutput, 117 | attr.visual, CWBackPixel or CWBorderPixel or CWColormap, addr frameAttr) 118 | let close = self.dpy.XCreateWindow(top, cint attr.width - 119 | self.config.buttonSize.cint, 0, self.config.buttonSize.cuint, cuint frameHeight, 120 | 0, attr.depth, 121 | InputOutput, 122 | attr.visual, CWBackPixel or CWBorderPixel or CWColormap, addr frameAttr) 123 | let maximize = self.dpy.XCreateWindow(top, cint attr.width - 124 | self.config.buttonSize.cint, 0, self.config.buttonSize.cuint, cuint frameHeight, 125 | 0, attr.depth, 126 | InputOutput, 127 | attr.visual, CWBackPixel or CWBorderPixel or CWColormap, addr frameAttr) 128 | let minimize = self.dpy.XCreateWindow(top, cint attr.width - 129 | self.config.buttonSize.cint, 0, self.config.buttonSize.cuint, cuint frameHeight, 130 | 0, attr.depth, 131 | InputOutput, 132 | attr.visual, CWBackPixel or CWBorderPixel or CWColormap, addr frameAttr) 133 | for win in [close, maximize, minimize]: discard self.dpy.XSelectInput(win, EnterWindowMask or LeaveWindowMask) 134 | for window in [frame, ev.window, top, titleWin]: discard self.dpy.XMapWindow window 135 | let draw = self.dpy.XftDrawCreate(titleWin, attr.visual, attr.colormap) 136 | var color: XftColor 137 | discard self.dpy.XftColorAllocName(attr.visual, attr.colormap, cstring("#" & 138 | self.config.textActivePixel.toHex 6), addr color) 139 | var title = block: 140 | var atr: Atom 141 | var afr: cint 142 | var nr: culong 143 | var bar: culong 144 | var prop_return: ptr char 145 | discard self.dpy.XGetWindowProperty(ev.window, self.netAtoms[NetWMName], 146 | 0, high clong, false, self.dpy.XInternAtom("UTF8_STRING", false), 147 | addr atr, addr afr, addr nr, addr bar, addr prop_return) 148 | if prop_return == nil: discard self.dpy.XFetchName(ev.window, cast[ 149 | ptr cstring](addr prop_return)) 150 | cast[cstring](prop_return) 151 | if title == nil: title = "Unnamed Window" # why the heck does this window not have a name?! 152 | for button in [1'u8, 3]: 153 | for mask in [uint32 0, Mod2Mask, LockMask, 154 | Mod3Mask, Mod2Mask or LockMask, 155 | LockMask or Mod3Mask, Mod2Mask or Mod3Mask, 156 | Mod2Mask or LockMask or Mod3Mask]: 157 | discard self.dpy.XGrabButton(button, mask, titleWin, true, 158 | ButtonPressMask or PointerMotionMask or ButtonReleaseMask, GrabModeAsync, GrabModeAsync, None, None) 159 | for mask in [uint32 0, Mod2Mask, LockMask, 160 | Mod3Mask, Mod2Mask or LockMask, 161 | LockMask or Mod3Mask, Mod2Mask or Mod3Mask, 162 | Mod2Mask or LockMask or Mod3Mask]: 163 | for win in [close, maximize, minimize]: discard self.dpy.XGrabButton(1, mask, win, 164 | true, ButtonPressMask or PointerMotionMask or ButtonReleaseMask, GrabModeAsync, GrabModeAsync, 165 | None, None) 166 | for mask in [uint32 0, Mod2Mask, LockMask, 167 | Mod3Mask, Mod2Mask or LockMask, 168 | LockMask or Mod3Mask, Mod2Mask or Mod3Mask, 169 | Mod2Mask or LockMask or Mod3Mask]: 170 | discard self.dpy.XGrabButton(1, mask, ev.window, true, ButtonPressMask, 171 | GrabModeSync, GrabModeSync, None, None) 172 | self.clients.add Client(window: ev.window, frame: Frame(window: frame, 173 | top: top, close: close, maximize: maximize, minimize: minimize, 174 | title: titleWin), draw: draw, color: color, 175 | title: $title, tags: self.tags, floating: self.layout == lyFloating, 176 | frameHeight: frameHeight, csd: csd, class: $chr.resClass, maximized: max) 177 | if max: 178 | self.maximizeClient(self.clients[self.clients.len - 1], true) 179 | self.updateClientList 180 | let extents = [self.config.borderWidth, self.config.borderWidth, 181 | self.config.borderWidth+frameHeight, self.config.borderWidth] 182 | discard self.dpy.XChangeProperty( 183 | ev.window, 184 | self.netAtoms[NetFrameExtents], 185 | XaCardinal, 186 | 32, 187 | PropModeReplace, 188 | cast[cstring](unsafeAddr extents), 189 | 4 190 | ) 191 | for window in [frame, ev.window, top]: discard self.dpy.XRaiseWindow window 192 | discard self.dpy.XSetInputFocus(ev.window, RevertToPointerRoot, CurrentTime) 193 | self.focused = some uint self.clients.len - 1 194 | self.raiseClient self.clients[self.focused.get] 195 | if self.layout == lyTiling: self.tileWindows 196 | while true: 197 | var currEv: XEvent 198 | if self.dpy.XNextEvent(addr currEv) != Success: continue 199 | if currEv.theType == Expose: 200 | self.renderTop self.clients[self.clients.len - 1] 201 | break 202 | for client in self.clients: 203 | for i, tag in client.tags: 204 | if not tag: continue 205 | discard self.dpy.XChangeProperty( 206 | client.window, 207 | self.netAtoms[NetWMDesktop], 208 | XaCardinal, 209 | 32, 210 | PropModeReplace, 211 | cast[cstring](unsafeAddr i), 212 | 1 213 | ) 214 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/events/clientmessage.nim: -------------------------------------------------------------------------------- 1 | import ../wm, ../atoms, ../types, ../log 2 | import x11/[xlib, x, xinerama, xatom, xft, xutil] 3 | import std/[options, strutils] 4 | import regex 5 | 6 | proc handleClientMessage*(self: var Wm; ev: XClientMessageEvent) = 7 | if ev.messageType == self.netAtoms[NetWMState]: 8 | var clientOpt = self.findClient do (client: Client) -> 9 | bool: client.window == ev.window 10 | if clientOpt.isNone: return 11 | var client = clientOpt.get[0] 12 | if ev.format != 32: return # check we can access the union member 13 | if (ev.data.l[1] == int self.netAtoms[NetWMStateFullScreen]) or ( 14 | ev.data.l[2] == int self.netAtoms[NetWMStateFullScreen]): 15 | if ev.data.l[0] == 1 and not client.fullscreen: # Client is asking to be fullscreened 16 | log "Fullscreening client" 17 | var attr: XWindowAttributes 18 | discard self.dpy.XGetWindowAttributes(client.frame.window, addr attr) 19 | client.beforeGeom = some Geometry(x: attr.x, y: attr.y, 20 | width: uint attr.width, height: uint attr.height) 21 | var scrNo: cint 22 | var scrInfo = cast[ptr UncheckedArray[XineramaScreenInfo]]( 23 | self.dpy.XineramaQueryScreens(addr scrNo)) 24 | discard self.dpy.XSetWindowBorderWidth(client.frame.window, 0) 25 | # where the hell is our window at 26 | var x: int 27 | var y: int 28 | var width: uint 29 | var height: uint 30 | if scrno == 1: 31 | # 1st monitor, cuz only one 32 | x = 0 33 | y = 0 34 | width = scrInfo[0].width.uint 35 | height = scrInfo[0].height.uint 36 | else: 37 | var cumulWidth = 0 38 | var cumulHeight = 0 39 | for i in countup(0, scrNo - 1): 40 | cumulWidth += scrInfo[i].width 41 | cumulHeight += scrInfo[i].height 42 | if attr.x <= cumulWidth - attr.width: 43 | x = scrInfo[i].xOrg 44 | y = scrInfo[i].yOrg 45 | width = scrInfo[i].width.uint 46 | height = scrInfo[i].height.uint 47 | discard self.dpy.XMoveResizeWindow( 48 | client.frame.window, cint x, cint y, cuint width, cuint height) 49 | discard self.dpy.XMoveResizeWindow( 50 | client.window, 0, 0, cuint width, cuint height) 51 | for window in [client.window, client.frame.window]: discard self.dpy.XRaiseWindow window 52 | discard self.dpy.XSetInputFocus(client.window, RevertToPointerRoot, CurrentTime) 53 | var arr = [self.netAtoms[NetWMStateFullScreen]] 54 | # change the property 55 | discard self.dpy.XChangeProperty(client.window, self.netAtoms[ 56 | NetWMState], XaAtom, 32, PropModeReplace, cast[cstring]( 57 | addr arr), 1) 58 | client.fullscreen = true 59 | elif ev.data.l[0] == 0 and client.fullscreen: 60 | log "Unfullscreening client" 61 | client.fullscreen = false 62 | discard self.dpy.XMoveResizeWindow(client.frame.window, 63 | cint client.beforeGeom.get.x, cint client.beforeGeom.get.y, 64 | cuint client.beforeGeom.get.width, 65 | cuint client.beforeGeom.get.height) 66 | discard self.dpy.XMoveResizeWindow(client.window, 67 | 0, cint self.config.frameHeight, 68 | cuint client.beforeGeom.get.width, 69 | cuint client.beforeGeom.get.height - self.config.frameHeight) 70 | discard self.dpy.XChangeProperty(client.window, self.netAtoms[ 71 | NetWMState], XaAtom, 32, PropModeReplace, cast[cstring]([]), 0) 72 | self.renderTop client[] 73 | discard self.dpy.XSetWindowBorderWidth(client.frame.window, 74 | cuint self.config.borderWidth) 75 | elif (ev.data.l[1] == int self.netAtoms[NetWMStateMaximizedHorz]) or (ev.data.l[1] == int self.netAtoms[NetWMStateMaximizedVert]) or 76 | (ev.data.l[2] == int self.netAtoms[NetWMStateMaximizedVert]) or (ev.data.l[2] == int self.netAtoms[NetWMStateMaximizedHorz]): 77 | if ev.data.l[0] == 2: 78 | self.maximizeClient client[] 79 | elif ev.data.l[0] == 1: # add max 80 | if client.maximized: return 81 | self.maximizeClient client[] 82 | elif ev.data.l[0] == 0: # rm max 83 | if not client.maximized: return 84 | self.maximizeClient client[] 85 | else: 86 | # wtf 87 | discard 88 | elif ev.messageType == self.netAtoms[NetActiveWindow]: 89 | if ev.format != 32: return 90 | let clientOpt = self.findClient do (client: Client) -> 91 | bool: client.window == ev.window 92 | if clientOpt.isNone: return 93 | let client = clientOpt.get[0] 94 | discard self.dpy.XRaiseWindow client.frame.window 95 | discard self.dpy.XSetInputFocus(client.window, RevertToPointerRoot, CurrentTime) 96 | discard self.dpy.XMapWindow client.frame.window 97 | self.focused = some clientOpt.get[1] 98 | self.raiseClient clientOpt.get[0][] 99 | elif ev.messageType == self.netAtoms[NetCurrentDesktop]: 100 | self.tags.switchTag uint8 ev.data.l[0] 101 | self.updateTagState 102 | let numdesk = [ev.data.l[0]] 103 | discard self.dpy.XChangeProperty( 104 | self.root, 105 | self.netAtoms[NetCurrentDesktop], 106 | XaCardinal, 107 | 32, 108 | PropModeReplace, 109 | cast[cstring](unsafeAddr numdesk), 110 | 1 111 | ) 112 | discard self.dpy.XSetInputFocus(self.root, RevertToPointerRoot, CurrentTime) 113 | self.focused = none uint 114 | if self.clients.len == 0: return 115 | var lcot = -1 116 | for i, c in self.clients: 117 | if c.tags == self.tags: lcot = i 118 | if lcot == -1: return 119 | self.focused = some uint lcot 120 | self.raiseClient self.clients[self.focused.get] 121 | if self.layout == lyTiling: self.tileWindows 122 | elif ev.messageType == self.ipcAtoms[IpcClientMessage]: # Register events from our IPC-based event system 123 | if ev.format != 32: return # check we can access the union member 124 | if ev.data.l[0] == clong self.ipcAtoms[IpcBorderInactivePixel]: 125 | log "Changing inactive border pixel to " & $ev.data.l[1] 126 | self.config.borderInactivePixel = uint ev.data.l[1] 127 | for i, client in self.clients: 128 | if (self.focused.isSome and uint(i) != self.focused.get) or 129 | self.focused.isNone: discard self.dpy.XSetWindowBorder( 130 | client.frame.window, self.config.borderInactivePixel) 131 | elif ev.data.l[0] == clong self.ipcAtoms[IpcBorderActivePixel]: 132 | log "Changing active border pixel to " & $ev.data.l[1] 133 | self.config.borderActivePixel = uint ev.data.l[1] 134 | if self.focused.isSome: discard self.dpy.XSetWindowBorder(self.clients[ 135 | self.focused.get].frame.window, self.config.borderActivePixel) 136 | elif ev.data.l[0] == clong self.ipcAtoms[IpcBorderWidth]: 137 | log "Changing border width to " & $ev.data.l[1] 138 | self.config.borderWidth = uint ev.data.l[1] 139 | for client in self.clients: 140 | discard self.dpy.XSetWindowBorderWidth(client.frame.window, 141 | cuint self.config.borderWidth) 142 | # In the case that the border width changed, the outer frame's dimensions also changed. 143 | # To the X perspective because borders are handled by the server the actual window 144 | # geometry remains the same. However, we need to still inform the client of the change 145 | # by changing the _NET_FRAME_EXTENTS property, if it's EWMH compliant it may respect 146 | # this. 147 | let extents = [self.config.borderWidth, self.config.borderWidth, 148 | self.config.borderWidth+self.config.frameHeight, 149 | self.config.borderWidth] 150 | discard self.dpy.XChangeProperty( 151 | client.window, 152 | self.netAtoms[NetFrameExtents], 153 | XaCardinal, 154 | 32, 155 | PropModeReplace, 156 | cast[cstring](unsafeAddr extents), 157 | 4 158 | ) 159 | elif ev.data.l[0] == clong self.ipcAtoms[IpcFrameInactivePixel]: 160 | log "Changing frame inactive pixel to " & $ev.data.l[1] 161 | self.config.frameInactivePixel = uint ev.data.l[1] 162 | for i, client in self.clients: 163 | if self.focused.isSome and i == self.focused.get.int: return 164 | for window in [client.frame.top,client.frame.title,client.frame.window,client.frame.close,client.frame.maximize]: discard self.dpy.XSetWindowBackground(window, 165 | cuint self.config.frameInactivePixel) 166 | elif ev.data.l[0] == clong self.ipcAtoms[IpcFrameActivePixel]: 167 | log "Changing frame active pixel to " & $ev.data.l[1] 168 | self.config.frameActivePixel = uint ev.data.l[1] 169 | if self.focused.isNone: return 170 | for win in [ 171 | self.clients[self.focused.get].frame.window, 172 | self.clients[self.focused.get].frame.top, 173 | self.clients[self.focused.get].frame.title, 174 | self.clients[self.focused.get].frame.close, 175 | self.clients[self.focused.get].frame.maximize 176 | ]: discard self.dpy.XSetWindowBackground(win, 177 | cuint self.config.frameActivePixel) 178 | elif ev.data.l[0] == clong self.ipcAtoms[IpcFrameHeight]: 179 | log "Changing frame height to " & $ev.data.l[1] 180 | self.config.frameHeight = uint ev.data.l[1] 181 | for client in mitems self.clients: 182 | if client.csd: return 183 | client.frameHeight = self.config.frameHeight 184 | var attr: XWindowAttributes 185 | discard self.dpy.XGetWindowAttributes(client.window, addr attr) 186 | discard self.dpy.XResizeWindow(client.frame.window, cuint attr.width, 187 | cuint attr.height + cint self.config.frameHeight) 188 | discard self.dpy.XMoveResizeWindow(client.window, 0, 189 | cint self.config.frameHeight, cuint attr.width, cuint attr.height) 190 | # See the comment in the setter for IpcBorderWidth. The exact same thing applies for 191 | # IpcFrameWidth, except in this case the geometry from X11 perspective is actually impacted. 192 | let extents = [self.config.borderWidth, self.config.borderWidth, 193 | self.config.borderWidth+self.config.frameHeight, 194 | self.config.borderWidth] 195 | discard self.dpy.XChangeProperty( 196 | client.window, 197 | self.netAtoms[NetFrameExtents], 198 | XaCardinal, 199 | 32, 200 | PropModeReplace, 201 | cast[cstring](unsafeAddr extents), 202 | 4 203 | ) 204 | if self.layout == lyTiling: self.tileWindows 205 | elif ev.data.l[0] == clong self.ipcAtoms[IpcTextActivePixel]: 206 | log "Chaging text active pixel to " & $ev.data.l[1] 207 | self.config.textActivePixel = uint ev.data.l[1] 208 | if self.focused.isNone: return 209 | self.raiseClient self.clients[self.focused.get] 210 | elif ev.data.l[0] == clong self.ipcAtoms[IpcTextInactivePixel]: 211 | log "Chaging text inactive pixel to " & $ev.data.l[1] 212 | self.config.textInactivePixel = uint ev.data.l[1] 213 | if self.focused.isNone: return 214 | self.raiseClient self.clients[self.focused.get] 215 | elif ev.data.l[0] == clong self.ipcAtoms[IpcTextFont]: 216 | log "IpcTextFont" 217 | var fontProp: XTextProperty 218 | var fontList: ptr UncheckedArray[cstring] 219 | var n: cint 220 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 221 | IpcTextFont]) 222 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 223 | ptr ptr cstring](addr fontList), addr n) 224 | log "Changing text font to " & $fontList[0] 225 | self.font = self.dpy.XftFontOpenName(XDefaultScreen self.dpy, fontList[0]) 226 | if err >= Success and n > 0 and fontList != nil: 227 | XFreeStringList cast[ptr cstring](fontList) 228 | discard XFree fontProp.value 229 | elif ev.data.l[0] == clong self.ipcAtoms[IpcTextOffset]: 230 | log "Changing text offset to (x: " & $ev.data.l[1] & ", y: " & $ev.data.l[ 231 | 2] & ")" 232 | self.config.textOffset = (x: uint ev.data.l[1], y: uint ev.data.l[2]) 233 | for client in self.clients.mitems: self.renderTop client 234 | elif ev.data.l[0] == clong self.ipcAtoms[IpcKillClient]: 235 | let window = if ev.data.l[1] == 0: self.clients[ 236 | if self.focused.isSome: self.focused.get else: return].window else: Window ev.data.l[1] 237 | discard self.dpy.XKillClient window 238 | elif ev.data.l[0] == clong self.ipcAtoms[IpcCloseClient]: 239 | let window = if ev.data.l[1] == 0: self.clients[ 240 | if self.focused.isSome: self.focused.get else: return].window else: Window ev.data.l[1] 241 | let cm = XEvent(xclient: XClientMessageEvent(format: 32, 242 | theType: ClientMessage, serial: 0, sendEvent: true, display: self.dpy, 243 | window: window, messageType: self.dpy.XInternAtom("WM_PROTOCOLS", 244 | false), 245 | data: XClientMessageData(l: [clong self.dpy.XInternAtom( 246 | "WM_DELETE_WINDOW", false), CurrentTime, 0, 0, 0]))) 247 | discard self.dpy.XSendEvent(window, false, NoEventMask, cast[ptr XEvent](unsafeAddr cm)) 248 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMaximizeClient]: 249 | let window = if ev.data.l[1] == 0: self.clients[ 250 | if self.focused.isSome: self.focused.get else: return].window else: Window ev.data.l[1] 251 | var client: Client 252 | if self.focused.isSome: 253 | client = self.clients[self.focused.get] 254 | else: 255 | var co = self.findClient do (c: Client) -> bool: c.window == Window ev.data.l[1] 256 | if co.isNone: return 257 | client = (co.get)[0][] 258 | self.maximizeClient client 259 | elif ev.data.l[0] == clong self.ipcAtoms[IpcSwitchTag]: 260 | echo ev.data.l 261 | self.tags.switchTag uint8 ev.data.l[1] - 1 262 | self.updateTagState 263 | let numdesk = [ev.data.l[1] - 1] 264 | discard self.dpy.XChangeProperty( 265 | self.root, 266 | self.netAtoms[NetCurrentDesktop], 267 | XaCardinal, 268 | 32, 269 | PropModeReplace, 270 | cast[cstring](unsafeAddr numdesk), 271 | 1 272 | ) 273 | discard self.dpy.XSetInputFocus(self.root, RevertToPointerRoot, CurrentTime) 274 | self.focused = none uint 275 | if self.clients.len == 0: return 276 | var lcot = -1 277 | for i, c in self.clients: 278 | if c.tags == self.tags: lcot = i 279 | if lcot == -1: return 280 | self.focused = some uint lcot 281 | discard self.dpy.XRaiseWindow self.clients[self.focused.get].frame.window 282 | discard self.dpy.XSetInputFocus(self.clients[self.focused.get].window, RevertToPointerRoot, CurrentTime) 283 | self.raiseClient self.clients[self.focused.get] 284 | if self.layout == lyTiling: self.tileWindows 285 | elif ev.data.l[0] == clong self.ipcAtoms[IpcAddTag]: 286 | discard 287 | elif ev.data.l[0] == clong self.ipcAtoms[IpcRemoveTag]: 288 | discard 289 | elif ev.data.l[0] == clong self.ipcAtoms[IpcLayout]: 290 | # We recieve this IPC event when a client such as wormc wishes to change the layout (eg, floating -> tiling) 291 | if ev.data.l[1] notin {0, 1}: return 292 | self.layout = Layout ev.data.l[1] 293 | for i, _ in self.clients: 294 | self.clients[i].floating = self.layout == lyFloating 295 | log $self.clients 296 | if self.layout == lyTiling: self.tileWindows 297 | elif ev.data.l[0] == clong self.ipcAtoms[IpcGaps]: 298 | self.config.gaps = int ev.data.l[1] 299 | if self.layout == lyTiling: self.tileWindows 300 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMaster]: 301 | # Get the index of the client, for swapping. 302 | # this isn't actually done yet 303 | let newMasterIdx = block: 304 | if ev.data.l[1] != 0: 305 | let clientOpt = self.findClient do (client: Client) -> 306 | bool: client.window == uint ev.data.l[1] 307 | if clientOpt.isNone: return 308 | clientOpt.get[1] 309 | else: 310 | if self.focused.isSome: self.focused.get else: return 311 | var 312 | currMasterOpt: Option[Client] = none Client 313 | currMasterIdx: uint = 0 314 | for i, client in self.clients: 315 | if client.tags == self.tags: # We only care about clients on the current tag. 316 | if currMasterOpt.isNone: # This must be the first client on the tag, otherwise master would not be nil; therefore, we promote it to master. 317 | currMasterOpt = some self.clients[i] 318 | currMasterIdx = uint i 319 | if currMasterOpt.isNone: return 320 | let currMaster = currMasterOpt.get 321 | self.clients[currMasterIdx] = self.clients[newMasterIdx] 322 | self.clients[newMasterIdx] = currMaster 323 | if self.layout == lyTiling: self.tileWindows 324 | elif ev.data.l[0] == clong self.ipcAtoms[IpcStruts]: 325 | self.config.struts = ( 326 | top: uint ev.data.l[1], 327 | bottom: uint ev.data.l[2], 328 | left: uint ev.data.l[3], 329 | right: uint ev.data.l[4] 330 | ) 331 | if self.layout == lyTiling: self.tileWindows 332 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMoveTag]: # [tag, wid | 0, 0, 0, 0] 333 | log $ev.data.l 334 | let tag = ev.data.l[1] - 1 335 | let client = block: 336 | if ev.data.l[2] != 0: 337 | let clientOpt = self.findClient do (client: Client) -> 338 | bool: client.window == uint ev.data.l[2] 339 | if clientOpt.isNone: return 340 | clientOpt.get[1] 341 | else: 342 | if self.focused.isSome: self.focused.get else: return 343 | self.clients[client].tags = [false, false, false, false, false, false, 344 | false, false, false] 345 | self.clients[client].tags[tag] = true 346 | self.updateTagState 347 | if self.layout == lyTiling: self.tileWindows 348 | elif ev.data.l[0] == clong self.ipcAtoms[IpcFloat]: 349 | let client = block: 350 | if ev.data.l[1] != 0: 351 | let clientOpt = self.findClient do (client: Client) -> 352 | bool: client.window == uint ev.data.l[1] 353 | if clientOpt.isNone: return 354 | clientOpt.get[1] 355 | else: 356 | if self.focused.isSome: self.focused.get else: return 357 | self.clients[client].floating = not self.clients[client].floating 358 | if self.layout == lyTiling: self.tileWindows 359 | elif ev.data.l[0] == clong self.ipcAtoms[IpcFrameLeft]: 360 | var fontProp: XTextProperty 361 | var fontList: ptr UncheckedArray[cstring] 362 | var n: cint 363 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 364 | IpcFrameLeft]) 365 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 366 | ptr ptr cstring](addr fontList), addr n) 367 | if fontList == nil or fontList[0] == nil and err >= Success and n > 0: return 368 | let x = ($fontList[0]).split ";" 369 | var parts: seq[FramePart] 370 | for v in x: 371 | parts.add case v: 372 | of "T": fpTitle 373 | of "C": fpClose 374 | of "M": fpMaximize 375 | of "I": fpMinimize 376 | else: continue 377 | self.config.frameParts.left = parts 378 | log $self.config.frameParts 379 | XFreeStringList cast[ptr cstring](fontList) 380 | elif ev.data.l[0] == clong self.ipcAtoms[IpcFrameCenter]: 381 | var fontProp: XTextProperty 382 | var fontList: ptr UncheckedArray[cstring] 383 | var n: cint 384 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 385 | IpcFrameCenter]) 386 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 387 | ptr ptr cstring](addr fontList), addr n) 388 | if fontList == nil or fontList[0] == nil and err >= Success and n > 0: return 389 | let x = ($fontList[0]).split ";" 390 | var parts: seq[FramePart] 391 | for v in x: 392 | parts.add case v: 393 | of "T": fpTitle 394 | of "C": fpClose 395 | of "M": fpMaximize 396 | of "I": fpMinimize 397 | else: continue 398 | self.config.frameParts.center = parts 399 | log $self.config.frameParts 400 | XFreeStringList cast[ptr cstring](fontList) 401 | elif ev.data.l[0] == clong self.ipcAtoms[IpcFrameRight]: 402 | log "Frame Right" 403 | var fontProp: XTextProperty 404 | var fontList: ptr UncheckedArray[cstring] 405 | var n: cint 406 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 407 | IpcFrameRight]) 408 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 409 | ptr ptr cstring](addr fontList), addr n) 410 | if fontList == nil or fontList[0] == nil and err >= Success and n > 0: return 411 | let x = ($fontList[0]).split ";" 412 | var parts: seq[FramePart] 413 | for v in x: 414 | parts.add case v: 415 | of "T": fpTitle 416 | of "C": fpClose 417 | of "M": fpMaximize 418 | of "I": fpMinimize 419 | else: continue 420 | self.config.frameParts.right = parts 421 | log $self.config.frameParts 422 | XFreeStringList cast[ptr cstring](fontList) 423 | elif ev.data.l[0] == clong self.ipcAtoms[IpcButtonOffset]: 424 | self.config.buttonOffset = ( 425 | x: uint ev.data.l[1], 426 | y: uint ev.data.l[2] 427 | ) 428 | log $self.config.buttonOffset 429 | elif ev.data.l[0] == clong self.ipcAtoms[IpcButtonSize]: 430 | self.config.buttonSize = uint ev.data.l[1] 431 | elif ev.data.l[0] == clong self.ipcAtoms[IpcRootMenu]: 432 | var fontProp: XTextProperty 433 | var fontList: ptr UncheckedArray[cstring] 434 | var n: cint 435 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 436 | IpcRootMenu]) 437 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 438 | ptr ptr cstring](addr fontList), addr n) 439 | log "Changing root menu path to " & $fontList[0] 440 | self.config.rootMenu = $fontList[0] 441 | if err >= Success and n > 0 and fontList != nil: 442 | XFreeStringList cast[ptr cstring](fontList) 443 | discard XFree fontProp.value 444 | elif ev.data.l[0] == clong self.ipcAtoms[IpcCloseActivePath]: 445 | var fontProp: XTextProperty 446 | var fontList: ptr UncheckedArray[cstring] 447 | var n: cint 448 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 449 | IpcCloseActivePath]) 450 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 451 | ptr ptr cstring](addr fontList), addr n) 452 | log "Changing active close path to " & $fontList[0] 453 | self.config.closePaths[bsActive] = $fontList[0] 454 | if err >= Success and n > 0 and fontList != nil: 455 | XFreeStringList cast[ptr cstring](fontList) 456 | discard XFree fontProp.value 457 | elif ev.data.l[0] == clong self.ipcAtoms[IpcCloseInactivePath]: 458 | var fontProp: XTextProperty 459 | var fontList: ptr UncheckedArray[cstring] 460 | var n: cint 461 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 462 | IpcCloseInactivePath]) 463 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 464 | ptr ptr cstring](addr fontList), addr n) 465 | log "Changing inactive close path to " & $fontList[0] 466 | self.config.closePaths[bsInactive] = $fontList[0] 467 | if err >= Success and n > 0 and fontList != nil: 468 | XFreeStringList cast[ptr cstring](fontList) 469 | discard XFree fontProp.value 470 | elif ev.data.l[0] == clong self.ipcAtoms[IpcCloseActiveHoveredPath]: 471 | var fontProp: XTextProperty 472 | var fontList: ptr UncheckedArray[cstring] 473 | var n: cint 474 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 475 | IpcCloseActiveHoveredPath]) 476 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 477 | ptr ptr cstring](addr fontList), addr n) 478 | log "Changing active hovered close path to " & $fontList[0] 479 | self.config.closePaths[bsActiveHover] = $fontList[0] 480 | if err >= Success and n > 0 and fontList != nil: 481 | XFreeStringList cast[ptr cstring](fontList) 482 | discard XFree fontProp.value 483 | elif ev.data.l[0] == clong self.ipcAtoms[IpcCloseInactiveHoveredPath]: 484 | var fontProp: XTextProperty 485 | var fontList: ptr UncheckedArray[cstring] 486 | var n: cint 487 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 488 | IpcCloseInactiveHoveredPath]) 489 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 490 | ptr ptr cstring](addr fontList), addr n) 491 | log "Changing inactive hovered close path to " & $fontList[0] 492 | self.config.closePaths[bsInactiveHover] = $fontList[0] 493 | if err >= Success and n > 0 and fontList != nil: 494 | XFreeStringList cast[ptr cstring](fontList) 495 | discard XFree fontProp.value 496 | elif ev.data.l[0] == clong self.ipcAtoms[IpcClosePath]: 497 | var fontProp: XTextProperty 498 | var fontList: ptr UncheckedArray[cstring] 499 | var n: cint 500 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 501 | IpcClosePath]) 502 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 503 | ptr ptr cstring](addr fontList), addr n) 504 | log "Changing close path to " & $fontList[0] 505 | self.config.closePaths[bsActive] = $fontList[0] 506 | self.config.closePaths[bsInactive] = $fontList[0] 507 | self.config.closePaths[bsActiveHover] = $fontList[0] 508 | self.config.closePaths[bsInactiveHover] = $fontList[0] 509 | if err >= Success and n > 0 and fontList != nil: 510 | XFreeStringList cast[ptr cstring](fontList) 511 | discard XFree fontProp.value 512 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMaximizeActivePath]: 513 | var fontProp: XTextProperty 514 | var fontList: ptr UncheckedArray[cstring] 515 | var n: cint 516 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 517 | IpcMaximizeActivePath]) 518 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 519 | ptr ptr cstring](addr fontList), addr n) 520 | log "Changing active maximize path to " & $fontList[0] 521 | self.config.maximizePaths[bsActive] = $fontList[0] 522 | if err >= Success and n > 0 and fontList != nil: 523 | XFreeStringList cast[ptr cstring](fontList) 524 | discard XFree fontProp.value 525 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMaximizeInactivePath]: 526 | var fontProp: XTextProperty 527 | var fontList: ptr UncheckedArray[cstring] 528 | var n: cint 529 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 530 | IpcMaximizeInactivePath]) 531 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 532 | ptr ptr cstring](addr fontList), addr n) 533 | log "Changing inactive maximize path to " & $fontList[0] 534 | self.config.maximizePaths[bsInactive] = $fontList[0] 535 | if err >= Success and n > 0 and fontList != nil: 536 | XFreeStringList cast[ptr cstring](fontList) 537 | discard XFree fontProp.value 538 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMaximizeActiveHoveredPath]: 539 | var fontProp: XTextProperty 540 | var fontList: ptr UncheckedArray[cstring] 541 | var n: cint 542 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 543 | IpcMaximizeActiveHoveredPath]) 544 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 545 | ptr ptr cstring](addr fontList), addr n) 546 | log "Changing active maximize hovered path to " & $fontList[0] 547 | self.config.maximizePaths[bsActiveHover] = $fontList[0] 548 | if err >= Success and n > 0 and fontList != nil: 549 | XFreeStringList cast[ptr cstring](fontList) 550 | discard XFree fontProp.value 551 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMaximizeInactiveHoveredPath]: 552 | var fontProp: XTextProperty 553 | var fontList: ptr UncheckedArray[cstring] 554 | var n: cint 555 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 556 | IpcMaximizeInactiveHoveredPath]) 557 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 558 | ptr ptr cstring](addr fontList), addr n) 559 | log "Changing inactive maximize hovered path to " & $fontList[0] 560 | self.config.maximizePaths[bsInactiveHover] = $fontList[0] 561 | if err >= Success and n > 0 and fontList != nil: 562 | XFreeStringList cast[ptr cstring](fontList) 563 | discard XFree fontProp.value 564 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMaximizePath]: 565 | var fontProp: XTextProperty 566 | var fontList: ptr UncheckedArray[cstring] 567 | var n: cint 568 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 569 | IpcMaximizePath]) 570 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 571 | ptr ptr cstring](addr fontList), addr n) 572 | log "Changing maximize path to " & $fontList[0] 573 | self.config.maximizePaths[bsActive] = $fontList[0] 574 | self.config.maximizePaths[bsInactive] = $fontList[0] 575 | self.config.maximizePaths[bsActiveHover] = $fontList[0] 576 | self.config.maximizePaths[bsInactiveHover] = $fontList[0] 577 | if err >= Success and n > 0 and fontList != nil: 578 | XFreeStringList cast[ptr cstring](fontList) 579 | discard XFree fontProp.value 580 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMinimizeActivePath]: 581 | var fontProp: XTextProperty 582 | var fontList: ptr UncheckedArray[cstring] 583 | var n: cint 584 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 585 | IpcMinimizeActivePath]) 586 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 587 | ptr ptr cstring](addr fontList), addr n) 588 | log "Changing active minimize path to " & $fontList[0] 589 | self.config.minimizePaths[bsActive] = $fontList[0] 590 | if err >= Success and n > 0 and fontList != nil: 591 | XFreeStringList cast[ptr cstring](fontList) 592 | discard XFree fontProp.value 593 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMinimizeInactivePath]: 594 | var fontProp: XTextProperty 595 | var fontList: ptr UncheckedArray[cstring] 596 | var n: cint 597 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 598 | IpcMinimizeInactivePath]) 599 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 600 | ptr ptr cstring](addr fontList), addr n) 601 | log "Changing inactive minimize path to " & $fontList[0] 602 | self.config.minimizePaths[bsInactive] = $fontList[0] 603 | if err >= Success and n > 0 and fontList != nil: 604 | XFreeStringList cast[ptr cstring](fontList) 605 | discard XFree fontProp.value 606 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMinimizeActiveHoveredPath]: 607 | var fontProp: XTextProperty 608 | var fontList: ptr UncheckedArray[cstring] 609 | var n: cint 610 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 611 | IpcMinimizeActiveHoveredPath]) 612 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 613 | ptr ptr cstring](addr fontList), addr n) 614 | log "Changing active minimize hovered path to " & $fontList[0] 615 | self.config.minimizePaths[bsActiveHover] = $fontList[0] 616 | if err >= Success and n > 0 and fontList != nil: 617 | XFreeStringList cast[ptr cstring](fontList) 618 | discard XFree fontProp.value 619 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMinimizeInactiveHoveredPath]: 620 | var fontProp: XTextProperty 621 | var fontList: ptr UncheckedArray[cstring] 622 | var n: cint 623 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 624 | IpcMinimizeInactiveHoveredPath]) 625 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 626 | ptr ptr cstring](addr fontList), addr n) 627 | log "Changing inactive minimize hovered path to " & $fontList[0] 628 | self.config.minimizePaths[bsInactiveHover] = $fontList[0] 629 | if err >= Success and n > 0 and fontList != nil: 630 | XFreeStringList cast[ptr cstring](fontList) 631 | discard XFree fontProp.value 632 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMinimizePath]: 633 | var fontProp: XTextProperty 634 | var fontList: ptr UncheckedArray[cstring] 635 | var n: cint 636 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 637 | IpcMinimizePath]) 638 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 639 | ptr ptr cstring](addr fontList), addr n) 640 | log "Changing minimize path to " & $fontList[0] 641 | self.config.minimizePaths[bsActive] = $fontList[0] 642 | self.config.minimizePaths[bsInactive] = $fontList[0] 643 | self.config.minimizePaths[bsActiveHover] = $fontList[0] 644 | self.config.minimizePaths[bsInactiveHover] = $fontList[0] 645 | if err >= Success and n > 0 and fontList != nil: 646 | XFreeStringList cast[ptr cstring](fontList) 647 | discard XFree fontProp.value 648 | elif ev.data.l[0] == clong self.ipcAtoms[IpcDecorationDisable]: 649 | var fontProp: XTextProperty 650 | var fontList: ptr UncheckedArray[cstring] 651 | var n: cint 652 | discard self.dpy.XGetTextProperty(self.root, addr fontProp, self.ipcAtoms[ 653 | IpcDecorationDisable]) 654 | let err = self.dpy.XmbTextPropertyToTextList(addr fontProp, cast[ 655 | ptr ptr cstring](addr fontList), addr n) 656 | log "Appending to decoration disable list: " & $fontList[0] 657 | self.noDecorList.add re2 $fontList[0] 658 | if err >= Success and n > 0 and fontList != nil: 659 | XFreeStringList cast[ptr cstring](fontList) 660 | discard XFree fontProp.value 661 | elif ev.data.l[0] == clong self.ipcAtoms[IpcMinimizeClient]: 662 | let window = if ev.data.l[1] == 0: self.clients[ 663 | if self.focused.isSome: self.focused.get else: return].window else: Window ev.data.l[1] 664 | var client: Client 665 | if self.focused.isSome: 666 | client = self.clients[self.focused.get] 667 | else: 668 | var co = self.findClient do (c: Client) -> bool: c.window == Window ev.data.l[1] 669 | if co.isNone: return 670 | client = (co.get)[0][] 671 | self.minimizeClient client 672 | elif ev.data.l[0] == clong self.ipcAtoms[IpcModifier]: 673 | log "Changing modifier to " & $ev.data.l[1] 674 | let oldModifier = self.config.modifier 675 | self.config.modifier = uint32 ev.data.l[1] 676 | let modifier: uint32 = self.config.modifier 677 | for button in [1'u8, 3]: 678 | discard self.dpy.XUngrabButton(button, oldModifier, self.root) 679 | for mask in [ 680 | modifier, modifier or Mod2Mask, modifier or LockMask, 681 | modifier or Mod3Mask, modifier or Mod2Mask or LockMask, modifier or 682 | LockMask or Mod3Mask, modifier or Mod2Mask or Mod3Mask, modifier or 683 | Mod2Mask or LockMask or Mod3Mask 684 | ]: 685 | discard self.dpy.XGrabButton( 686 | button, 687 | mask, 688 | self.root, 689 | true, 690 | ButtonPressMask or PointerMotionMask, 691 | GrabModeAsync, 692 | GrabModeAsync, 693 | None, 694 | None 695 | ) 696 | elif ev.data.l[0] == clong self.ipcAtoms[IpcFocusMode]: 697 | if ev.data.l[1] == 1: 698 | self.focusMode = FocusFollowsClick 699 | else: 700 | self.focusMode = FocusFollowsMouse 701 | -------------------------------------------------------------------------------- /src/wm.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/[options, os, sequtils, strutils], 3 | x11/[xlib, x, xft, xatom, xinerama, xrender], 4 | types, 5 | atoms, 6 | log, 7 | pixie, 8 | regex 9 | 10 | converter toXBool*(x: bool): XBool = x.XBool 11 | converter toBool*(x: XBool): bool = x.bool 12 | 13 | type 14 | Wm* = object 15 | dpy*: ptr Display 16 | root*: Window 17 | motionInfo*: Option[MotionInfo] 18 | currEv*: XEvent 19 | clients*: seq[Client] 20 | font*: ptr XftFont 21 | netAtoms*: array[NetAtom, Atom] 22 | ipcAtoms*: array[IpcAtom, Atom] 23 | config*: Config 24 | focused*: Option[uint] 25 | tags*: TagSet 26 | layout*: Layout 27 | noDecorList*: seq[Regex2] 28 | focusMode*: FocusMode 29 | 30 | proc initWm*(): Wm = 31 | let dpy = XOpenDisplay nil 32 | 33 | if dpy == nil: 34 | quit 1 35 | 36 | log "Opened display" 37 | 38 | let root = XDefaultRootWindow dpy 39 | 40 | for button in [1'u8, 3]: 41 | # list from sxhkd (Mod2Mask NumLock, Mod3Mask ScrollLock, LockMask CapsLock). 42 | let modifier: uint32 = Mod1Mask 43 | for mask in [ 44 | modifier, modifier or Mod2Mask, modifier or LockMask, 45 | modifier or Mod3Mask, modifier or Mod2Mask or LockMask, modifier or 46 | LockMask or Mod3Mask, modifier or Mod2Mask or Mod3Mask, modifier or 47 | Mod2Mask or LockMask or Mod3Mask 48 | ]: 49 | discard dpy.XGrabButton( 50 | button, 51 | mask, 52 | root, 53 | true, 54 | ButtonPressMask or PointerMotionMask, 55 | GrabModeAsync, 56 | GrabModeAsync, 57 | None, 58 | None 59 | ) 60 | 61 | discard dpy.XSelectInput( 62 | root, 63 | SubstructureRedirectMask or SubstructureNotifyMask or ButtonPressMask 64 | ) 65 | 66 | let 67 | font = dpy.XftFontOpenName(XDefaultScreen dpy, "Noto Sans Mono:size=11") 68 | netAtoms = getNetAtoms dpy 69 | 70 | discard dpy.XChangeProperty( 71 | root, 72 | netAtoms[NetSupportingWMCheck], 73 | XaWindow, 74 | 32, 75 | PropModeReplace, 76 | cast[cstring](unsafeAddr root), 77 | 1 78 | ) 79 | 80 | discard dpy.XChangeProperty( 81 | root, 82 | netAtoms[NetSupported], 83 | XaAtom, 84 | 32, 85 | PropModeReplace, 86 | cast[cstring](unsafeAddr netAtoms), 87 | netAtoms.len.cint 88 | ) 89 | 90 | let wmname = "worm".cstring 91 | discard dpy.XChangeProperty( 92 | root, 93 | netAtoms[NetWMName], 94 | dpy.XInternAtom("UTF8_STRING", false), 95 | 8, 96 | PropModeReplace, 97 | wmname, 98 | 4 99 | ) 100 | 101 | var numdesk = [9] 102 | discard dpy.XChangeProperty( 103 | root, 104 | netAtoms[NetNumberOfDesktops], 105 | XaCardinal, 106 | 32, 107 | PropModeReplace, 108 | cast[cstring](addr numdesk), 109 | 1 110 | ) 111 | 112 | numdesk = [0] 113 | discard dpy.XChangeProperty( 114 | root, 115 | netAtoms[NetCurrentDesktop], 116 | XaCardinal, 117 | 32, 118 | PropModeReplace, 119 | cast[cstring](addr numdesk), 120 | 1 121 | ) 122 | 123 | discard dpy.XChangeProperty( 124 | root, 125 | netAtoms[NetClientList], 126 | XaWindow, 127 | 32, 128 | PropModeReplace, 129 | nil, 130 | 0 131 | ) 132 | 133 | discard XSetErrorHandler( 134 | proc(dpy: ptr Display; err: ptr XErrorEvent): cint {.cdecl.} = 0 135 | ) 136 | 137 | discard dpy.XSync false 138 | 139 | discard dpy.XFlush 140 | 141 | # The default configuration is reasonably sane, and for now based on the 142 | # Iceberg colorscheme. It may be changed later; it's recommended for users to 143 | # write their own. 144 | Wm( 145 | dpy: dpy, 146 | root: root, 147 | motionInfo: none MotionInfo, 148 | font: font, 149 | netAtoms: netAtoms, 150 | ipcAtoms: getIpcAtoms dpy, 151 | config: Config( 152 | borderActivePixel: uint 0xFF7499CC, 153 | borderInactivePixel: uint 0xFF000000, 154 | borderWidth: 1, 155 | frameActivePixel: uint 0xFF161821, 156 | frameInactivePixel: uint 0xFF666666, 157 | frameHeight: 30, 158 | textActivePixel: uint 0xFFFFFFFF, 159 | textInactivePixel: uint 0xFF000000, 160 | textOffset: (x: uint 10, y: uint 20), 161 | gaps: 0, 162 | buttonSize: 14, 163 | struts: (top: uint 10, bottom: uint 40, left: uint 10, right: uint 10), 164 | modifier: Mod1Mask 165 | ), 166 | tags: defaultTagSet(), 167 | layout: lyFloating, 168 | noDecorList: @[], 169 | focusMode: FocusFollowsClick 170 | ) 171 | 172 | proc findClient*( 173 | self: var Wm; 174 | predicate: proc(client: Client): bool 175 | ): Option[(ptr Client, uint)] = 176 | 177 | for i, client in self.clients: 178 | if predicate client: 179 | return some((addr self.clients[i], uint i)) 180 | 181 | proc createBtnImg(c: Config, imgPath: string, framePixel: uint): Image = 182 | let btnSize = c.buttonSize.int 183 | result = newImage(btnSize, btnSize) 184 | 185 | let buttonColor = cast[array[3, uint8]](framePixel) 186 | 187 | result.fill(rgba(buttonColor[2], buttonColor[1], buttonColor[0], 255)) 188 | 189 | let img = readImage(imgPath) 190 | result.draw( 191 | img, 192 | scale(vec2(btnSize / img.width, btnSize / img.height)) 193 | ) 194 | 195 | proc getBGRXBitmap(im: Image): seq[ColorRGBX] = 196 | var ctx = newContext im 197 | # convert to BGRA 198 | result = ctx.image.data 199 | 200 | for i, color in result: 201 | let x = color 202 | # RGBX -> BGRX 203 | result[i].r = x.b 204 | result[i].b = x.r 205 | 206 | proc XCreateImage( 207 | self: var Wm, 208 | imgPath: string, 209 | framePixel: uint, 210 | attr: XWindowAttributes 211 | ): PXImage = 212 | var screen = self.config.createBtnImg(imgPath, framePixel) 213 | 214 | var 215 | bitmap = screen.getBGRXBitmap() 216 | frameBuffer = addr bitmap[0] 217 | 218 | result = XCreateImage( 219 | self.dpy, 220 | attr.visual, 221 | attr.depth.cuint, 222 | ZPixmap, 223 | 0, 224 | cast[cstring](frameBuffer), 225 | self.config.buttonSize.cuint, 226 | self.config.buttonSize.cuint, 227 | 8, 228 | self.config.buttonSize.cint*4 229 | ) 230 | 231 | proc XPutImage(self: var Wm, img: PXImage, win: Window, gc: GC) = 232 | let btnSize = self.config.buttonSize.cuint 233 | discard self.dpy.XPutImage(win, gc, img, 0, 0, 0, 0, btnSize, btnSize) 234 | 235 | proc renderTop*(self: var Wm; client: var Client) = 236 | var extent: XGlyphInfo 237 | self.dpy.XftTextExtentsUtf8( 238 | self.font, 239 | cast[ptr char](cstring client.title), 240 | cint client.title.len, addr extent 241 | ) 242 | 243 | var attr: XWindowAttributes 244 | discard self.dpy.XGetWindowAttributes(client.frame.window, addr attr) 245 | 246 | for win in [client.frame.title, client.frame.close, client.frame.maximize]: 247 | discard self.dpy.XClearWindow win 248 | 249 | var 250 | gcVal: XGCValues 251 | gc = self.dpy.XCreateGC(client.frame.close, 0, addr gcVal) 252 | 253 | # discard self.dpy.XSetForeground(gc, self.config.textActivePixel) 254 | let 255 | isFocused = self.focused.isSome and client == self.clients[self.focused.get] 256 | fp = 257 | if isFocused: 258 | self.config.frameActivePixel 259 | else: 260 | self.config.frameInactivePixel 261 | var buttonState = if isFocused: 262 | bsActive 263 | else: 264 | bsInactive 265 | let buttonStateOrig = buttonState 266 | 267 | # draw the 3 'regions' of the titlebar; left, center, right 268 | #var 269 | # closeExists = false 270 | # maximizeExists = false 271 | # minimizeExists = false 272 | 273 | # can't remember why I did this, however it causes issues with hovering. 274 | # look into it later. 275 | 276 | #discard self.dpy.XUnmapWindow client.frame.close 277 | #discard self.dpy.XUnmapWindow client.frame.maximize 278 | #discard self.dpy.XUnmapWindow client.frame.minimize 279 | 280 | for i, part in self.config.frameParts.left: 281 | case part: 282 | of fpTitle: 283 | let 284 | buttonSize = self.config.buttonSize.cint 285 | buttonXOffset = self.config.buttonOffset.x.cint 286 | leftFrame0 = self.config.frameParts.left[0] 287 | 288 | offset = 289 | if i == 1 and leftFrame0 in {fpClose, fpMaximize, fpMinimize}: 290 | buttonSize + buttonXOffset 291 | elif i == 2: 292 | (buttonSize + buttonXOffset) * 2 293 | elif i == 3: 294 | (buttonSize + buttonXOffset) * 3 295 | else: 296 | 0 297 | 298 | client.draw.XftDrawStringUtf8( 299 | addr client.color, 300 | self.font, 301 | self.config.textOffset.x.cint + offset, 302 | self.config.textOffset.y.cint, 303 | cast[ptr char](cstring client.title), 304 | client.title.len.cint 305 | ) 306 | 307 | of fpClose: 308 | 309 | buttonState = if client.frame.closeHovered and buttonStateOrig == bsActive: 310 | bsActiveHover 311 | elif client.frame.closeHovered and buttonStateOrig == bsInactive: 312 | bsInactiveHover 313 | else: 314 | buttonStateOrig 315 | 316 | if not fileExists self.config.closePaths[buttonState]: 317 | buttonState = buttonStateOrig 318 | if not fileExists self.config.closePaths[buttonState]: continue 319 | 320 | discard self.dpy.XMapWindow client.frame.close 321 | 322 | let 323 | buttonSize = self.config.buttonSize.cint 324 | buttonXOffset = self.config.buttonOffset.x.cint 325 | buttonYOffset = self.config.buttonOffset.y.cint 326 | textXOffset = self.config.textOffset.x.cint 327 | leftFrame0 = self.config.frameParts.left[0] 328 | 329 | offset = 330 | if i == 1 and leftFrame0 == fpTitle: 331 | extent.width + textXOffset 332 | elif i == 1 and leftFrame0 in {fpMaximize, fpMinimize}: 333 | buttonSize + buttonXOffset 334 | elif i == 2 and (leftFrame0 == fpTitle or self.config.frameParts.left[1] == fpTitle): 335 | extent.width + textXOffset + 2*buttonXOffset + buttonSize 336 | elif i == 2: # no title 337 | 2*(buttonSize + buttonXOffset) 338 | elif i == 3: 339 | extent.width + textXOffset + buttonXOffset*2 + buttonSize*2 340 | else: 341 | 0 342 | 343 | discard self.dpy.XMoveWindow( 344 | client.frame.close, 345 | buttonXOffset + offset, 346 | buttonYOffset 347 | ) 348 | 349 | let image = self.XCreateImage(self.config.closePaths[buttonState], fp, attr) 350 | 351 | self.XPutImage(image, client.frame.close, gc) 352 | 353 | of fpMaximize: 354 | 355 | buttonState = if client.frame.maximizeHovered and buttonStateOrig == bsActive: 356 | bsActiveHover 357 | elif client.frame.maximizeHovered and buttonStateOrig == bsInactive: 358 | bsInactiveHover 359 | else: 360 | buttonStateOrig 361 | 362 | if not fileExists self.config.maximizePaths[buttonState]: 363 | buttonState = buttonStateOrig 364 | if not fileExists self.config.maximizePaths[buttonState]: continue 365 | 366 | discard self.dpy.XMapWindow client.frame.maximize 367 | 368 | let 369 | leftFrame0 = self.config.frameParts.left[0] 370 | btnSize = self.config.buttonSize.cint 371 | btnXOffset = self.config.buttonOffset.x.cint 372 | textXOffset = self.config.textOffset.x.cint 373 | buttonSize = self.config.buttonSize.cint 374 | 375 | offset = 376 | if i == 1 and leftFrame0 == fpTitle: 377 | extent.width.cint 378 | elif i == 1 and leftFrame0 in {fpClose, fpMinimize}: 379 | btnSize + btnXOffset 380 | elif i == 2 and (leftFrame0 == fpTitle or self.config.frameParts.left[1] == fpTitle): 381 | extent.width + textXOffset + btnXOffset + buttonSize 382 | elif i == 2: # no title 383 | 2*(buttonSize + btnXOffset) 384 | elif i == 3: 385 | extent.width + textXOffset + btnXOffset*2 + buttonSize*2 386 | else: 387 | 0 388 | 389 | discard self.dpy.XMoveWindow( 390 | client.frame.maximize, 391 | self.config.buttonOffset.x.cint + offset, 392 | self.config.buttonOffset.y.cint 393 | ) 394 | 395 | let image = self.XCreateImage(self.config.maximizePaths[buttonState], fp, attr) 396 | 397 | self.XPutImage(image, client.frame.maximize, gc) 398 | of fpMinimize: 399 | 400 | buttonState = if client.frame.minimizeHovered and buttonStateOrig == bsActive: 401 | bsActiveHover 402 | elif client.frame.minimizeHovered and buttonStateOrig == bsInactive: 403 | bsInactiveHover 404 | else: 405 | buttonStateOrig 406 | 407 | if not fileExists self.config.minimizePaths[buttonState]: 408 | buttonState = buttonStateOrig 409 | if not fileExists self.config.minimizePaths[buttonState]: continue 410 | 411 | discard self.dpy.XMapWindow client.frame.minimize 412 | 413 | let 414 | leftFrame0 = self.config.frameParts.left[0] 415 | btnSize = self.config.buttonSize.cint 416 | btnXOffset = self.config.buttonOffset.x.cint 417 | textXOffset = self.config.textOffset.x.cint 418 | buttonSize = self.config.buttonSize.cint 419 | 420 | offset = 421 | if i == 1 and leftFrame0 == fpTitle: 422 | extent.width.cint + btnXOffset + textXOffset 423 | elif i == 1 and leftFrame0 in {fpClose, fpMaximize}: 424 | btnSize + btnXOffset 425 | elif i == 2 and (leftFrame0 == fpTitle or self.config.frameParts.left[1] == fpTitle): 426 | extent.width + textXOffset + btnXOffset + buttonSize 427 | elif i == 2: # no title 428 | 2*(buttonSize + btnXOffset) 429 | elif i == 3: 430 | extent.width + textXOffset + btnXOffset*2 + buttonSize*2 431 | else: 432 | 0 433 | 434 | discard self.dpy.XMoveWindow( 435 | client.frame.minimize, 436 | self.config.buttonOffset.x.cint + offset, 437 | self.config.buttonOffset.y.cint 438 | ) 439 | 440 | let image = self.XCreateImage(self.config.minimizePaths[buttonState], fp, attr) 441 | 442 | self.XPutImage(image, client.frame.minimize, gc) 443 | for i, part in self.config.frameParts.center: 444 | case part: 445 | of fpTitle: 446 | 447 | let configButtonSize = 448 | if i == 2: 449 | self.config.buttonSize.cint 450 | else: 451 | 0 452 | 453 | client.draw.XftDrawStringUtf8( 454 | addr client.color, 455 | self.font, 456 | ((attr.width div 2).cint - (extent.width.cint div 2)) + 457 | configButtonSize + self.config.textOffset.x.cint, 458 | self.config.textOffset.y.cint, 459 | cast[ptr char](cstring client.title), 460 | client.title.len.cint 461 | ) 462 | 463 | of fpClose: 464 | 465 | buttonState = if client.frame.closeHovered and buttonStateOrig == bsActive: 466 | bsActiveHover 467 | elif client.frame.closeHovered and buttonStateOrig == bsInactive: 468 | bsInactiveHover 469 | else: 470 | buttonStateOrig 471 | 472 | if not fileExists self.config.closePaths[buttonState]: 473 | buttonState = buttonStateOrig 474 | if not fileExists self.config.closePaths[buttonState]: continue 475 | 476 | let image = self.XCreateImage(self.config.closePaths[buttonState], fp, attr) 477 | 478 | let 479 | btnSize = self.config.buttonSize.cint 480 | btnXOffset = self.config.buttonOffset.x.cint 481 | textXOffset = self.config.textOffset.x.cint 482 | centerFrames = self.config.frameParts.center 483 | 484 | discard self.dpy.XMoveWindow( 485 | client.frame.close, 486 | (if i == 0: -btnXOffset else: btnXOffset) + ( 487 | if (i == 1 and centerFrames[0] == fpTitle and centerFrames.len == 2): 488 | textXOffset + extent.width div 2 489 | elif (i == 1 and centerFrames[0] == fpTitle and 490 | centerFrames.len > 2 and centerFrames[1] == fpMaximize): 491 | -(extent.width div 2) - btnSize - btnXOffset - textXOffset 492 | elif i == 2 and centerFrames[0] == fpTitle: 493 | (extent.width div 2) + btnXOffset + textXOffset + btnSize 494 | elif i == 1 and centerFrames[0] == fpTitle: 495 | (extent.width div 2) + btnXOffset + textXOffset - btnSize 496 | elif (i == 1 and centerFrames.len >= 3 and 497 | centerFrames[0] == fpMaximize and centerFrames[2] == fpTitle): 498 | # meh 499 | -(extent.width div 2) 500 | elif i == 2 and centerFrames[1] == fpTitle: 501 | btnSize + extent.width div 2 502 | elif i == 1 and centerFrames[0] in {fpMaximize, fpMinimize}: 503 | btnSize - btnXOffset 504 | elif i == 2: 505 | # -btnSize 506 | btnSize * 2 507 | else: 508 | 0 509 | ) + (attr.width div 2) - ( 510 | if (i == 0 and centerFrames.len > 1 and 511 | centerFrames.find(fpTitle) != -1): 512 | self.config.buttonSize.cint + extent.width div 2 513 | else: 514 | 0), 515 | self.config.buttonOffset.y.cint 516 | ) 517 | 518 | discard self.dpy.XMapWindow client.frame.close 519 | 520 | self.XPutImage(image, client.frame.close, gc) 521 | 522 | of fpMaximize: 523 | 524 | buttonState = if client.frame.maximizeHovered and buttonStateOrig == bsActive: 525 | bsActiveHover 526 | elif client.frame.maximizeHovered and buttonStateOrig == bsInactive: 527 | bsInactiveHover 528 | else: 529 | buttonStateOrig 530 | 531 | if not fileExists self.config.maximizePaths[buttonState]: 532 | buttonState = buttonStateOrig 533 | if not fileExists self.config.maximizePaths[buttonState]: continue 534 | 535 | discard self.dpy.XMapWindow client.frame.maximize 536 | 537 | let 538 | btnSize = self.config.buttonSize.cint 539 | btnXOffset = self.config.buttonOffset.x.cint 540 | btnYOffset = self.config.buttonOffset.y.cint 541 | textXOffset = self.config.textOffset.x.cint 542 | centerFrames = self.config.frameParts.center 543 | 544 | # M;T;C 545 | discard self.dpy.XMoveWindow( 546 | client.frame.maximize, 547 | btnXOffset + ( 548 | if i == 1 and centerFrames[0] == fpTitle: 549 | textXOffset + extent.width div 2 550 | elif i == 1 and centerFrames[0] in {fpClose, fpMinimize}: 551 | btnSize - btnXOffset 552 | elif i == 2 and centerFrames[1] == fpTitle: 553 | extent.width div 2 554 | elif i == 2 and (centerFrames[0] == fpTitle or centerFrames[1] == fpTitle): 555 | extent.width div 2 + btnSize + btnXOffset 556 | elif i == 2: 557 | btnSize * 2 558 | elif i == 0 and centerFrames.len > 2 and (centerFrames[0] == fpTitle or centerFrames[1] == fpTitle): 559 | # meh 560 | -(extent.width div 2) - btnXOffset 561 | elif i == 0: 562 | -btnXOffset * 2 563 | else: 564 | 0 565 | ) + (attr.width div 2), 566 | btnYOffset 567 | ) 568 | 569 | let image = self.XCreateImage(self.config.maximizePaths[buttonState], fp, attr) 570 | 571 | self.XPutImage(image, client.frame.maximize, gc) 572 | of fpMinimize: 573 | 574 | buttonState = if client.frame.minimizeHovered and buttonStateOrig == bsActive: 575 | bsActiveHover 576 | elif client.frame.minimizeHovered and buttonStateOrig == bsInactive: 577 | bsInactiveHover 578 | else: 579 | buttonStateOrig 580 | 581 | if not fileExists self.config.minimizePaths[buttonState]: 582 | buttonState = buttonStateOrig 583 | if not fileExists self.config.minimizePaths[buttonState]: continue 584 | 585 | let image = self.XCreateImage(self.config.minimizePaths[buttonState], fp, attr) 586 | 587 | let 588 | btnSize = self.config.buttonSize.cint 589 | btnXOffset = self.config.buttonOffset.x.cint 590 | textXOffset = self.config.textOffset.x.cint 591 | centerFrames = self.config.frameParts.center 592 | 593 | discard self.dpy.XMoveWindow( 594 | client.frame.minimize, 595 | (if i == 0: -btnXOffset else: btnXOffset) + ( 596 | if (i == 1 and centerFrames[0] == fpTitle and centerFrames.len == 2): 597 | textXOffset + extent.width div 2 598 | elif (i == 1 and centerFrames[0] == fpTitle and 599 | centerFrames.len > 2 and centerFrames[1] in {fpClose, fpMaximize}): 600 | -(extent.width div 2) - btnSize - btnXOffset - textXOffset 601 | elif i == 1 and centerFrames[0] == fpTitle: 602 | (extent.width div 2) + btnXOffset + textXOffset - btnSize 603 | elif (i == 1 and centerFrames.len >= 3 and 604 | centerFrames[0] in {fpMaximize, fpClose} and centerFrames[2] == fpTitle): 605 | # meh 606 | -(extent.width div 2) 607 | elif i == 2 and (centerFrames[1] == fpTitle or centerFrames[0] == fpTitle): 608 | (extent.width div 2) + btnXOffset + textXOffset + btnSize 609 | elif i == 2: 610 | # ez 611 | btnSize * 2 612 | elif (i == 1 and centerFrames.len >= 3 and (centerFrames[0] == fpTitle or centerFrames[1] == fpTitle)): 613 | -(extent.width div 2) + btnSize 614 | elif i == 1 and centerFrames[0] in {fpMaximize, fpClose}: 615 | btnSize - btnXOffset 616 | else: 617 | 0 618 | ) + (attr.width div 2) - ( 619 | if (i == 0 and centerFrames.len > 1 and 620 | centerFrames.find(fpTitle) != -1): 621 | self.config.buttonSize.cint + extent.width div 2 622 | else: 623 | 0), 624 | self.config.buttonOffset.y.cint 625 | ) 626 | 627 | discard self.dpy.XMapWindow client.frame.minimize 628 | 629 | self.XPutImage(image, client.frame.minimize, gc) 630 | 631 | for i, part in self.config.frameParts.right: 632 | 633 | case part: 634 | of fpTitle: 635 | 636 | let 637 | rightFrames = self.config.frameParts.right 638 | textXOffset = self.config.textOffset.x.cint 639 | btnXOffset = self.config.buttonOffset.x.cint 640 | btnSize = self.config.buttonSize.cint 641 | 642 | client.draw.XftDrawStringUtf8( 643 | addr client.color, 644 | self.font, 645 | ( 646 | if (rightFrames.len == 1 or (rightFrames.len == 2 and i == 1 and 647 | rightFrames[0] in {fpClose, fpMaximize, fpMinimize})): 648 | attr.width.cint - (extent.width.cint + textXOffset) 649 | elif (rightFrames.len == 2 and i == 0 and 650 | rightFrames[1] in {fpClose, fpMaximize, fpMinimize}): 651 | attr.width.cint - extent.width.cint - textXOffset - btnXOffset - btnSize 652 | elif (i == 1 and rightFrames.len == 3 and 653 | rightFrames[0] in {fpClose, fpMaximize, fpMinimize}): 654 | attr.width.cint - (extent.width.cint + btnSize + btnXOffset) 655 | elif i == 2: 656 | attr.width.cint - extent.width.cint - textXOffset 657 | elif i == 0 and rightFrames.len == 3: 658 | attr.width.cint - extent.width.cint - btnSize * 2 - btnXOffset * 3 659 | else: 660 | 0 661 | ), 662 | self.config.textOffset.y.cint, 663 | cast[ptr char](cstring client.title), 664 | client.title.len.cint 665 | ) 666 | 667 | of fpClose: 668 | 669 | buttonState = if client.frame.closeHovered and buttonStateOrig == bsActive: 670 | bsActiveHover 671 | elif client.frame.closeHovered and buttonStateOrig == bsInactive: 672 | bsInactiveHover 673 | else: 674 | buttonStateOrig 675 | 676 | if not fileExists self.config.closePaths[buttonState]: 677 | buttonState = buttonStateOrig 678 | if not fileExists self.config.closePaths[buttonState]: continue 679 | 680 | discard self.dpy.XMapWindow client.frame.close 681 | 682 | let image = self.XCreateImage(self.config.closePaths[buttonState], fp, attr) 683 | 684 | let 685 | btnXOffset = self.config.buttonOffset.x.cint 686 | btnYOffset = self.config.buttonOffset.y.cint 687 | textXOffset = self.config.textOffset.x.cint 688 | btnSize = self.config.buttonSize.cint 689 | rightFrames = self.config.frameParts.right 690 | 691 | discard self.dpy.XMoveWindow( 692 | client.frame.close, 693 | (if i == 0: - btnXOffset else: btnXOffset) + 694 | ( 695 | if i == 1 and rightFrames.len == 2 and fpTitle notin rightFrames: 696 | -btnXOffset*2 697 | elif i == 1: 698 | -btnSize - btnXOffset*3 699 | elif i == 0 and rightFrames.len == 2 and rightFrames[1] == fpTitle: 700 | - extent.width - btnSize 701 | elif (i == 0 and rightFrames.len == 2 and rightFrames[1] in {fpMinimize, fpMaximize}): 702 | - btnSize - btnXOffset 703 | elif i == 0 and rightFrames.len == 3 and fpTitle in rightFrames: 704 | - btnSize - textXOffset - btnXOffset - extent.width 705 | elif i == 0 and rightFrames.len == 3: 706 | -btnSize * 2 - btnXOffset*2 707 | elif i == 2: 708 | - btnXOffset*2 709 | else: 710 | 0 711 | ) + attr.width - btnSize - ( 712 | if i == 0 and self.config.frameParts.center.len > 1: 713 | btnSize + extent.width div 2 714 | else: 715 | 0 716 | ), 717 | btnYOffset 718 | ) 719 | 720 | 721 | self.XPutImage(image, client.frame.close, gc) 722 | 723 | of fpMaximize: 724 | 725 | buttonState = if client.frame.maximizeHovered and buttonStateOrig == bsActive: 726 | bsActiveHover 727 | elif client.frame.maximizeHovered and buttonStateOrig == bsInactive: 728 | bsInactiveHover 729 | else: 730 | buttonStateOrig 731 | 732 | if not fileExists self.config.maximizePaths[buttonState]: 733 | buttonState = buttonStateOrig 734 | if not fileExists self.config.maximizePaths[buttonState]: continue 735 | 736 | discard self.dpy.XMapWindow client.frame.maximize 737 | 738 | let 739 | rightFrames = self.config.frameParts.right 740 | btnSize = self.config.buttonSize.cint 741 | btnXOffset = self.config.buttonOffset.x.cint 742 | textOffset = self.config.textOffset.x.cint 743 | 744 | offset = 745 | if i == 1 and rightFrames[0] == fpTitle and rightFrames.len == 3: 746 | - btnSize * 2 - btnXOffset 747 | elif i == 1 and rightFrames[0] == fpTitle: 748 | - btnSize 749 | elif (i == 1 and rightFrames[0] in {fpClose, fpMinimize} and 750 | rightFrames.len == 3 and rightFrames[2] == fpTitle): 751 | - extent.width - btnXOffset * 2 - textOffset 752 | elif i == 1 and fpTitle notin rightFrames and rightFrames.len == 3: 753 | - btnSize - btnXOffset*3 754 | elif i == 1 and rightFrames[0] in {fpClose, fpMinimize}: 755 | - btnXOffset * 3 756 | elif i == 2: 757 | - btnXOffset * 2 758 | elif i == 0 and rightFrames.len == 2 and rightFrames[1] in {fpClose, fpMinimize}: 759 | - btnXOffset * 3 - btnSize 760 | elif i == 0 and rightFrames.len > 2 and rightFrames[1] in {fpClose, fpMinimize} and fpTitle in rightFrames: 761 | - btnXOffset * 3 - btnSize - extent.width 762 | elif i == 0 and rightFrames.len >= 2 and rightFrames[1] == fpTitle: 763 | - extent.width - btnXOffset*2 - btnSize 764 | elif i == 0 and rightFrames.len == 1: 765 | - btnXOffset * 2 766 | elif i == 0 and rightFrames.len == 3: 767 | - btnSize * 2 - btnXOffset * 4 768 | else: 769 | 0 770 | 771 | discard self.dpy.XMoveWindow( 772 | client.frame.maximize, 773 | self.config.buttonOffset.x.cint + offset + attr.width - 774 | self.config.buttonSize.cint, 775 | self.config.buttonOffset.y.cint 776 | ) 777 | 778 | let image = self.XCreateImage(self.config.maximizePaths[buttonState], fp, attr) 779 | 780 | self.XPutImage(image, client.frame.maximize, gc) 781 | of fpMinimize: 782 | 783 | buttonState = if client.frame.minimizeHovered and buttonStateOrig == bsActive: 784 | bsActiveHover 785 | elif client.frame.minimizeHovered and buttonStateOrig == bsInactive: 786 | bsInactiveHover 787 | else: 788 | buttonStateOrig 789 | 790 | if not fileExists self.config.minimizePaths[buttonState]: 791 | buttonState = buttonStateOrig 792 | if not fileExists self.config.minimizePaths[buttonState]: continue 793 | 794 | let 795 | rightFrames = self.config.frameParts.right 796 | btnSize = self.config.buttonSize.cint 797 | btnXOffset = self.config.buttonOffset.x.cint 798 | 799 | offset = 800 | if i == 1 and rightFrames[0] == fpTitle and rightFrames.len == 3: 801 | - btnSize * 2 - btnXOffset 802 | elif i == 1 and rightFrames[0] == fpTitle: 803 | - btnSize 804 | elif (i == 1 and rightFrames[0] in {fpClose, fpMaximize} and 805 | rightFrames.len == 3 and rightFrames[2] == fpTitle): 806 | - extent.width - btnXOffset * 2 807 | elif i == 1 and rightFrames.len == 3: 808 | - btnXOffset*3 - btnSize 809 | elif i == 1: 810 | -btnXOffset*2 811 | elif i == 2: 812 | - btnXOffset * 2 813 | elif i == 0 and rightFrames.len == 2 and rightFrames[1] in {fpClose, fpMaximize}: 814 | - btnXOffset * 4 - btnSize 815 | elif i == 0 and rightFrames.len > 2 and rightFrames[1] in {fpClose, fpMaximize} and fpTitle in rightFrames: 816 | - btnXOffset * 3 - btnSize - extent.width 817 | elif i == 0 and rightFrames.len >= 2 and rightFrames[1] == fpTitle: 818 | - extent.width - btnXOffset*2 - btnSize 819 | elif i == 0 and rightFrames.len == 1: 820 | - btnXOffset * 2 821 | elif i == 0 and rightFrames.len == 3: 822 | # we might be doing the plain old I;M;C that everyone loves lol 823 | (- btnSize * 2) - (btnXOffset * 4) 824 | else: 825 | 0 826 | 827 | discard self.dpy.XMoveWindow( 828 | client.frame.minimize, 829 | self.config.buttonOffset.x.cint + offset + attr.width - 830 | self.config.buttonSize.cint, 831 | self.config.buttonOffset.y.cint 832 | ) 833 | 834 | discard self.dpy.XMapWindow client.frame.minimize 835 | 836 | let image = self.XCreateImage(self.config.minimizePaths[buttonState], fp, attr) 837 | 838 | self.XPutImage(image, client.frame.minimize, gc) 839 | 840 | 841 | proc tileWindows*(self: var Wm): void = 842 | log "Tiling windows" 843 | var clientLen: uint = 0 844 | var master: ptr Client = nil 845 | for i, client in self.clients: 846 | if client.fullscreen: return # causes issues 847 | if client.tags == self.tags and not client.floating: # We only care about clients on the current tag. 848 | if master == nil: # This must be the first client on the tag, otherwise master would not be nil; therefore, we promote it to master. 849 | master = addr self.clients[i] 850 | inc clientLen 851 | if master == nil: return 852 | if clientLen == 0: return # we got nothing to tile. 853 | var scrNo: cint 854 | var scrInfo = cast[ptr UncheckedArray[XineramaScreenInfo]]( 855 | self.dpy.XineramaQueryScreens(addr scrNo)) 856 | # echo cuint scrInfo[0].width shr (if clientLen == 1: 0 else: 1) 857 | let masterWidth = if clientLen == 1: 858 | uint scrInfo[0].width - self.config.struts.left.cint - 859 | self.config.struts.right.cint - cint self.config.borderWidth*2 860 | else: 861 | uint scrInfo[0].width shr 1 - self.config.struts.left.cint - 862 | cint self.config.borderWidth*2 863 | log $masterWidth 864 | discard self.dpy.XMoveResizeWindow(master.frame.window, 865 | cint self.config.struts.left, cint self.config.struts.top, 866 | cuint masterWidth, cuint scrInfo[0].height - 867 | self.config.struts.top.int16 - self.config.struts.bottom.int16 - 868 | cint self.config.borderWidth*2) 869 | discard self.dpy.XResizeWindow(master.window, cuint masterWidth, 870 | cuint scrInfo[0].height - self.config.struts.top.cint - 871 | self.config.struts.bottom.cint - master.frameHeight.cint - 872 | cint self.config.borderWidth*2) 873 | for win in [master.frame.title, master.frame.top]: discard self.dpy.XResizeWindow(win, cuint masterWidth, cuint master.frameHeight) 874 | self.renderTop master[] 875 | # discard self.dpy.XMoveResizeWindow(master.frame.window, cint self.config.struts.left, cint self.config.struts.top, cuint scrInfo[0].width shr (if clientLen == 1: 0 else: 1) - int16(self.config.borderWidth * 2) - self.config.gaps*2 - int16 self.config.struts.right, cuint scrInfo[0].height - int16(self.config.borderWidth * 2) - int16(self.config.struts.top) - int16(self.config.struts.bottom)) # bring the master window up to cover half the screen 876 | # discard self.dpy.XResizeWindow(master.window, cuint scrInfo[0].width shr (if clientLen == 1: 0 else: 1) - int16(self.config.borderWidth*2) - self.config.gaps*2 - int16 self.config.struts.right, cuint scrInfo[0].height - int16(self.config.borderWidth*2) - int16(self.config.frameHeight) - int16(self.config.struts.top) - int16(self.config.struts.bottom)) # bring the master window up to cover half the screen 877 | var irrevelantLen: uint = 0 878 | for i, client in self.clients: 879 | if client.tags != self.tags or client == master[] or client.floating: 880 | inc irrevelantLen 881 | continue 882 | if clientLen == 2: 883 | discard self.dpy.XMoveWindow(client.frame.window, cint scrInfo[ 884 | 0].width shr 1 + self.config.gaps, cint self.config.struts.top) 885 | let w = cuint scrInfo[0].width shr ( 886 | if clientLen == 1: 0 else: 1) - int16(self.config.borderWidth*2) - 887 | self.config.gaps - self.config.struts.right.cint 888 | discard self.dpy.XResizeWindow(client.frame.top, w, cuint self.config.frameHeight) 889 | discard self.dpy.XResizeWindow(client.frame.title, w, cuint self.config.frameHeight) 890 | discard self.dpy.XResizeWindow(client.window, w, cuint scrInfo[ 891 | 0].height - self.config.struts.top.cint - 892 | self.config.struts.bottom.cint - client.frameHeight.cint - 893 | cint self.config.borderWidth*2) 894 | else: 895 | let stackElem = i - int irrevelantLen - 896 | 1 # How many windows are there in the stack? We must subtract 1 to ignore the master window; which we iterate over too. 897 | let yGap = if stackElem != 0: 898 | self.config.gaps 899 | else: 900 | 0 901 | # let subStrut = if stackElem = clientLen 902 | # XXX: the if stackElem == 1: 0 else: self.config.gaps is a huge hack 903 | # and also incorrect behavior; while usually un-noticeable it makes the top window in the stack bigger by the gaps. Fix this!! 904 | let w = cuint scrInfo[0].width shr ( 905 | if clientLen == 1: 0 else: 1) - int16(self.config.borderWidth*2) - 906 | self.config.gaps - self.config.struts.right.cint 907 | discard self.dpy.XResizeWindow(client.frame.top, w, cuint self.config.frameHeight) 908 | discard self.dpy.XResizeWindow(client.frame.title, w, cuint self.config.frameHeight) 909 | discard self.dpy.XMoveWindow(client.frame.window, cint scrInfo[ 910 | 0].width shr 1 + yGap, cint((float(scrInfo[0].height) - ( 911 | self.config.struts.bottom.float + self.config.struts.top.float)) * (( 912 | i - int irrevelantLen) / int clientLen - 1)) + 913 | self.config.struts.top.cint + (if stackElem == 914 | 1: 0 else: self.config.gaps.cint)) 915 | discard self.dpy.XResizeWindow(client.window, w, cuint ((scrInfo[ 916 | 0].height - self.config.struts.bottom.cint - 917 | self.config.struts.top.cint) div int16(clientLen - 1)) - int16( 918 | self.config.borderWidth*2) - int16(client.frameHeight) - ( 919 | if stackElem == 1: 0 else: self.config.gaps)) 920 | self.renderTop self.clients[i] 921 | discard self.dpy.XSync false 922 | discard self.dpy.XFlush 923 | 924 | proc minimizeClient*( 925 | self: var Wm; 926 | client: var Client 927 | ) = 928 | 929 | if not client.minimized: 930 | discard self.dpy.XUnmapWindow(client.frame.window) 931 | client.minimized = true 932 | else: 933 | client.minimized = false 934 | discard self.dpy.XMapWindow(client.frame.window) 935 | 936 | proc maximizeClient*( 937 | self: var Wm; 938 | client: var Client, 939 | force = false, 940 | forceun = false 941 | ) = 942 | 943 | if (not force and client.maximized) or (force and forceun): 944 | if client.beforeGeomMax.isNone: 945 | return 946 | 947 | let geom = get client.beforeGeomMax 948 | 949 | client.maximized = false 950 | 951 | discard self.dpy.XMoveResizeWindow( 952 | client.frame.window, 953 | geom.x.cint, 954 | geom.y.cint, 955 | geom.width.cuint, 956 | geom.height.cuint 957 | ) 958 | 959 | discard self.dpy.XMoveResizeWindow( 960 | client.window, 961 | 0, 962 | cint self.config.frameHeight.cint, 963 | cuint geom.width.cuint, 964 | (geom.height - self.config.frameHeight).cuint 965 | ) 966 | 967 | discard self.dpy.XChangeProperty( 968 | client.window, 969 | self.netAtoms[NetWMState], 970 | XaAtom, 971 | 32, 972 | PropModeReplace, 973 | cast[cstring]([]), 974 | 0 975 | ) 976 | 977 | self.renderTop client 978 | return 979 | 980 | client.maximized = true 981 | 982 | # maximize the provided client 983 | var 984 | scrNo: cint 985 | scrInfo = cast[ptr UncheckedArray[XineramaScreenInfo]]( 986 | self.dpy.XineramaQueryScreens(addr scrNo) 987 | ) 988 | 989 | if scrInfo == nil: 990 | return 991 | 992 | # where the hell is our window at 993 | var attr: XWindowAttributes 994 | discard self.dpy.XGetWindowAttributes(client.frame.window, addr attr) 995 | 996 | client.beforeGeomMax = some Geometry( 997 | x: attr.x, 998 | y: attr.y, 999 | width: attr.width.uint, 1000 | height: attr.height.uint 1001 | ) 1002 | 1003 | var 1004 | x: int 1005 | y: int 1006 | width: uint 1007 | height: uint 1008 | 1009 | if scrNo == 1: 1010 | # 1st monitor, cuz only one 1011 | x = 0 1012 | y = 0 1013 | width = scrInfo[0].width.uint 1014 | height = scrInfo[0].height.uint 1015 | else: 1016 | var 1017 | cumulWidth = 0 1018 | cumulHeight = 0 1019 | 1020 | for i in countup(0, scrNo - 1): 1021 | cumulWidth += scrInfo[i].width 1022 | cumulHeight += scrInfo[i].height 1023 | 1024 | if attr.x <= cumulWidth - attr.width: 1025 | x = scrInfo[i].xOrg 1026 | y = scrInfo[i].yOrg 1027 | width = scrInfo[i].width.uint 1028 | height = scrInfo[i].height.uint 1029 | 1030 | let strut = self.config.struts 1031 | 1032 | discard self.dpy.XMoveWindow( 1033 | client.frame.window, 1034 | (strut.left + x.uint).cint, 1035 | (strut.top + y.uint).cint 1036 | ) 1037 | 1038 | let masterWidth = ( 1039 | scrInfo[0].width - strut.left.cint - strut.right.cint - 1040 | self.config.borderWidth.cint*2 1041 | ).uint 1042 | discard self.dpy.XResizeWindow( 1043 | client.frame.window, 1044 | masterWidth.cuint, 1045 | (height - strut.top - strut.bottom - self.config.borderWidth.cuint*2).cuint 1046 | ) 1047 | 1048 | discard self.dpy.XResizeWindow( 1049 | client.window, 1050 | cuint masterWidth, 1051 | cuint( 1052 | height - strut.top - strut.bottom - 1053 | client.frameHeight - self.config.borderWidth.cuint*2 1054 | ) 1055 | ) 1056 | 1057 | for win in [client.frame.top, client.frame.title]: 1058 | discard self.dpy.XResizeWindow( 1059 | win, 1060 | masterWidth.cuint, 1061 | self.config.frameHeight.cuint 1062 | ) 1063 | 1064 | var states = [NetWMStateMaximizedHorz, NetWMStateMaximizedVert] 1065 | 1066 | discard self.dpy.XChangeProperty( 1067 | client.window, 1068 | self.netAtoms[NetWMState], 1069 | XaAtom, 1070 | 32, 1071 | PropModeReplace, 1072 | cast[cstring](addr states), 1073 | 0 1074 | ) 1075 | 1076 | let conf = XConfigureEvent(theType: ConfigureNotify, display: self.dpy, 1077 | event: client.window, window: client.window, x: (strut.left + x.uint).cint, 1078 | y: (strut.top + y.uint + client.frameHeight.uint).cint, width: masterWidth.cint, 1079 | height: (height - strut.top - strut.bottom - self.config.borderWidth.cuint*2 - client.frameHeight).cint) 1080 | discard self.dpy.XSendEvent(client.window, false, StructureNotifyMask, cast[ 1081 | ptr XEvent](unsafeAddr conf)) 1082 | 1083 | discard self.dpy.XSync false 1084 | 1085 | discard self.dpy.XFlush 1086 | 1087 | self.renderTop client 1088 | 1089 | proc updateClientList*(self: Wm) = 1090 | let wins = self.clients.mapIt(it.window) 1091 | 1092 | # TODO: make NetClientListStacking use actual stacking order. 1093 | 1094 | if wins.len == 0: 1095 | discard self.dpy.XChangeProperty( 1096 | self.root, 1097 | self.netAtoms[NetClientList], 1098 | XaWindow, 1099 | 32, 1100 | PropModeReplace, 1101 | nil, 1102 | 0 1103 | ) 1104 | discard self.dpy.XChangeProperty( 1105 | self.root, 1106 | self.netAtoms[NetClientListStacking], 1107 | XaWindow, 1108 | 32, 1109 | PropModeReplace, 1110 | nil, 1111 | 0 1112 | ) 1113 | return 1114 | 1115 | discard self.dpy.XChangeProperty( 1116 | self.root, 1117 | self.netAtoms[NetClientList], 1118 | XaWindow, 1119 | 32, 1120 | PropModeReplace, 1121 | cast[cstring](unsafeAddr wins[0]), 1122 | wins.len.cint 1123 | ) 1124 | discard self.dpy.XChangeProperty( 1125 | self.root, 1126 | self.netAtoms[NetClientListStacking], 1127 | XaWindow, 1128 | 32, 1129 | PropModeReplace, 1130 | cast[cstring](unsafeAddr wins[0]), 1131 | wins.len.cint 1132 | ) 1133 | 1134 | proc updateTagState*(self: Wm) = 1135 | for client in self.clients: 1136 | for i, tag in client.tags: 1137 | if tag: 1138 | discard self.dpy.XChangeProperty( 1139 | client.window, 1140 | self.netAtoms[NetWMDesktop], 1141 | XaCardinal, 1142 | 32, 1143 | PropModeReplace, 1144 | cast[cstring](unsafeAddr i), 1145 | 1 1146 | ) 1147 | if self.tags[i] and tag: 1148 | discard self.dpy.XMapWindow client.frame.window 1149 | break 1150 | discard self.dpy.XUnmapWindow client.frame.window 1151 | 1152 | proc raiseClient*(self: var Wm, client: var Client) = 1153 | for locClient in self.clients.mitems: 1154 | if locClient != client: 1155 | discard self.dpy.XSetWindowBorder(locClient.frame.window, 1156 | self.config.borderInactivePixel) 1157 | var attr: XWindowAttributes 1158 | discard self.dpy.XGetWindowAttributes(locClient.window, addr attr) 1159 | var color: XftColor 1160 | discard self.dpy.XftColorAllocName(attr.visual, attr.colormap, cstring( 1161 | "#" & self.config.textInactivePixel.toHex 6), addr color) 1162 | locClient.color = color 1163 | for win in [ 1164 | locClient.frame.window, 1165 | locClient.frame.top, 1166 | locClient.frame.title, 1167 | locClient.frame.close, 1168 | locClient.frame.maximize 1169 | ]: 1170 | discard self.dpy.XSetWindowBackground(win, self.config.frameInactivePixel) 1171 | self.renderTop locClient 1172 | discard self.dpy.XSetWindowBorder(client.frame.window, 1173 | self.config.borderActivePixel) 1174 | var attr: XWindowAttributes 1175 | discard self.dpy.XGetWindowAttributes(client.window, addr attr) 1176 | var color: XftColor 1177 | discard self.dpy.XftColorAllocName(attr.visual, attr.colormap, cstring( 1178 | "#" & self.config.textActivePixel.toHex 6), addr color) 1179 | client.color = color 1180 | for win in [ 1181 | client.frame.window, 1182 | client.frame.top, 1183 | client.frame.title, 1184 | client.frame.close, 1185 | client.frame.maximize 1186 | ]: 1187 | discard self.dpy.XSetWindowBackground(win, self.config.frameActivePixel) 1188 | self.renderTop client 1189 | discard self.dpy.XSync false 1190 | discard self.dpy.XFlush 1191 | --------------------------------------------------------------------------------