├── 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 |
82 |
83 |
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 |
95 |
98 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------