├── .gitignore ├── SECURITY.md ├── doWM.desktop ├── exampleConfig ├── autostart.sh └── doWM.yml ├── .github ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── lint.yml │ └── build.yml ├── main.go ├── go.mod ├── CONTRIBUTING.md ├── PKGBUILD ├── LICENSE ├── Makefile ├── .golangci.yml ├── go.sum ├── wm ├── alertwin.go ├── config.go └── window_manager.go ├── CODE_OF_CONDUCT.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | doWM 2 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Currently, only the latest version is maintained 6 | -------------------------------------------------------------------------------- /doWM.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=doWM 3 | Comment=Floating and Tiling window manager for x11 4 | Exec=/usr/local/bin/doWM 5 | TryExec=/usr/local/bin/doWM 6 | Type=Application 7 | -------------------------------------------------------------------------------- /exampleConfig/autostart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dunst & 4 | feh --bg-scale ~/wallpapers/wal.png 5 | 6 | polybar & 7 | 8 | # Pijulius picom 9 | ~/picom/build/src/picom & 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Package main. 2 | package main 3 | 4 | import ( 5 | "log/slog" 6 | 7 | "github.com/BobdaProgrammer/doWM/wm" 8 | ) 9 | 10 | func main() { 11 | WM, err := wm.Create() 12 | if err != nil { 13 | slog.Error("Couldn't initialize window manager", "error:", err) 14 | return 15 | } 16 | defer WM.Close() 17 | 18 | WM.Run() 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/BobdaProgrammer/doWM 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/fsnotify/fsnotify v1.9.0 9 | github.com/goccy/go-yaml v1.18.0 10 | github.com/jezek/xgb v1.1.1 11 | github.com/jezek/xgbutil v0.0.0-20250620170308-517212d66001 12 | github.com/mattn/go-shellwords v1.0.12 13 | ) 14 | 15 | require golang.org/x/sys v0.13.0 // indirect 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # file explainations 2 | There are only a few files to be concerned with: 3 | `main.go` - simply starts the wm 4 | `wm/window_manager.go` - the whole window manager in that one single file (don't worry it is commented) 5 | `exampleConfig/` - this folder contains the example configuration that a user should copy into their .config on first installation 6 | `MakeFile` - the MakeFile to install the WM 7 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | lint: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | - name: setup go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version-file: ./go.mod 16 | - name: lint 17 | uses: golangci/golangci-lint-action@v8 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Setup Go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version-file: ./go.mod 17 | - name: Build 18 | run: | 19 | CGO_ENABLED=0 go build -v . 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: BobdaProgrammer 2 | pkgname=doWM 3 | pkgver=1.0.3 4 | pkgrel=1 5 | pkgdesc="A beautiful tiling and floating x11 window manager" 6 | arch=('x86_64') 7 | url="https://github.com/BobdaProgrammer/doWM" 8 | license=('MIT') 9 | depends=('xorg-server') 10 | makedepends=('go' 'git') 11 | source=("doWM.desktop" 12 | "LICENSE") 13 | sha256sums=('SKIP' 'SKIP') 14 | 15 | build() { 16 | cd "$srcdir" 17 | cd .. 18 | go build -o doWM 19 | } 20 | 21 | package() { 22 | # Install binary 23 | sudo install -Dm755 "../doWM" "/usr/local/bin/doWM" 24 | 25 | # Install .desktop session file 26 | sudo install -Dm644 "doWM.desktop" "/usr/share/xsessions/doWM.desktop" 27 | 28 | # License 29 | sudo install -vDm644 "LICENSE" "/usr/share/licenses/$pkgname" 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 BobdaProgrammer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | BINDIR := $(PREFIX)/bin 3 | XSESSIONS := /usr/share/xsessions 4 | USER_CONFIG := $(HOME)/.config/doWM 5 | 6 | build: 7 | go build -o doWM 8 | @echo "Built successfully!" 9 | 10 | install: 11 | # Install binary locally 12 | mkdir -p $(BINDIR) 13 | sudo install -m755 doWM $(BINDIR)/doWM 14 | 15 | # Install .desktop session file 16 | mkdir -p $(XSESSIONS) 17 | sudo install -m644 doWM.desktop $(XSESSIONS)/doWM.desktop 18 | 19 | 20 | @echo "Installed successfully!" 21 | 22 | config: 23 | @echo "Setting up doWM user config..." 24 | mkdir -p $(USER_CONFIG) 25 | @if [ ! -f $(USER_CONFIG)/autostart.sh ]; then \ 26 | cp exampleConfig/autostart.sh $(USER_CONFIG)/autostart.sh && \ 27 | chmod +x $(USER_CONFIG)/autostart.sh && \ 28 | echo "Installed example autostart.sh"; \ 29 | else \ 30 | echo "autostart.sh already exists, skipping..."; \ 31 | fi 32 | @if [ ! -f $(USER_CONFIG)/doWM.yml ]; then \ 33 | cp exampleConfig/doWM.yml $(USER_CONFIG)/doWM.yml && \ 34 | echo "Installed example doWM.yml"; \ 35 | else \ 36 | echo "doWM.yml already exists, skipping..."; \ 37 | fi 38 | @echo "Config setup complete!" 39 | 40 | .PHONY: all install uninstall 41 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | timeout: 5m 4 | linters: 5 | default: all 6 | disable: 7 | - depguard 8 | - err113 9 | - exhaustruct 10 | - forbidigo 11 | - funcorder 12 | - funlen 13 | - gochecknoglobals 14 | - gocognit 15 | - gocyclo 16 | - godox 17 | - maintidx 18 | - mnd 19 | - nestif 20 | - nlreturn 21 | - noctx 22 | - noinlineerr 23 | - varnamelen 24 | - wrapcheck 25 | - wsl 26 | - wsl_v5 27 | - tagliatelle 28 | - gosec 29 | - lll 30 | - godot 31 | settings: 32 | cyclop: 33 | max-complexity: 16 34 | gosec: 35 | excludes: 36 | - G115 37 | - G204 38 | exclusions: 39 | generated: lax 40 | #presets: 41 | #- comments 42 | #- common-false-positives 43 | #- legacy 44 | #- std-error-handling 45 | paths: 46 | - third_party$ 47 | - builtin$ 48 | - examples$ 49 | formatters: 50 | enable: 51 | - gci 52 | - gofumpt 53 | - goimports 54 | - golines 55 | settings: 56 | golines: 57 | max-len: 120 58 | shorten-comments: true 59 | chain-split-dots: false 60 | exclusions: 61 | generated: lax 62 | paths: 63 | - third_party$ 64 | - builtin$ 65 | - examples$ 66 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= 2 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= 3 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 4 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 5 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 6 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 7 | github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 8 | github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= 9 | github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 10 | github.com/jezek/xgbutil v0.0.0-20250620170308-517212d66001 h1:QzytUteVo3b0NqBGPPdq3GELwd0mpqYUHatYJFWRbZ0= 11 | github.com/jezek/xgbutil v0.0.0-20250620170308-517212d66001/go.mod h1:AHecLyFNy6AN9f/+0AH/h1MI7X1+JL5bmCz4XlVZk7Y= 12 | github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= 13 | github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= 14 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 15 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | -------------------------------------------------------------------------------- /wm/alertwin.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/jezek/xgb" 10 | "github.com/jezek/xgb/xproto" 11 | ) 12 | 13 | var font xproto.Font 14 | var win xproto.Window 15 | var gc xproto.Gcontext 16 | var screen *xproto.ScreenInfo 17 | 18 | func createErrWindow(conn *xgb.Conn) { 19 | // Create a window 20 | border := screen.BlackPixel 21 | colrep, err := xproto.AllocColor(conn, screen.DefaultColormap, 255*257, 0*257, 0*257).Reply() 22 | if err == nil { 23 | border = colrep.Pixel 24 | } else { 25 | log.Println("couldn't make border colour", err) 26 | } 27 | log.Println(colrep.Pixel, colrep.Pixel, colrep.Red, colrep.Green, colrep.Blue) 28 | win, _ = xproto.NewWindowId(conn) 29 | xproto.CreateWindow(conn, 30 | screen.RootDepth, 31 | win, 32 | screen.Root, 33 | 10, 10, screen.WidthInPixels-20, 75, 3, 34 | xproto.WindowClassInputOutput, 35 | screen.RootVisual, 36 | xproto.CwBackPixel|xproto.CwEventMask|xproto.CwBorderPixel, 37 | []uint32{ 38 | screen.BlackPixel, 39 | border, 40 | xproto.EventMaskExposure, 41 | }, 42 | ) 43 | 44 | xproto.ChangeWindowAttributes(conn, win, xproto.CwOverrideRedirect, []uint32{1}) 45 | 46 | // Set the window title 47 | xproto.ChangeProperty(conn, xproto.PropModeReplace, 48 | win, xproto.AtomWmName, xproto.AtomString, 8, 49 | uint32(len("Config Error")), []byte("Config Error"), 50 | ) 51 | 52 | xproto.MapWindow(conn, win) 53 | 54 | // Load a core X font (e.g., "fixed") 55 | font, _ = xproto.NewFontId(conn) 56 | xproto.OpenFont(conn, font, uint16(len("fixed")), "fixed") 57 | 58 | // Create a GC (graphics context) that uses this font 59 | gc, _ = xproto.NewGcontextId(conn) 60 | xproto.CreateGC(conn, gc, xproto.Drawable(win), 61 | xproto.GcForeground|xproto.GcFont, 62 | []uint32{screen.WhitePixel, uint32(font)}, 63 | ) 64 | } 65 | 66 | func errwinclose(conn *xgb.Conn) { 67 | // Clean up 68 | xproto.DestroyWindow(conn, win) 69 | xproto.CloseFont(conn, font) 70 | } 71 | 72 | func (wm *WindowManager) errwin(msg string) { 73 | conn := wm.conn 74 | screen = wm.screen 75 | createErrWindow(conn) 76 | wm.pointerToWindow(wm.root) 77 | log.Println("ERRWIN MESSAGE:", msg, byte(len(msg)), strings.Count(msg, "\n")) 78 | lines := strings.Split(msg, "\n") 79 | var offset int16 = 0 80 | for i := range lines { 81 | err := xproto.ImageText8Checked(conn, byte(len(lines[i])), xproto.Drawable(win), gc, 20, 20+offset, lines[i]).Check() 82 | if err != nil { 83 | log.Println("err showing text", err.Error()) 84 | } 85 | offset += 20 86 | } 87 | } 88 | 89 | func internAtom(conn *xgb.Conn, name string) (xproto.Atom, error) { 90 | reply, err := xproto.InternAtom(conn, true, uint16(len(name)), name).Reply() 91 | if err != nil { 92 | return 0, err 93 | } 94 | return reply.Atom, nil 95 | } 96 | 97 | func parseConfigError(err string) string { 98 | // get line num that error is referencing 99 | sb := strings.Index(err, "[") 100 | se := strings.Index(err, "]") 101 | 102 | str := err[sb+1 : se] 103 | ln := strings.Split(str, ":")[0] 104 | num, _ := strconv.Atoi(ln) 105 | 106 | // the error gives a list of lines before and after the actual error line so we cut out only the line we need and the arrow below that points to where it is on the line 107 | lines := strings.Split(err, "\n") 108 | numlines := lines[1 : len(lines)-1] 109 | final := lines[0] + " " 110 | found := false 111 | for i := range numlines { 112 | if strings.Contains(numlines[i], fmt.Sprint(num, " |")) { 113 | final += numlines[i] + "\n" 114 | found = true 115 | } else if found { 116 | final += strings.Repeat(" ", 25+len(lines[0])) + numlines[i] 117 | found = false 118 | } 119 | } 120 | 121 | return final 122 | } 123 | -------------------------------------------------------------------------------- /wm/config.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "log/slog" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/fsnotify/fsnotify" 11 | "github.com/goccy/go-yaml" 12 | ) 13 | 14 | // Config represents the application configuration. 15 | // tiling window gaps, unfocused/focused window border colors, mod key for all wm actions, window border width, keybinds 16 | type Config struct { 17 | lyts map[int][]Layout 18 | Layouts []map[int][]Layout `yaml:"layouts"` 19 | AutoReload bool `yaml:"auto-reload"` 20 | Gap uint32 `yaml:"gaps"` 21 | Resize uint32 `yaml:"resize-amount"` 22 | OuterGap uint32 `yaml:"outer-gap"` 23 | StartTiling bool `yaml:"default-tiling"` 24 | BorderUnactive uint32 `yaml:"unactive-border-color"` 25 | BorderActive uint32 `yaml:"active-border-color"` 26 | ModKey string `yaml:"mod-key"` 27 | BorderWidth uint32 `yaml:"border-width"` 28 | Keybinds []Keybind `yaml:"keybinds"` 29 | AutoFullscreen bool `yaml:"auto-fullscreen"` 30 | WorkspaceAutoBackAndForth bool `yaml:"workspace-auto-back-and-forth"` 31 | Monitors []MonitorConfig `yaml:"monitors"` 32 | } 33 | 34 | func (wm *WindowManager) configListener() { 35 | // Create new watcher. 36 | watcher, err := fsnotify.NewWatcher() 37 | if err != nil { 38 | slog.Error("Couldn't create config listener", "error:", err) 39 | } 40 | wm.configWatcher = watcher 41 | 42 | home, _ := os.UserHomeDir() 43 | // Start listening for events. 44 | go func() { 45 | for { 46 | select { 47 | case event, ok := <-watcher.Events: 48 | if !ok { 49 | slog.Info("Event error") 50 | return 51 | } 52 | log.Println("event:", event) 53 | if event.Has(fsnotify.Write) && event.Name == filepath.Join(home, ".config", "doWM", "doWM.yml") { 54 | log.Println("modified file:", event.Name) 55 | wm.config = wm.createConfig(true) 56 | if len(wm.config.Monitors) != 0 { 57 | wm.positionMonitors() 58 | } 59 | wm.reload(start) 60 | mMask = wm.mod 61 | } 62 | case err, ok := <-watcher.Errors: 63 | if !ok { 64 | slog.Info("watcher error") 65 | return 66 | } 67 | slog.Error("error:", "error:", err) 68 | } 69 | } 70 | }() 71 | 72 | // Add config path. 73 | err = watcher.Add(filepath.Join(home, ".config", "doWM")) 74 | if err != nil { 75 | slog.Error("Couldn't listen to config file", "error:", err) 76 | } 77 | } 78 | 79 | // read and create config, if certain values, aren't provided, use the default values, if run automatically it will not update config if there are errors. 80 | func (wm *WindowManager) createConfig(auto bool) Config { 81 | var tmp Config 82 | if auto { 83 | tmp = wm.config 84 | } 85 | // Set defaults manually 86 | cfg := Config{ 87 | AutoReload: false, 88 | Gap: 6, 89 | OuterGap: 0, 90 | BorderWidth: 3, 91 | ModKey: "Mod1", 92 | BorderUnactive: 0x8bd5ca, 93 | BorderActive: 0xa6da95, 94 | Keybinds: []Keybind{}, 95 | lyts: createLayouts(), 96 | Layouts: []map[int][]Layout{}, 97 | StartTiling: false, 98 | AutoFullscreen: false, 99 | WorkspaceAutoBackAndForth: false, 100 | Monitors: []MonitorConfig{}, 101 | } 102 | 103 | home, _ := os.UserHomeDir() 104 | f, err := os.ReadFile(filepath.Join(home, ".config", "doWM", "doWM.yml")) 105 | if err != nil { 106 | slog.Error("Couldn't read doWM.yml config file", "error:", err) 107 | if wm.conferror { 108 | errwinclose(wm.conn) 109 | } 110 | errstr := "doWM.yml file doesnt exist in config folder, after fixing, if your keybinds had an error, use mod+shift+r to reload config, otherwise use your reload-config keybind" 111 | if auto { 112 | errstr = "doWM.yml file doesnt exist in config folder" 113 | } 114 | wm.errwin(errstr) 115 | wm.conferror = true 116 | if auto { 117 | return tmp 118 | } else { 119 | return cfg 120 | } 121 | } 122 | 123 | if err := yaml.Unmarshal(f, &cfg); err != nil { 124 | slog.Error("Couldn't parse doWM.yml config file", "error:", err) 125 | if wm.conferror { 126 | errwinclose(wm.conn) 127 | } 128 | errstr := fmt.Sprint("Error in config file: ", parseConfigError(err.Error()), "\n", "after fixing, if your keybinds had an error, use mod+shift+r to reload config, otherwise use your reload-config keybind") 129 | if auto { 130 | errstr = fmt.Sprint("Error in config file: ", parseConfigError(err.Error()), "\n", "using auto reload, so once there are no errors, the config will automatically update") 131 | } 132 | wm.errwin(errstr) 133 | wm.conferror = true 134 | if auto { 135 | return tmp 136 | } else { 137 | return cfg 138 | } 139 | } 140 | 141 | if wm.conferror { 142 | errwinclose(wm.conn) 143 | } 144 | wm.conferror = false 145 | 146 | if len(cfg.Layouts) > 0 { 147 | lyts := map[int][]Layout{} 148 | for _, lyt := range cfg.Layouts { 149 | for key, val := range lyt { 150 | lyts[key] = val 151 | break 152 | } 153 | } 154 | 155 | cfg.lyts = lyts 156 | } 157 | 158 | return cfg 159 | } 160 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

doWM

3 |
4 |

5 |

6 |

7 | 8 | 9 |

10 | 11 | 12 | Issues 13 | 14 | 15 | License 16 | 17 |

18 | 19 | ## Contents 20 | - [Description](#description) 21 | - [Discussions](#discussions) 22 | - [Screenshots](#screenshots) 23 | - [Installation](#installation) 24 | - [Configuration](#configuration) 25 | - [Monitors](#monitors) 26 | - [Star History](#star-history) 27 | - [Progress](#progress) 28 | 29 | ## Description 30 | doWM is a beautiful floating and tiling window manager for X11 designed for efficiency, beauty and performance, completely written in golang. 31 | 32 | ## Discussions 33 | I highly recommend that if you have any questions that aren't issue related, you post something on the [discussions](https://github.com/BobdaProgrammer/doWM/discussions) page, I check it regularly and hopefully I can respond and help you with any questions you have. 34 | 35 | ## screenshots 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | ------------- 48 | 49 | 50 | ## Installation 51 | Currently the best way is to build from source: 52 | 53 | You will want to have go installed 54 | 55 | ```bash 56 | git clone https://github.com/BobdaProgrammer/doWM 57 | cd doWM 58 | go build -o ./doWM 59 | make install config 60 | ``` 61 | `make install config` will install doWM and move an an example config into the config folder, you can just run make install and write your own from scratch but the example config is a good place to start 62 | 63 | ------------- 64 | 65 | This is incase you just run `make install`, it is what you can use to copy the default config into the folder 66 | ``` 67 | mkdir ~/.config/doWM 68 | cp -r ./exampleConfig/* ~/.config/doWM/ 69 | chmod +x ~/.config/doWM/autostart.sh 70 | ``` 71 | 72 | > [!WARNING] 73 | > Make sure to look through the config, to alter startup programs and keybinds so that you can use doWM out of the box 74 | 75 | > [!NOTE] 76 | > To logout, I suggest you use `kill $(pgrep -o doWM)` 77 | 78 | ## Configuration 79 | doWM is configured with `~/.config/doWM/doWM.yml` and `~/.config/doWM/autostart.sh` 80 | simply put any autostart commands in autostart.sh, and then remember to chmod +x it. 81 | the main config file is very simple and is described clearly in the comments on /exampleConfig/doWM.yml 82 | 83 | Colors are to be written in hex format starting with 0x for example white: 0xffffff (could be 0xFFFFFF it is case insensitive) 84 | 85 | You have a few general options: 86 | - outer-gap (gap from edge of tiling space to windows) 87 | - gaps (pixel gaps in tiling) 88 | - resize-amount (the amount of pixels by which a window will increase/decrease with each resize) 89 | - auto-reload (wether the WM should auto reload the config when changes are made, the config will not auto update if errors are present) 90 | - default-tiling (if true, tiling will be enabled on start) 91 | - auto-fullscreen (if true, windows will auto fullscreen when they request) 92 | - mod-key (which key should be used for all wm commands) 93 | - border-width (border width of windows) 94 | - unactive-border-color (the color for the border of unactive windows 95 | - active-border-color (the color for the border of an active window) 96 | - workspace-auto-back-and-forth (whether switching to the current workspace number switches to the previous workspace, default is false) 97 | 98 | The multi monitor system is fairly simple, you don't need to add it to your config but you can if you want to specify positions for your monitors. The only rule for the positions is they cannot be negative, this means that the monitor that is the highest has a Y of 0, where as ones lower than that could be something like 1080, same with X, the one on the furthest left would be 0, then to the right of that could be 1920. Here is an example of two monitors, one it above and to the right and the other below and to the left: 99 | ```yml 100 | monitors: 101 | # highest and to the right 102 | - x: 960 103 | y: 0 104 | 105 | # furthest to the left and below 106 | - x: 0 107 | y: 1080 108 | ``` 109 | To move a window between monitors, just drag it between and it will follow 110 | 111 | Although there are some default tiling layouts which will serve you well, you can easily customize your tiling layouts. The system works quite simply, in the `layouts:` you would have a list of each of the window numbers you want to have a layout/s for, for example 1 through 5 so you would have layouts for up to 5 windows in a workspace, any more than that and the window would just be placed above the windows to be moved to a separate workspace or closed. For each window number, you specify `- windows:` for each layout, in side of windows you would have a list of windows, represented like this: 112 | ```yml 113 | - x: 0.0 # the X percentage in the tiling space, 0.5 would have the top left corner halfway through the width of the tiling space 114 | y: 0.0 # the Y percentage in the tiling space 115 | width: 1.0 # The width percentage in the tiling space, 1.0 is the whole width 116 | height: 1.0 # The height percentage in the tiling space 117 | ``` 118 | In the example above, it would have one window that takes up the whole space since its top left corner is at (0, 0) and its width and height are the full tiling space. 119 | Below is an example of a simple layout config for 1 and 2 windows: 120 | ```yml 121 | layouts: 122 | - 1: 123 | - windows: # 1 window - takes up whole space 124 | - x: 0.0 125 | y: 0.0 126 | width: 1.0 127 | height: 1.0 128 | 129 | 130 | - 2: 131 | - windows: # 2 windows - 1st layout is split halfway - 2nd layout is for one being 2/3 of the width, the other 1/3 132 | - x: 0.0 133 | y: 0.0 134 | width: 0.5 135 | height: 1.0 136 | - x: 0.5 137 | y: 0.0 138 | width: 0.5 139 | height: 1.0 140 | - windows: 141 | - x: 0.0 142 | y: 0.0 143 | width: 0.66666666 144 | height: 1.0 145 | - x: 0.6666666666666 146 | y: 0.0 147 | width: 0.33333333333333 148 | height: 1 149 | ``` 150 | There is much longer one that goes up to 10 windows in the example config that you can check out 151 | 152 | 153 | there are also some default keybinds like modkey+(0-9) to switch workspaces and with a shift to move a window between workspaces, but you can also set your own keybinds 154 | 155 | each keybind either executes a command or plays a role in the wm. Here are all the roles: 156 | - quit (close window) 157 | - force-quit (force close window) 158 | - toggle-tiling (toggle tiling mode) 159 | - toggle-fullscreen (toggle fullscreen on window) 160 | - swap-window-left (shift window left in tiling mode) 161 | - swap-window-right (shift window right in tiling mode) 162 | - focus-window-left (focus the window to the left in tiling mode) 163 | - focus-window-right (focus the window to the right in tiling mode) 164 | - reload-config (reload doWM.yml) 165 | - increase-gap (increase gap between windows in tiling temporarily - reset next session) 166 | - decrease-gap (decrease gap between windows in tiling, also temporary) 167 | - detach-tiling (separate a workspace from global tiling - e.g that workspace could be floating with rest tiling - it is also toggling, so if detached it will re-attach) 168 | - next-layout (switch to the next layout for the current window number) 169 | - resize-x-scale-up (increases the width of the current window) 170 | - resize-x-scale-down (decreases the width of the current window) 171 | - resize-y-scale-up (increases the height of the current window) 172 | - resize-y-scale-down (decreases height of current window) 173 | - move-x-left (moves window to the left) 174 | - move-x-right (moves window to the right) 175 | - move-y-up (moves window up) 176 | - move-y-down (moves window down) 177 | 178 | each keybind also has a key and a shift option, key is the character of the key (can also be things like "f1" "space" or "return") and shift is a bool for if shift should be pressed or not to register. 179 | 180 | for example: 181 | ```yml 182 | # When mod + t is pressed then open kitty 183 | - key: "t" 184 | shift: false 185 | exec: "kitty" 186 | # When mod + shift + right arrow is pressed then switch the focused window to the right 187 | - key: "right" 188 | shift: true 189 | role: "swap-window-right" 190 | ``` 191 | 192 | For an example config, look at [/exampleConfig](https://github.com/BobdaProgrammer/doWM/tree/main/exampleConfig) 193 | 194 | ## Monitors 195 | doWM supports multiple monitors and you can see how to configure them in the configuration section. Each monitor has 10 workspaces and are independent of the other monitors unless you drag a window between them, it will then move it to the other monitor. 196 | 197 | ## Star History 198 | 199 | [![Star History Chart](https://api.star-history.com/svg?repos=BobdaProgrammer/doWM&type=Timeline)](https://www.star-history.com/#BobdaProgrammer/doWM&Timeline) 200 | 201 | ## progress 202 | - [x] move/resize 203 | - [x] workspaces 204 | - [x] move window between workspaces 205 | - [x] focus on hover 206 | - [x] configuration 207 | - [x] auto reloading config 208 | - [x] keybinds 209 | - [x] floating 210 | - [x] tiling 211 | - [x] swap windows in tiling 212 | - [x] change focus in tiling 213 | - [x] many layouts 214 | - [x] bar support 215 | - [x] fullscreen 216 | - [x] startup commands 217 | - [x] picom support 218 | - [x] multi monitor support 219 | - [ ] auto update monitors if new one is plugged in 220 | -------------------------------------------------------------------------------- /exampleConfig/doWM.yml: -------------------------------------------------------------------------------- 1 | # gap from edge of tiling space to windows 2 | outer-gap: 0 3 | 4 | # gaps for tiling windows 5 | gaps: 10 6 | 7 | # the amount the window will be resized with the keybinds 8 | resize-amount: 5 9 | 10 | # wether the WM should auto reload the config when changes are made, (if there is an error in the config, it will keep the old config and not auto update) 11 | auto-reload: true 12 | 13 | # the mod key used for all window manager actions 14 | # Mod1 = alt 15 | # Mod4 = windows/super key 16 | # those are the usual although all 1-5 are supported 17 | mod-key: "Mod4" 18 | 19 | # if set to true, tiling will be enabled at the start instead of having to toggle tiling 20 | default-tiling: true 21 | 22 | # if set to true, if a window asks for fullscreen, the wm will auto fullscreen it instead of the user doing it manually 23 | auto-fullscreen: false 24 | 25 | border-width: 2 26 | 27 | # border color for unfocused windows 28 | unactive-border-color: 0xf5a97f 29 | 30 | # border color for focused windows 31 | active-border-color: 0xed8796 32 | 33 | # define positions of monitors, 0 on the Y is the highest up and 0 on the X is the furthest to the left 34 | # 35 | # monitors: 36 | # # highest and to the right 37 | # - x: 960 38 | # y: 0 39 | # 40 | # # furthest to the left and below 41 | # - x: 0 42 | # y: 1080 43 | 44 | # Completely new layout system, you can have multiple layouts for different numbers of windows, if a layout isnt supported, it will just add a window ontop of the rest that are tiled to be moved somewhere else 45 | # layouts are specified like: 46 | # - : 47 | # - windows: # the first layout for that window number 48 | # - x: # x percentage 49 | # y: # y percentage 50 | # width: # width percentage 51 | # height # height percentage 52 | # - x: 53 | # y: 54 | # width: 55 | # height: 56 | # 57 | # # and so on... 58 | # - windows: # the next layout for that window number 59 | # - : 60 | # 61 | layouts: 62 | - 1: 63 | - windows: # 1 window - takes up whole space 64 | - x: 0.0 65 | y: 0.0 66 | width: 1.0 67 | height: 1.0 68 | 69 | 70 | - 2: 71 | - windows: # 2 windows - 1st layout is split halfway - 2nd layout is for one being 2/3 of the width, the other 1/3 72 | - x: 0.0 73 | y: 0.0 74 | width: 0.5 75 | height: 1.0 76 | - x: 0.5 77 | y: 0.0 78 | width: 0.5 79 | height: 1.0 80 | - windows: 81 | - x: 0.0 82 | y: 0.0 83 | width: 0.66666666 84 | height: 1.0 85 | - x: 0.6666666666666 86 | y: 0.0 87 | width: 0.33333333333333 88 | height: 1 89 | 90 | 91 | - 3: 92 | - windows: # 3 windows - 1st layout is for 1 window halfway width and full height and the other two split over the other half - 2nd layout is split 1/3 width each - 2nd layout 93 | - x: 0.0 94 | y: 0.0 95 | width: 0.5 96 | height: 1.0 97 | - x: 0.5 98 | y: 0.0 99 | width: 0.5 100 | height: 0.5 101 | - x: 0.5 102 | y: 0.5 103 | width: 0.5 104 | height: 0.5 105 | - windows: 106 | - x: 0.0 107 | y: 0.0 108 | width: 0.333333333 109 | height: 1.0 110 | - x: 0.33333333333333 111 | y: 0.0 112 | width: 0.333333333 113 | height: 1.0 114 | - x: 0.6666666666666 115 | y: 0.0 116 | width: 0.333333333 117 | height: 1.0 118 | - windows: 119 | - x: 0.0 120 | y: 0.0 121 | height: 0.333333333 122 | width: 1.0 123 | - y: 0.33333333333333 124 | x: 0.0 125 | height: 0.333333333 126 | width: 1.0 127 | - y: 0.6666666666666 128 | x: 0.0 129 | height: 0.333333333 130 | width: 1.0 131 | - windows: 132 | - x: 0.0 133 | y: 0.0 134 | width: 0.333333333 135 | height: 0.5 136 | - x: 0.333333333 137 | y: 0.00 138 | width: 0.66666666666 139 | height: 0.5 140 | - x: 0.00 141 | y: 0.5 142 | width: 1.0 143 | height: 0.5 144 | - 4: 145 | - windows: # 4 windows 146 | - x: 0.0 147 | y: 0.0 148 | width: 0.5 149 | height: 0.5 150 | - x: 0.5 151 | y: 0.0 152 | width: 0.5 153 | height: 0.5 154 | - x: 0.0 155 | y: 0.5 156 | width: 0.5 157 | height: 0.5 158 | - x: 0.5 159 | y: 0.5 160 | width: 0.5 161 | height: 0.5 162 | - windows: 163 | - x: 0.0 164 | y: 0.0 165 | width: 0.25 166 | height: 1.0 167 | - x: 0.25 168 | y: 0.0 169 | width: 0.25 170 | height: 1.0 171 | - x: 0.5 172 | y: 0.0 173 | width: 0.25 174 | height: 1.0 175 | - x: 0.75 176 | y: 0.0 177 | width: 0.25 178 | height: 1.0 179 | - windows: # 4 windows 180 | - x: 0.0 181 | y: 0.0 182 | width: 0.5 183 | height: 1.0 184 | - x: 0.5 185 | y: 0.0 186 | width: 0.5 187 | height: 0.33333333 188 | - x: 0.5 189 | y: 0.33333333 190 | width: 0.5 191 | height: 0.33333333 192 | - x: 0.5 193 | y: 0.66666666 194 | width: 0.5 195 | height: 0.333333333 196 | - windows: 197 | - x: 0.0 198 | y: 0.0 199 | width: 0.5 200 | height: 0.6666666 201 | - x: 0.5 202 | y: 0.0 203 | width: 0.5 204 | height: 0.3333333 205 | - x: 0.5 206 | y: 0.333333333 207 | width: 0.5 208 | height: 0.33333333 209 | - x: 0.00 210 | y: 0.66666666666 211 | width: 1.0 212 | height: 0.333333333 213 | 214 | - 5: 215 | - windows: # 5 windows 216 | - x: 0.0 217 | y: 0.0 218 | width: 0.3333333333 219 | height: 0.5 220 | - x: 0.33333333333 221 | y: 0.0 222 | width: 0.3333333333 223 | height: 0.5 224 | - x: 0.66666666666 225 | y: 0.0 226 | width: 0.3333333333 227 | height: 0.5 228 | - x: 0.0 229 | y: 0.5 230 | width: 0.5 231 | height: 0.5 232 | - x: 0.5 233 | y: 0.5 234 | width: 0.5 235 | height: 0.5 236 | 237 | - 6: 238 | - windows: # 6 windows 239 | - x: 0.0 240 | y: 0.0 241 | width: 0.3333333333 242 | height: 0.5 243 | - x: 0.33333333333 244 | y: 0.0 245 | width: 0.3333333333 246 | height: 0.5 247 | - x: 0.66666666666 248 | y: 0.0 249 | width: 0.3333333333 250 | height: 0.5 251 | - x: 0.0 252 | y: 0.5 253 | width: 0.3333333333 254 | height: 0.5 255 | - x: 0.33333333333 256 | y: 0.5 257 | width: 0.3333333333 258 | height: 0.5 259 | - x: 0.66666666666 260 | y: 0.5 261 | width: 0.3333333333 262 | height: 0.5 263 | 264 | - 7: 265 | - windows: # 7 windows: 266 | - x: 0.0 267 | y: 0.0 268 | width: 0.5 269 | height: 0.3333333333 270 | - x: 0.0 271 | y: 0.33333333333333 272 | width: 0.5 273 | height: 0.3333333333 274 | - x: 0.0 275 | y: 0.6666666666666 276 | width: 0.5 277 | height: 0.3333333333 278 | - x: 0.5 279 | y: 0.0 280 | width: 0.5 281 | height: 0.25 282 | - x: 0.5 283 | y: 0.25 284 | width: 0.5 285 | height: 0.25 286 | - x: 0.5 287 | y: 0.5 288 | width: 0.5 289 | height: 0.25 290 | - x: 0.5 291 | y: 0.75 292 | width: 0.5 293 | height: 0.25 294 | 295 | - 8: 296 | - windows: # 8 windows: 297 | - x: 0.0 298 | y: 0.0 299 | width: 0.5 300 | height: 0.25 301 | - x: 0.0 302 | y: 0.25 303 | width: 0.5 304 | height: 0.25 305 | - x: 0.0 306 | y: 0.5 307 | width: 0.5 308 | height: 0.25 309 | - x: 0.0 310 | y: 0.75 311 | width: 0.5 312 | height: 0.25 313 | - x: 0.5 314 | y: 0.0 315 | width: 0.5 316 | height: 0.25 317 | - x: 0.5 318 | y: 0.25 319 | width: 0.5 320 | height: 0.25 321 | - x: 0.5 322 | y: 0.5 323 | width: 0.5 324 | height: 0.25 325 | - x: 0.5 326 | y: 0.75 327 | width: 0.5 328 | height: 0.25 329 | - 9: 330 | - windows: # 10 windows: 331 | - x: 0.0 332 | y: 0.0 333 | width: 0.25 334 | height: 1.0 335 | - x: 0.25 336 | y: 0.0 337 | width: 0.25 338 | height: 0.25 339 | - x: 0.25 340 | y: 0.25 341 | width: 0.25 342 | height: 0.25 343 | - x: 0.25 344 | y: 0.5 345 | width: 0.25 346 | height: 0.25 347 | - x: 0.25 348 | y: 0.75 349 | width: 0.25 350 | height: 0.25 351 | - x: 0.5 352 | y: 0.0 353 | width: 0.5 354 | height: 0.25 355 | - x: 0.5 356 | y: 0.25 357 | width: 0.5 358 | height: 0.25 359 | - x: 0.5 360 | y: 0.5 361 | width: 0.5 362 | height: 0.25 363 | - x: 0.5 364 | y: 0.75 365 | width: 0.5 366 | height: 0.25 367 | - 10: 368 | - windows: # 10 windows: 369 | - x: 0.0 370 | y: 0.0 371 | width: 0.25 372 | height: 1.0 373 | - x: 0.25 374 | y: 0.0 375 | width: 0.25 376 | height: 0.25 377 | - x: 0.25 378 | y: 0.25 379 | width: 0.25 380 | height: 0.25 381 | - x: 0.25 382 | y: 0.5 383 | width: 0.25 384 | height: 0.25 385 | - x: 0.25 386 | y: 0.75 387 | width: 0.25 388 | height: 0.25 389 | - x: 0.5 390 | y: 0.0 391 | width: 0.25 392 | height: 0.25 393 | - x: 0.5 394 | y: 0.25 395 | width: 0.25 396 | height: 0.25 397 | - x: 0.5 398 | y: 0.5 399 | width: 0.25 400 | height: 0.25 401 | - x: 0.5 402 | y: 0.75 403 | width: 0.25 404 | height: 0.25 405 | - x: 0.75 406 | y: 0.0 407 | width: 0.25 408 | height: 1.0 409 | 410 | # keybindings 411 | # follow this pattern 412 | # a key (can't be multiple) in lowercase 413 | # wether the kebybind is with shift 414 | # either a command to exec or a role in the window manager 415 | # roles are: 416 | # - quit = gracefully close a window 417 | # - force-quit = forcefully close window 418 | # - toggle-tiling = toggle tiling on and off 419 | # - toggle-fullscreen = toggle fullscreen on window 420 | # - swap-window-left = swap a window with the previous window in a tiling layout 421 | # - swap-window-right = swap a window with the next window in a tiling layout 422 | # - focus-window-left = focus the previous window in a tiling layout 423 | # - focus-window-right = focus the next window in a tiling layout 424 | # - reload-config = reload the config (not autostart.sh) 425 | # - increase-gap = increase the gap between tiling windows (not perminent so resets next session) 426 | # - decrease gap = decrease the gap between tiling windows (also not perminent) 427 | # - detach-tiling = make a workspace's tiling seperate from the global tiling, so it could be floating while the other workspaces are tiling, this is toggling, so if it is detached it will attach, otherwise it will detach 428 | # - next-layout = switch to the next layout for the current window number 429 | keybinds: 430 | - key: "w" 431 | shift: false 432 | exec: "rofi -show drun" 433 | - key: "t" 434 | shift: false 435 | exec: "kitty" 436 | - key: "e" 437 | shift: false 438 | exec: "thunar" 439 | - key: "f1" 440 | shift: false 441 | exec: "pactl set-sink-mute @DEFAULT_SINK@ toggle'" 442 | - key: "f2" 443 | shift: false 444 | exec: "sh -c 'pactl set-sink-volume @DEFAULT_SINK@ -5% && eww update VOLUME=$(~/.config/eww/scripts/volume.sh --get)&& ~/.config/eww/openers/volumeBar.sh'" 445 | - key: "f3" 446 | shift: false 447 | exec: "sh -c 'pactl set-sink-volume @DEFAULT_SINK@ +5% && eww update VOLUME=$(~/.config/eww/scripts/volume.sh --get)&& ~/.config/eww/openers/volumeBar.sh'" 448 | - key: "f4" 449 | shift: false 450 | exec: "sh -c 'brightnessctl set 5- && eww update BRIGHTNESS=$(~/.config/eww/scripts/brightness.sh --get) && eww update BRIGHTNESSPER=$(~/.config/eww/scripts/brightness.sh --get-percent) && /home/sam/.config/eww/openers/brightnessBar.sh'" 451 | - key: "f5" 452 | shift: false 453 | exec: "sh -c 'brightnessctl set +5 && eww update BRIGHTNESS=$(~/.config/eww/scripts/brightness.sh --get) && eww update BRIGHTNESSPER=$(~/.config/eww/scripts/brightness.sh --get-percent) && /home/sam/.config/eww/openers/brightnessBar.sh'" 454 | - key: "c" 455 | shift: false 456 | role: "quit" 457 | - key: "s" 458 | shift: false 459 | exec: "flameshot gui" 460 | - key: "c" 461 | shift: true 462 | role: "force-quit" 463 | - key: "f" 464 | shift: false 465 | role: "toggle-fullscreen" 466 | - key: "v" 467 | shift: false 468 | role: "toggle-tiling" 469 | - key: "v" 470 | shift: true 471 | role: "detach-tiling" 472 | - key: "left" 473 | shift: true 474 | role: "swap-window-left" 475 | - key: "right" 476 | shift: true 477 | role: "swap-window-right" 478 | - key: "left" 479 | shift: false 480 | role: "focus-window-left" 481 | - key: "right" 482 | shift: false 483 | role: "focus-window-right" 484 | - key: "r" 485 | shift: true 486 | role: "reload-config" 487 | - key: "i" 488 | shift: false 489 | role: "next-layout" 490 | - key: "up" 491 | shift: false 492 | role: "increase-gap" 493 | - key: "down" 494 | shift: false 495 | role: "decrease-gap" 496 | - key: "h" 497 | shift: false 498 | role: "resize-x-scale-down" 499 | - key: "j" 500 | shift: false 501 | role: "resize-y-scale-down" 502 | - key: "k" 503 | shift: false 504 | role: "resize-y-scale-up" 505 | - key: "l" 506 | shift: false 507 | role: "resize-x-scale-up" 508 | - key: "h" 509 | shift: true 510 | role: "move-x-left" 511 | - key: "j" 512 | shift: true 513 | role: "move-y-down" 514 | - key: "k" 515 | shift: true 516 | role: "move-y-up" 517 | - key: "l" 518 | shift: true 519 | role: "move-x-right" 520 | -------------------------------------------------------------------------------- /wm/window_manager.go: -------------------------------------------------------------------------------- 1 | // Package wm provides an X11 window manager. 2 | package wm 3 | 4 | import ( 5 | "bytes" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "log/slog" 10 | "math" 11 | "os" 12 | "os/exec" 13 | "os/user" 14 | "path/filepath" 15 | "strconv" 16 | 17 | "github.com/fsnotify/fsnotify" 18 | "github.com/jezek/xgb" 19 | "github.com/jezek/xgb/randr" 20 | "github.com/jezek/xgb/xproto" 21 | "github.com/jezek/xgbutil" 22 | "github.com/jezek/xgbutil/keybind" 23 | "github.com/mattn/go-shellwords" 24 | ) 25 | 26 | // for moving and resizing, basically the window that will be moved/resized 27 | var start xproto.ButtonPressEvent 28 | var attr *xproto.GetGeometryReply 29 | 30 | // Mod key mask 31 | var mMask uint16 32 | 33 | // XUtil represents the state of xgbutil. 34 | var XUtil *xgbutil.XUtil 35 | 36 | // Colormap represents the default colormap of the screen 37 | var Colormap xproto.Colormap 38 | 39 | // MonitorConfig is the position of monitors defined in the user config 40 | type MonitorConfig struct { 41 | X int `yaml:"x"` 42 | Y int `yaml:"y"` 43 | } 44 | 45 | // Keybind represents a keybind: keycode, the letter of the key, if shift should be pressed, 46 | // command (can be empty), role in wm (can be empty). 47 | type Keybind struct { 48 | Keycode uint32 49 | Key string `yaml:"key"` 50 | Shift bool `yaml:"shift"` 51 | Exec string `yaml:"exec"` 52 | Role string `yaml:"role"` 53 | } 54 | 55 | // LayoutWindow represents where a window is on a layout (dynamic by using percentages). 56 | type LayoutWindow struct { 57 | WidthPercentage float64 `yaml:"width"` 58 | HeightPercentage float64 `yaml:"height"` 59 | XPercentage float64 `yaml:"x"` 60 | YPercentage float64 `yaml:"y"` 61 | } 62 | 63 | // Layout represents a tiling layout of windows. 64 | type Layout struct { 65 | Windows []LayoutWindow `yaml:"windows"` 66 | } 67 | 68 | // RLayoutWindow represents a resized layout window. 69 | type RLayoutWindow struct { 70 | Width, Height, X, Y uint16 71 | } 72 | 73 | // ResizeLayout represents a resized layout. 74 | type ResizeLayout struct { 75 | Windows []RLayoutWindow 76 | } 77 | 78 | // Window represents a basic window struct. 79 | type Window struct { 80 | id xproto.Window 81 | X, Y int 82 | Width, Height int 83 | Fullscreen bool 84 | Client xproto.Window 85 | } 86 | 87 | // Space represents an area on the screen. 88 | type Space struct { 89 | X, Y int 90 | Width, Height int 91 | } 92 | 93 | // Workspace is a map from client windows to the frame, the reverse of that, window IDs to windows, and if that 94 | // workspace is tiling or not (in case it needs to update to sync with the main wm). 95 | type Workspace struct { 96 | tiling bool 97 | layoutIndex int 98 | detachTiling bool 99 | windowList []*Window 100 | resized bool 101 | resizedLayout ResizeLayout 102 | } 103 | 104 | // Monitor is representing a monitor which effectively houses its own workspaces and windows etc. the monitor is 105 | // actually just a space in the root window 106 | type Monitor struct { 107 | X int16 108 | Y int16 109 | Width uint16 110 | Height uint16 111 | workspaceIndex int 112 | lastWorkspaceIndex int 113 | Workspaces []Workspace 114 | CurrWorkspace *Workspace 115 | TilingSpace Space 116 | layoutIndex int 117 | tiling bool 118 | crtc randr.Crtc 119 | } 120 | 121 | // WindowManager represents the connection, root window, width and height of screen, workspaces, 122 | // the current workspace index,the current workspace, atoms for EMWH, if the wm is tiling, the space for tiling 123 | // windows to be, the different tiling layouts, the wm config, the mod key. 124 | type WindowManager struct { 125 | conferror bool 126 | configWatcher *fsnotify.Watcher 127 | screen *xproto.ScreenInfo 128 | conn *xgb.Conn 129 | root xproto.Window 130 | atoms map[string]xproto.Atom 131 | monitors []Monitor 132 | currMonitor *Monitor 133 | config Config 134 | mod uint16 135 | windows map[xproto.Window]*Window 136 | crtcToMonitor map[randr.Crtc]*Monitor 137 | } 138 | 139 | func (wm *WindowManager) cursor() { //nolint:unused 140 | // Load the default cursor ("left_ptr") from the theme 141 | cursorFont, err := xproto.NewFontId(wm.conn) 142 | if err != nil { 143 | slog.Error("Failed to allocate font ID:", "error:", err) 144 | return 145 | } 146 | 147 | cursorID, _ := xproto.NewCursorId(wm.conn) 148 | 149 | // Open the cursor font 150 | err = xproto.OpenFontChecked(wm.conn, cursorFont, uint16(len("cursor")), "cursor").Check() 151 | if err != nil { 152 | slog.Error("Failed to open cursor font:", "error:", err) 153 | return 154 | } 155 | 156 | // Create a cursor from the font - 68 = "left_ptr" in the standard cursor font 157 | // You can look up other cursor IDs from X11 cursor font tables if you want other styles 158 | err = xproto.CreateGlyphCursorChecked( 159 | wm.conn, cursorID, cursorFont, cursorFont, 160 | 68, 69, // source and mask glyph (left_ptr) 161 | 255, 255, 255, // foreground RGB 162 | 0, 0, 0). // background RGB 163 | Check() 164 | if err != nil { 165 | slog.Error("Failed to create cursor: %v", "error:", err) 166 | } 167 | 168 | // Set the cursor on the root window 169 | err = xproto.ChangeWindowAttributesChecked( 170 | wm.conn, wm.root, xproto.CwCursor, []uint32{uint32(cursorID)}).Check() 171 | if err != nil { 172 | slog.Error("Failed to set cursor on root window: %v", "error:", err) 173 | } 174 | } 175 | 176 | // creates simple tiling layouts for 1-4 windows, any more is simply left on top to be moved 177 | func createLayouts() map[int][]Layout { 178 | return map[int][]Layout{ 179 | 1: {{ 180 | Windows: []LayoutWindow{ 181 | { 182 | XPercentage: 0, 183 | YPercentage: 0, 184 | WidthPercentage: 1, 185 | HeightPercentage: 1, 186 | }, 187 | }, 188 | }}, 189 | 2: {{ 190 | Windows: []LayoutWindow{ 191 | { 192 | XPercentage: 0, 193 | YPercentage: 0, 194 | WidthPercentage: 0.5, 195 | HeightPercentage: 1, 196 | }, 197 | { 198 | XPercentage: 0.5, 199 | YPercentage: 0, 200 | WidthPercentage: 0.5, 201 | HeightPercentage: 1, 202 | }, 203 | }, 204 | }}, 205 | 3: {{ 206 | Windows: []LayoutWindow{ 207 | { 208 | XPercentage: 0.0, 209 | YPercentage: 0, 210 | WidthPercentage: 1.0 / 3, 211 | HeightPercentage: 1, 212 | }, 213 | { 214 | XPercentage: 1.0 / 3, 215 | YPercentage: 0, 216 | WidthPercentage: 1.0 / 3, 217 | HeightPercentage: 1, 218 | }, 219 | { 220 | XPercentage: 2.0 / 3, 221 | YPercentage: 0, 222 | WidthPercentage: 1.0 / 3, 223 | HeightPercentage: 1, 224 | }, 225 | }, 226 | }}, 227 | 4: {{ 228 | Windows: []LayoutWindow{ 229 | { 230 | XPercentage: 0, 231 | YPercentage: 0, 232 | WidthPercentage: 0.5, 233 | HeightPercentage: 0.5, 234 | }, 235 | { 236 | XPercentage: 0.5, 237 | YPercentage: 0, 238 | WidthPercentage: 0.5, 239 | HeightPercentage: 0.5, 240 | }, 241 | { 242 | XPercentage: 0, 243 | YPercentage: 0.5, 244 | WidthPercentage: 0.5, 245 | HeightPercentage: 0.5, 246 | }, 247 | { 248 | XPercentage: 0.5, 249 | YPercentage: 0.5, 250 | WidthPercentage: 0.5, 251 | HeightPercentage: 0.5, 252 | }, 253 | }, 254 | }}, 255 | } 256 | } 257 | 258 | // Create creates the X connection and get the root window, create workspaces and create window manager struct. 259 | func Create() (*WindowManager, error) { 260 | // establish connection 261 | X, err := xgb.NewConn() 262 | if err != nil { 263 | slog.Error("Couldn't open X display", "error", err) 264 | return nil, fmt.Errorf("could not open X display %w", err) 265 | } 266 | 267 | // Every extension must be initialized before it can be used. 268 | err = randr.Init(X) 269 | if err != nil { 270 | slog.Error("Couldn't init", "error:", err) 271 | return nil, fmt.Errorf("could not use randr for monitors %w", err) 272 | } 273 | 274 | // get xgbutil connection aswell for keybinds 275 | XUtil, err = xgbutil.NewConnXgb(X) 276 | if err != nil { 277 | return nil, fmt.Errorf("could not create xgbutil connection: %w", err) 278 | } 279 | 280 | keybind.Initialize(XUtil) 281 | 282 | // get root and dimensions of screen 283 | setup := xproto.Setup(X) 284 | screen := setup.DefaultScreen(X) 285 | Colormap = screen.DefaultColormap 286 | root := screen.Root 287 | 288 | // Gets the current screen resources. Screen resources contains a list 289 | // of names, crtcs, outputs and modes, among other things. 290 | resources, err := randr.GetScreenResources(X, root).Reply() 291 | if err != nil { 292 | slog.Error("Couldn't get resources", "error:", err) 293 | return nil, fmt.Errorf("could not get resources %w", err) 294 | } 295 | 296 | monitors := []Monitor{} 297 | crtcToMonitor := map[randr.Crtc]*Monitor{} 298 | 299 | for i, crtc := range resources.Crtcs { 300 | info, err := randr.GetCrtcInfo(X, crtc, 0).Reply() 301 | if err != nil { 302 | slog.Error("Couldn't get Crtc monitor info :(", "error:", err) 303 | break 304 | } 305 | 306 | // Skip disabled CRTCs 307 | if info.Width == 0 || info.Height == 0 { 308 | continue 309 | } 310 | 311 | monitors = append(monitors, Monitor{}) 312 | monitors[i].X = info.X 313 | monitors[i].Y = info.Y 314 | monitors[i].Width = info.Width 315 | monitors[i].Height = info.Height 316 | monitors[i].crtc = crtc 317 | crtcToMonitor[crtc] = &monitors[i] 318 | fmt.Println("X", info.X, "Y", info.Y, "Width", info.Width, "Height", info.Height, "crtc", crtc) 319 | } 320 | 321 | /*dimensions, err := xproto.GetGeometry(X, xproto.Drawable(root)).Reply() 322 | if err != nil { 323 | return nil, fmt.Errorf("couldn't get screen dimensions: %w", err) 324 | }*/ 325 | 326 | for i := range monitors { 327 | // create workspaces 328 | workspaces := make([]Workspace, 10) 329 | for i := range workspaces { 330 | workspaces[i] = Workspace{ 331 | windowList: []*Window{}, 332 | tiling: false, 333 | detachTiling: false, 334 | layoutIndex: 0, 335 | resized: false, 336 | resizedLayout: ResizeLayout{}, 337 | } 338 | } 339 | monitors[i].Workspaces = workspaces 340 | monitors[i].workspaceIndex = 0 341 | monitors[i].lastWorkspaceIndex = 0 342 | monitors[i].CurrWorkspace = &workspaces[0] 343 | monitors[i].layoutIndex = 0 344 | monitors[i].tiling = false 345 | } 346 | 347 | // Tell RandR to send us events. (I think these are all of them, as of 1.3.) 348 | err = randr.SelectInputChecked(X, root, 349 | randr.NotifyMaskScreenChange| 350 | randr.NotifyMaskCrtcChange| 351 | randr.NotifyMaskOutputChange| 352 | randr.NotifyMaskOutputProperty).Check() 353 | if err != nil { 354 | slog.Error("Can't get notified from randr", "error:", err) 355 | } 356 | 357 | // return the window manager struct 358 | return &WindowManager{ 359 | conferror: false, 360 | screen: screen, 361 | conn: X, 362 | root: root, 363 | monitors: monitors, 364 | currMonitor: &monitors[0], 365 | atoms: map[string]xproto.Atom{}, 366 | windows: map[xproto.Window]*Window{}, 367 | crtcToMonitor: crtcToMonitor, 368 | }, nil 369 | } 370 | 371 | func fileExists(filename string) bool { 372 | _, err := os.Stat(filename) 373 | return !os.IsNotExist(err) 374 | } 375 | 376 | func getNumLockMask(conn *xgb.Conn) uint16 { 377 | numLockSym := uint32(0xff7f) // XK_Num_Lock 378 | numLockKeycode := getKeycodeForKeysym(conn, numLockSym) 379 | if numLockKeycode == 0 { 380 | slog.Info("Num Lock keycode not found") 381 | return 0 382 | } 383 | 384 | modMap, err := xproto.GetModifierMapping(conn).Reply() 385 | if err != nil { 386 | slog.Error("failed to get modifier mapping: %v", "error: ", err) 387 | } 388 | 389 | // Each modifier (Shift, Lock, Control, Mod1-Mod5) has modMap.KeycodesPerModifier keycodes 390 | for modIndex := range 8 { 391 | for i := range int(modMap.KeycodesPerModifier) { 392 | index := modIndex*int(modMap.KeycodesPerModifier) + i 393 | if modMap.Keycodes[index] == numLockKeycode { 394 | return 1 << uint(modIndex) 395 | } 396 | } 397 | } 398 | 399 | return 0 400 | } 401 | 402 | func getKeycodeForKeysym(conn *xgb.Conn, keysym uint32) xproto.Keycode { 403 | setup := xproto.Setup(conn) 404 | firstKeycode := setup.MinKeycode 405 | lastKeycode := setup.MaxKeycode 406 | 407 | // Number of keycodes in range: 408 | count := lastKeycode - firstKeycode + 1 409 | 410 | keymap, err := xproto.GetKeyboardMapping(conn, firstKeycode, uint8(count)).Reply() 411 | if err != nil { 412 | slog.Error("failed to get keyboard mapping", "error:", err) 413 | return 0 414 | } 415 | 416 | targetKeysym := xproto.Keysym(keysym) 417 | 418 | for kc := firstKeycode; kc <= lastKeycode; kc++ { 419 | offset := int(kc-firstKeycode) * int(keymap.KeysymsPerKeycode) 420 | for i := range int(keymap.KeysymsPerKeycode) { 421 | if keymap.Keysyms[offset+i] == targetKeysym { 422 | return kc 423 | } 424 | } 425 | } 426 | return 0 427 | } 428 | 429 | // Converts a keysym (layout-agnostic symbol like XK_b) to a keycode (layout-specific) 430 | func keysymToKeycode(conn *xgb.Conn, target xproto.Keysym) (xproto.Keycode, error) { 431 | setup := xproto.Setup(conn) 432 | first := setup.MinKeycode 433 | last := setup.MaxKeycode 434 | count := last - first + 1 435 | 436 | reply, err := xproto.GetKeyboardMapping(conn, first, uint8(count)).Reply() 437 | if err != nil { 438 | return 0, fmt.Errorf("GetKeyboardMapping failed: %w", err) 439 | } 440 | 441 | for kc := first; kc <= last; kc++ { 442 | offset := int(kc-first) * int(reply.KeysymsPerKeycode) 443 | for i := 0; i < int(reply.KeysymsPerKeycode); i++ { 444 | if reply.Keysyms[offset+i] == target { 445 | return kc, nil 446 | } 447 | } 448 | } 449 | return 0, fmt.Errorf("no keycode for keysym 0x%X", target) 450 | } 451 | 452 | // gets keycode of key and sets it, then tells the X server to notify us when this keybind is pressed. 453 | func (wm *WindowManager) createKeybind(kb *Keybind) Keybind { 454 | code := keybind.StrToKeycodes(XUtil, kb.Key) 455 | if len(code) < 1 { 456 | return Keybind{ 457 | Keycode: 0, 458 | Key: "", 459 | Shift: false, 460 | Exec: "", 461 | } 462 | } 463 | KeyCode := code[0] 464 | kb.Keycode = uint32(KeyCode) 465 | Mask := wm.mod 466 | if kb.Shift { 467 | Mask |= xproto.ModMaskShift 468 | } 469 | err := xproto.GrabKeyChecked(wm.conn, true, wm.root, Mask, KeyCode, xproto.GrabModeAsync, xproto.GrabModeAsync). 470 | Check() 471 | if err != nil { 472 | slog.Error("Couldn't grab key", "error:", err) 473 | } 474 | 475 | err = xproto.GrabKeyChecked( 476 | wm.conn, 477 | true, 478 | wm.root, 479 | Mask|xproto.ModMaskLock, 480 | KeyCode, 481 | xproto.GrabModeAsync, 482 | xproto.GrabModeAsync, 483 | ). 484 | Check() 485 | if err != nil { 486 | slog.Error("Couldn't grab key", "error:", err) 487 | } 488 | numlock := getNumLockMask(wm.conn) 489 | if numlock != wm.mod { 490 | err = xproto.GrabKeyChecked( 491 | wm.conn, 492 | true, 493 | wm.root, 494 | Mask|numlock, 495 | KeyCode, 496 | xproto.GrabModeAsync, 497 | xproto.GrabModeAsync, 498 | ). 499 | Check() 500 | } 501 | if err != nil { 502 | slog.Error("Couldn't create keybind", "error:", err) 503 | } 504 | 505 | return *kb 506 | } 507 | 508 | func (wm *WindowManager) reload(focused xproto.ButtonPressEvent) { 509 | if wm.config.AutoReload && wm.configWatcher == nil { 510 | wm.configListener() 511 | } else if !wm.config.AutoReload && wm.configWatcher != nil { 512 | wm.configWatcher.Close() 513 | wm.configWatcher = nil 514 | } 515 | // set the mod key for the wm 516 | var mMask uint16 517 | switch wm.config.ModKey { 518 | case "Mod1": 519 | mMask = xproto.ModMask1 520 | case "Mod2": 521 | mMask = xproto.ModMask2 522 | case "Mod3": 523 | mMask = xproto.ModMask3 524 | case "Mod4": 525 | mMask = xproto.ModMask4 526 | case "Mod5": 527 | mMask = xproto.ModMask5 528 | } 529 | 530 | wm.mod = mMask 531 | 532 | // manage keybinds for keybinds in the config 533 | for i, kb := range wm.config.Keybinds { 534 | wm.config.Keybinds[i] = wm.createKeybind(&kb) 535 | } 536 | wm.setKeyBinds() 537 | 538 | windowsParent, err := xproto.QueryTree(wm.conn, wm.root).Reply() 539 | if err != nil { 540 | return 541 | } 542 | windows := windowsParent.Children 543 | fmt.Println("windows:", windows) 544 | fmt.Println("new border width:", wm.config.BorderWidth) 545 | 546 | for _, window := range windows { 547 | if win, ok := wm.windows[window]; ok && !win.Fullscreen { 548 | col := wm.config.BorderUnactive 549 | if window == focused.Child { 550 | col = wm.config.BorderActive 551 | } 552 | 553 | // Set border width 554 | err := xproto.ConfigureWindowChecked( 555 | wm.conn, 556 | window, 557 | xproto.ConfigWindowBorderWidth, 558 | []uint32{wm.config.BorderWidth}, 559 | ). 560 | Check() 561 | if err != nil { 562 | slog.Error("Couldn't set border width", "error", err) 563 | } 564 | 565 | // Set border color 566 | err = xproto.ChangeWindowAttributesChecked(wm.conn, window, xproto.CwBorderPixel, []uint32{col}). 567 | Check() 568 | if err != nil { 569 | slog.Error("Couldn't set border color", "error", err) 570 | } 571 | } 572 | } 573 | 574 | wm.fitToLayout() 575 | } 576 | 577 | func (wm *WindowManager) positionMonitors() { 578 | resources, err := randr.GetScreenResources(wm.conn, wm.root).Reply() 579 | if err != nil { 580 | slog.Error("Couldn't get resources", "error:", err) 581 | return 582 | } 583 | 584 | count := 0 585 | for i, crtc := range resources.Crtcs { 586 | info, err := randr.GetCrtcInfo(wm.conn, crtc, 0).Reply() 587 | if err != nil { 588 | continue 589 | } 590 | if info.Width == 0 || info.Height == 0 { 591 | continue 592 | } 593 | 594 | if len(wm.config.Monitors) == count { 595 | break 596 | } 597 | X, Y := wm.config.Monitors[i].X, wm.config.Monitors[i].Y 598 | 599 | if info.Width == 0 || info.Height == 0 { 600 | continue 601 | } 602 | randr.SetCrtcConfig(wm.conn, crtc, 0, 0, int16(X), int16(Y), info.Mode, info.Rotation, info.Outputs) 603 | wm.monitors[count].X = int16(X) 604 | wm.monitors[count].Y = int16(Y) 605 | count++ 606 | } 607 | 608 | widths := 0 609 | heights := 0 610 | for _, mon := range wm.monitors { 611 | widths += int(mon.Width) 612 | heights += int(mon.Height) 613 | } 614 | 615 | widthsMM := 0 616 | heightsMM := 0 617 | for _, out := range resources.Outputs { 618 | info, _ := randr.GetOutputInfo(wm.conn, out, 0).Reply() 619 | widthsMM += int(info.MmWidth) 620 | heightsMM += int(info.MmHeight) 621 | } 622 | 623 | err = randr.SetScreenSizeChecked( 624 | wm.conn, 625 | wm.root, 626 | uint16(widths), 627 | uint16(heights), 628 | uint32(widthsMM), 629 | uint32(heightsMM), 630 | ).Check() 631 | if err != nil { 632 | slog.Error("Couldnt set screen size", "error", err) 633 | } 634 | } 635 | 636 | func (wm *WindowManager) pointerToWindow(window xproto.Window) error { 637 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(window)).Reply() 638 | if err != nil { 639 | return err 640 | } 641 | 642 | trans, err := xproto.TranslateCoordinates(wm.conn, window, xproto.Setup(wm.conn).DefaultScreen(wm.conn).Root, 0, 0). 643 | Reply() 644 | if err != nil { 645 | return err 646 | } 647 | 648 | x := trans.DstX + int16(geom.Width)/2 649 | y := trans.DstY + int16(geom.Height)/2 650 | 651 | return xproto.WarpPointerChecked(wm.conn, 0, xproto.Setup(wm.conn).DefaultScreen(wm.conn).Root, 0, 0, 0, 0, x, y). 652 | Check() 653 | } 654 | 655 | // Run runs the window manager. 656 | func (wm *WindowManager) Run() { //nolint:cyclop 657 | fmt.Println("window manager up and running") 658 | 659 | // get autostart 660 | user, err := user.Current() 661 | if err == nil { 662 | scriptPath := filepath.Join(user.HomeDir, ".config", "doWM", "autostart.sh") 663 | 664 | if fileExists(scriptPath) { 665 | fmt.Println("autostart exists..., running") 666 | _ = exec.Command(scriptPath).Start() 667 | } 668 | } 669 | 670 | // basically asks the X server for WM access 671 | err = xproto.ChangeWindowAttributesChecked( 672 | wm.conn, 673 | wm.root, 674 | xproto.CwEventMask, 675 | []uint32{ 676 | xproto.EventMaskSubstructureNotify | 677 | xproto.EventMaskSubstructureRedirect, 678 | }, 679 | ).Check() 680 | if err != nil { 681 | if err.Error() == "BadAccess" { 682 | slog.Error("Other window manager running on display") 683 | return 684 | } 685 | } 686 | 687 | // wm.cursor() 688 | 689 | // retrieve config and set values 690 | cfg := wm.createConfig(false) 691 | wm.config = cfg 692 | if len(wm.config.Monitors) != 0 { 693 | wm.positionMonitors() 694 | } 695 | if wm.config.StartTiling { 696 | cm := wm.currMonitor 697 | for i := range wm.monitors { 698 | wm.currMonitor = &wm.monitors[i] 699 | wm.toggleTiling() 700 | wm.fitToLayout() 701 | } 702 | wm.currMonitor = cm 703 | } 704 | if wm.config.AutoReload { 705 | wm.configListener() 706 | } 707 | defer func() { 708 | if wm.configWatcher != nil { 709 | wm.configWatcher.Close() 710 | } 711 | }() 712 | 713 | // for things like polybar, to show workspaces 714 | wm.broadcastWorkspace(0) 715 | wm.broadcastWorkspaceCount() 716 | 717 | // grab the server whilst we manage pre-existing windows 718 | err = xproto.GrabServerChecked( 719 | wm.conn, 720 | ).Check() 721 | if err != nil { 722 | slog.Error("Couldn't grab X server", "error:", err) 723 | return 724 | } 725 | 726 | // if there are any pre-existing windows, we need to manage them 727 | tree, err := xproto.QueryTree( 728 | wm.conn, 729 | wm.root, 730 | ).Reply() 731 | if err != nil { 732 | slog.Error("Couldn't query tree", "error:", err) 733 | return 734 | } 735 | 736 | root, TopLevelWindows := tree.Root, tree.Children 737 | 738 | if root != wm.root { 739 | slog.Error("Tree root not equal to window manager root") 740 | return 741 | } 742 | 743 | for _, window := range TopLevelWindows { 744 | if !shouldIgnoreWindow(wm.conn, window) { 745 | wm.frame(window, true) 746 | } 747 | } 748 | 749 | err = xproto.UngrabServerChecked(wm.conn).Check() 750 | if err != nil { 751 | slog.Error("Couldn't ungrab server", "error:", err.Error()) 752 | return 753 | } 754 | 755 | // set the mod key for the wm 756 | switch wm.config.ModKey { 757 | case "Mod1": 758 | mMask = xproto.ModMask1 759 | case "Mod2": 760 | mMask = xproto.ModMask2 761 | case "Mod3": 762 | mMask = xproto.ModMask3 763 | case "Mod4": 764 | mMask = xproto.ModMask4 765 | case "Mod5": 766 | mMask = xproto.ModMask5 767 | } 768 | 769 | wm.mod = mMask 770 | 771 | // manage keybinds for keybinds in the config 772 | for i, kb := range wm.config.Keybinds { 773 | wm.config.Keybinds[i] = wm.createKeybind(&kb) 774 | } 775 | 776 | wm.setKeyBinds() 777 | fmt.Println(wm.config.Keybinds) 778 | 779 | // Only grab with Mod + left or right click (not plain Button1) 780 | err = xproto.GrabButtonChecked( 781 | wm.conn, 782 | false, 783 | wm.root, 784 | uint16(xproto.EventMaskButtonPress|xproto.EventMaskButtonRelease|xproto.EventMaskPointerMotion), 785 | xproto.GrabModeAsync, 786 | xproto.GrabModeAsync, 787 | xproto.WindowNone, 788 | xproto.AtomNone, 789 | xproto.ButtonIndex1, 790 | mMask, 791 | ). 792 | Check() 793 | if err != nil { 794 | slog.Error("Couldn't grab button", "error:", err.Error()) 795 | } 796 | 797 | err = xproto.GrabButtonChecked( 798 | wm.conn, 799 | false, 800 | wm.root, 801 | uint16(xproto.EventMaskButtonPress|xproto.EventMaskButtonRelease|xproto.EventMaskPointerMotion), 802 | xproto.GrabModeAsync, 803 | xproto.GrabModeAsync, 804 | xproto.WindowNone, 805 | xproto.AtomNone, 806 | xproto.ButtonIndex3, 807 | mMask, 808 | ). 809 | Check() 810 | if err != nil { 811 | slog.Error("Couldn't grab window+c key", "error:", err.Error()) 812 | } 813 | 814 | // create EMWH atoms 815 | atoms := []string{ 816 | "_NET_WM_STATE", 817 | "_NET_WM_STATE_FULLSCREEN", 818 | "_NET_WM_STATE_ABOVE", 819 | "_NET_WM_STATE_BELOW", 820 | "_NET_WM_STATE_MAXIMIZED_HORZ", 821 | "_NET_WM_STATE_MAXIMIZED_VERT", 822 | "_NET_WM_WINDOW_TYPE", 823 | "_NET_WM_WINDOW_TYPE_DOCK", 824 | "_NET_WM_STRUT_PARTIAL", 825 | "_NET_WORKAREA", 826 | "_NET_CURRENT_DESKTOP", 827 | } 828 | 829 | for _, name := range atoms { 830 | a, _ := xproto.InternAtom(wm.conn, false, uint16(len(name)), name).Reply() 831 | fmt.Printf("%s = %d\n", name, a.Atom) 832 | wm.atoms[name] = a.Atom 833 | } 834 | wm.declareSupportedAtoms() 835 | 836 | for { 837 | // get next event 838 | event, err := wm.conn.WaitForEvent() 839 | if err != nil { 840 | slog.Error("Event error", "error:", err.Error()) 841 | continue 842 | } 843 | if event == nil { 844 | return 845 | } 846 | 847 | pointer, ptrerr := xproto.QueryPointer(wm.conn, wm.root).Reply() 848 | if ptrerr == nil { 849 | for i, mon := range wm.monitors { 850 | if pointer.RootX >= mon.X && pointer.RootX <= mon.X+int16(mon.Width) && pointer.RootY >= mon.Y && 851 | pointer.RootY <= mon.Y+int16(mon.Height) { 852 | wm.currMonitor = &wm.monitors[i] 853 | wm.setNetClientList() 854 | wm.setNetWorkArea() 855 | } 856 | } 857 | } 858 | 859 | if len(wm.currMonitor.CurrWorkspace.windowList) == 0 { 860 | err := xproto.SetInputFocusChecked(wm.conn, xproto.InputFocusPointerRoot, wm.root, xproto.TimeCurrentTime). 861 | Check() 862 | if err != nil { 863 | slog.Error("Couldn't set input focus", "error", err) 864 | } 865 | } 866 | switch ev := event.(type) { 867 | case randr.NotifyEvent: 868 | fmt.Println("RANDR NOTIFY", ev) 869 | 870 | case xproto.ButtonPressEvent: 871 | // set values on current window, used later with moving and resizing 872 | if ev.Child != 0 && ev.State&mMask != 0 { 873 | attr, _ = xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 874 | start = ev 875 | if ev.Detail == xproto.ButtonIndex1 { 876 | xproto.ConfigureWindow( 877 | wm.conn, 878 | ev.Child, 879 | xproto.ConfigWindowStackMode, 880 | []uint32{xproto.StackModeAbove}, 881 | ) 882 | } 883 | } else if ev.State&mMask == 0 { 884 | xproto.AllowEvents(wm.conn, xproto.AllowReplayPointer, xproto.TimeCurrentTime) 885 | } 886 | case xproto.ButtonReleaseEvent: 887 | ev, ok := event.(xproto.ButtonReleaseEvent) 888 | if !ok { 889 | break 890 | } 891 | 892 | // if we don't have the mouse down, we don't want to move or resize 893 | 894 | var startmon *Monitor 895 | var endmon *Monitor 896 | for _, mon := range wm.monitors { 897 | if start.RootX >= mon.X && start.RootX <= mon.X+int16(mon.Width) && start.RootY >= mon.Y && start.RootY <= mon.Y+int16(mon.Height) { 898 | startmon = &mon 899 | } 900 | if ev.RootX >= mon.X && ev.RootX <= mon.X+int16(mon.Width) && ev.RootY >= mon.Y && ev.RootY <= mon.Y+int16(mon.Height) { 901 | endmon = &mon 902 | } 903 | } 904 | 905 | if startmon != endmon { 906 | orx := wm.windows[ev.Child].X - int(startmon.X) 907 | ory := wm.windows[ev.Child].Y - int(startmon.Y) 908 | wm.currMonitor.CurrWorkspace.windowList = append(wm.currMonitor.CurrWorkspace.windowList, &Window{ 909 | id: ev.Child, 910 | X: int(endmon.X) + orx, 911 | Y: int(endmon.Y) + ory, 912 | Width: wm.windows[ev.Child].Width, 913 | Height: wm.windows[ev.Child].Height, 914 | Client: ev.Child, 915 | }) 916 | remove(&startmon.CurrWorkspace.windowList, ev.Child) 917 | wm.currMonitor = startmon 918 | wm.fitToLayout() 919 | wm.currMonitor = endmon 920 | wm.fitToLayout() 921 | } 922 | if wm.currMonitor.tiling { 923 | found := false 924 | for _, window := range wm.currMonitor.CurrWorkspace.windowList { 925 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(window.id)).Reply() 926 | if err != nil { 927 | continue 928 | } 929 | if window.id != ev.Child && 930 | ev.EventX < geom.X+int16(geom.Width) && 931 | ev.EventX > geom.X && 932 | ev.EventY < geom.Y+int16(geom.Height) && 933 | ev.EventY > geom.Y { 934 | fmt.Println("MOVING", ev.Child, window.id) 935 | swapWindowsID(&wm.currMonitor.CurrWorkspace.windowList, ev.Child, window.id) 936 | wm.fitToLayout() 937 | found = true 938 | break 939 | } 940 | } 941 | if !found { 942 | wm.fitToLayout() 943 | } 944 | } 945 | start.Child = 0 946 | xproto.AllowEvents(wm.conn, xproto.AllowReplayPointer, xproto.TimeCurrentTime) 947 | case xproto.MotionNotifyEvent: 948 | // if we have the mouse down and we are holding the mod key, and if we are not tiling and the window is not 949 | // full screen then do some simple maths to move and resize 950 | 951 | fmt.Println("X:", ev.RootX, "Y:", ev.RootY) 952 | for _, mon := range wm.monitors { 953 | if ev.RootX >= mon.X && ev.RootX <= mon.X+int16(mon.Width) && ev.RootY >= mon.Y && ev.RootY <= mon.Y+int16(mon.Height) { 954 | wm.currMonitor = &mon 955 | } 956 | } 957 | 958 | focusWindow(wm.conn, ev.Child) 959 | if start.Child != 0 && ev.State&mMask != 0 { 960 | if wm.windows[start.Child] != nil && wm.windows[start.Child].Fullscreen { 961 | break 962 | } 963 | xdiff := ev.RootX - start.RootX 964 | ydiff := ev.RootY - start.RootY 965 | Xoffset := attr.X + xdiff 966 | Yoffset := attr.Y + ydiff 967 | sizeY := attr.Height 968 | sizeX := attr.Width 969 | fmt.Println("start detail") 970 | fmt.Println(start.Detail) 971 | if start.Detail == xproto.ButtonIndex3 { 972 | if wm.currMonitor.CurrWorkspace.tiling { 973 | break 974 | } 975 | Xoffset = attr.X 976 | Yoffset = attr.Y 977 | sizeX = uint16(max(10, int(int16(attr.Width)+xdiff))) 978 | sizeY = uint16(max(10, int(int16(attr.Height)+ydiff))) 979 | } 980 | 981 | xproto.ConfigureWindow( 982 | wm.conn, 983 | start.Child, 984 | xproto.ConfigWindowX|xproto.ConfigWindowY| 985 | xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, 986 | []uint32{uint32(Xoffset), uint32(Yoffset), uint32(sizeX), uint32(sizeY)}, 987 | ) 988 | } 989 | case xproto.CreateNotifyEvent: 990 | fmt.Println("create notify") 991 | case xproto.ConfigureRequestEvent: 992 | wm.onConfigureRequest(ev) 993 | case xproto.MapRequestEvent: 994 | fmt.Println("MapRequest") 995 | wm.onMapRequest(ev) 996 | case xproto.ReparentNotifyEvent: 997 | fmt.Println("reparent notify") 998 | case xproto.MapNotifyEvent: 999 | fmt.Println("MapNotify") 1000 | case xproto.ConfigureNotifyEvent: 1001 | fmt.Println("ConfigureNotify") 1002 | case xproto.UnmapNotifyEvent: 1003 | fmt.Println("unmapping") 1004 | wm.onUnmapNotify(ev) 1005 | case xproto.DestroyNotifyEvent: 1006 | fmt.Println("DestroyNotify") 1007 | fmt.Println("Window:") 1008 | fmt.Println(ev.Window) 1009 | fmt.Println("Event:") 1010 | fmt.Println(ev.Event) 1011 | // if the destroy notify has come through but we haven't registered any kind of deletion then handle it 1012 | if _, ok := wm.windows[ev.Window]; ok { 1013 | wm.remDestroyedWin(ev.Window) 1014 | } 1015 | fmt.Println("finished destroying") 1016 | case xproto.EnterNotifyEvent: 1017 | // when we enter the frame, change the border color 1018 | fmt.Println("EnterNotify") 1019 | fmt.Println(ev.Event) 1020 | wm.onEnterNotify(ev) 1021 | case xproto.LeaveNotifyEvent: 1022 | // when we leave the frame, change the border color 1023 | fmt.Println("LeaveNotify") 1024 | fmt.Println(ev.Event) 1025 | wm.onLeaveNotify(ev) 1026 | case xproto.KeyPressEvent: 1027 | fmt.Println("keyPress") 1028 | // if mod key is down 1029 | if ev.State&mMask != 0 { 1030 | // go through keybinds if the keybind matches up to the current event then continue 1031 | for _, kb := range wm.config.Keybinds { 1032 | if ev.Detail == xproto.Keycode(kb.Keycode) && (ev.State&(mMask|xproto.ModMaskShift) == 1033 | (mMask | xproto.ModMaskShift) == kb.Shift) { 1034 | // if it has an exec then just execute it 1035 | if kb.Exec != "" { 1036 | fmt.Println("executing:", kb.Exec) 1037 | runCommand(kb.Exec) 1038 | fmt.Println("excuted") 1039 | } 1040 | switch kb.Role { 1041 | case "resize-x-scale-up": 1042 | if wm.currMonitor.CurrWorkspace.tiling { 1043 | if err := wm.pointerToWindow(ev.Child); err != nil { 1044 | slog.Error("Couldn't move pointer to window", "error:", err) 1045 | } 1046 | if !wm.resizeTiledX(true, ev) { 1047 | break 1048 | } 1049 | } else { 1050 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 1051 | if err != nil { 1052 | break 1053 | } 1054 | xproto.ConfigureWindowChecked(wm.conn, ev.Child, xproto.ConfigWindowWidth, 1055 | []uint32{uint32(geom.Width + uint16(wm.config.Resize))}) 1056 | if err := wm.pointerToWindow(ev.Child); err != nil { 1057 | slog.Error("Couldn't move pointer to window", "error:", err) 1058 | } 1059 | } 1060 | case "resize-x-scale-down": 1061 | if wm.currMonitor.CurrWorkspace.tiling { 1062 | if err := wm.pointerToWindow(ev.Child); err != nil { 1063 | slog.Error("Couldn't move pointer to window", "error:", err) 1064 | } 1065 | if !wm.resizeTiledX(false, ev) { 1066 | break 1067 | } 1068 | } else { 1069 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 1070 | if err != nil { 1071 | break 1072 | } 1073 | if geom.Width > 10 { 1074 | xproto.ConfigureWindowChecked(wm.conn, ev.Child, xproto.ConfigWindowWidth, 1075 | []uint32{uint32(geom.Width - uint16(wm.config.Resize))}) 1076 | if err := wm.pointerToWindow(ev.Child); err != nil { 1077 | slog.Error("Couldn't move pointer to window", "error:", err) 1078 | } 1079 | } 1080 | } 1081 | case "resize-y-scale-up": 1082 | if wm.currMonitor.CurrWorkspace.tiling { 1083 | if err := wm.pointerToWindow(ev.Child); err != nil { 1084 | slog.Error("Couldn't move pointer to window", "error:", err) 1085 | } 1086 | if !wm.resizeTiledY(true, ev) { 1087 | break 1088 | } 1089 | } else { 1090 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 1091 | if err != nil { 1092 | break 1093 | } 1094 | xproto.ConfigureWindowChecked(wm.conn, ev.Child, xproto.ConfigWindowHeight, 1095 | []uint32{uint32(geom.Height + uint16(wm.config.Resize))}) 1096 | if err := wm.pointerToWindow(ev.Child); err != nil { 1097 | slog.Error("Couldn't move pointer to window", "error:", err) 1098 | } 1099 | } 1100 | case "resize-y-scale-down": 1101 | if wm.currMonitor.CurrWorkspace.tiling { 1102 | if err := wm.pointerToWindow(ev.Child); err != nil { 1103 | slog.Error("Couldn't move pointer to window", "error:", err) 1104 | } 1105 | if !wm.resizeTiledY(false, ev) { 1106 | break 1107 | } 1108 | } else { 1109 | if wm.currMonitor.CurrWorkspace.tiling { 1110 | break 1111 | } 1112 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 1113 | if err != nil { 1114 | break 1115 | } 1116 | if geom.Height > 10 { 1117 | xproto.ConfigureWindowChecked(wm.conn, ev.Child, xproto.ConfigWindowHeight, 1118 | []uint32{uint32(geom.Height - uint16(wm.config.Resize))}) 1119 | if err := wm.pointerToWindow(ev.Child); err != nil { 1120 | slog.Error("Couldn't move pointer to window", "error:", err) 1121 | } 1122 | } 1123 | } 1124 | case "move-x-right": 1125 | if wm.currMonitor.CurrWorkspace.tiling { 1126 | break 1127 | } 1128 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 1129 | if err != nil { 1130 | break 1131 | } 1132 | xproto.ConfigureWindowChecked(wm.conn, ev.Child, xproto.ConfigWindowX, []uint32{uint32(geom.X + 10)}) 1133 | if err := wm.pointerToWindow(ev.Child); err != nil { 1134 | slog.Error("Couldn't move pointer to window", "error:", err) 1135 | } 1136 | case "move-x-left": 1137 | if wm.currMonitor.CurrWorkspace.tiling { 1138 | break 1139 | } 1140 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 1141 | if err != nil { 1142 | break 1143 | } 1144 | xproto.ConfigureWindowChecked(wm.conn, ev.Child, xproto.ConfigWindowX, []uint32{uint32(geom.X - 10)}) 1145 | if err := wm.pointerToWindow(ev.Child); err != nil { 1146 | slog.Error("Couldn't move pointer to window", "error:", err) 1147 | } 1148 | case "move-y-up": 1149 | if wm.currMonitor.CurrWorkspace.tiling { 1150 | break 1151 | } 1152 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 1153 | if err != nil { 1154 | break 1155 | } 1156 | xproto.ConfigureWindowChecked(wm.conn, ev.Child, xproto.ConfigWindowY, []uint32{uint32(geom.Y - 10)}) 1157 | if err := wm.pointerToWindow(ev.Child); err != nil { 1158 | slog.Error("Couldn't move pointer to window", "error:", err) 1159 | } 1160 | case "move-y-down": 1161 | if wm.currMonitor.CurrWorkspace.tiling { 1162 | break 1163 | } 1164 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 1165 | if err != nil { 1166 | break 1167 | } 1168 | xproto.ConfigureWindowChecked(wm.conn, ev.Child, xproto.ConfigWindowY, []uint32{uint32(geom.Y + 10)}) 1169 | if err := wm.pointerToWindow(ev.Child); err != nil { 1170 | slog.Error("Couldn't move pointer to window", "error:", err) 1171 | } 1172 | case "quit": 1173 | if _, ok := wm.windows[ev.Child]; ok { 1174 | // EMWH way of politely saying to destroy 1175 | if err := wm.sendWmDelete(wm.conn, wm.windows[ev.Child].id); err != nil { 1176 | slog.Error("send WmDelete", "error", err) 1177 | } 1178 | fmt.Println("closing window:", wm.windows[ev.Child].id, "frame:", ev.Child) 1179 | } 1180 | case "force-quit": 1181 | // force close 1182 | err := xproto.DestroyWindowChecked(wm.conn, wm.windows[ev.Child].id).Check() 1183 | if err != nil { 1184 | fmt.Println("Couldn't force destroy:", err) 1185 | } 1186 | case "toggle-tiling": 1187 | wm.toggleTiling() 1188 | case "detach-tiling": 1189 | if wm.currMonitor.CurrWorkspace.detachTiling { 1190 | wm.currMonitor.CurrWorkspace.detachTiling = false 1191 | if wm.currMonitor.tiling && !wm.currMonitor.CurrWorkspace.tiling { 1192 | wm.enableTiling() 1193 | } else if !wm.currMonitor.tiling && wm.currMonitor.CurrWorkspace.tiling { 1194 | wm.disableTiling() 1195 | } 1196 | } else { 1197 | wm.currMonitor.CurrWorkspace.detachTiling = true 1198 | } 1199 | wm.fitToLayout() 1200 | case "toggle-fullscreen": 1201 | wm.toggleFullScreen(ev.Child) 1202 | case "swap-window-left": 1203 | fmt.Println("swap left") 1204 | if wm.currMonitor.CurrWorkspace.tiling { 1205 | currWindow := ev.Child 1206 | swapLeft: 1207 | for i := range wm.currMonitor.CurrWorkspace.windowList { 1208 | if currWindow == wm.currMonitor.CurrWorkspace.windowList[i].id { 1209 | if i == 0 { 1210 | swapWindows(&wm.currMonitor.CurrWorkspace.windowList, i, len(wm.currMonitor.CurrWorkspace.windowList)-1) 1211 | } else { 1212 | swapWindows(&wm.currMonitor.CurrWorkspace.windowList, i, i-1) 1213 | } 1214 | wm.fitToLayout() 1215 | if err := wm.pointerToWindow(currWindow); err != nil { 1216 | slog.Error("Couldn't move pointer to window", "error:", err) 1217 | } 1218 | break swapLeft 1219 | } 1220 | } 1221 | } 1222 | case "swap-window-right": 1223 | fmt.Println("swap right") 1224 | if wm.currMonitor.CurrWorkspace.tiling { 1225 | currWindow := ev.Child 1226 | swapRight: 1227 | for i := range wm.currMonitor.CurrWorkspace.windowList { 1228 | if currWindow == wm.currMonitor.CurrWorkspace.windowList[i].id { 1229 | if i == len(wm.currMonitor.CurrWorkspace.windowList)-1 { 1230 | swapWindows(&wm.currMonitor.CurrWorkspace.windowList, i, 0) 1231 | } else { 1232 | swapWindows(&wm.currMonitor.CurrWorkspace.windowList, i, i+1) 1233 | } 1234 | wm.fitToLayout() 1235 | if err := wm.pointerToWindow(currWindow); err != nil { 1236 | slog.Error("Couldn't move pointer to window", "error:", err) 1237 | } 1238 | break swapRight 1239 | } 1240 | } 1241 | } 1242 | case "focus-window-right": 1243 | if wm.currMonitor.CurrWorkspace.tiling { 1244 | currWindow := ev.Child 1245 | focusRight: 1246 | for i := range wm.currMonitor.CurrWorkspace.windowList { 1247 | if currWindow == wm.currMonitor.CurrWorkspace.windowList[i].id { 1248 | if i == len(wm.currMonitor.CurrWorkspace.windowList)-1 { 1249 | if err := wm.pointerToWindow(wm.currMonitor.CurrWorkspace.windowList[0].id); err != nil { 1250 | slog.Error("Couldn't move pointer to window", "error:", err) 1251 | } 1252 | } else { 1253 | if err := wm.pointerToWindow(wm.currMonitor.CurrWorkspace.windowList[i+1].id); err != nil { 1254 | slog.Error("Couldn't move pointer to window", "error:", err) 1255 | } 1256 | } 1257 | break focusRight 1258 | } 1259 | } 1260 | } 1261 | case "focus-window-left": 1262 | if wm.currMonitor.CurrWorkspace.tiling { 1263 | currWindow := ev.Child 1264 | focusLeft: 1265 | for i := range wm.currMonitor.CurrWorkspace.windowList { 1266 | if currWindow == wm.currMonitor.CurrWorkspace.windowList[i].id { 1267 | if i == 0 { 1268 | err := wm.pointerToWindow(wm.currMonitor.CurrWorkspace.windowList[len(wm.currMonitor.CurrWorkspace.windowList)-1].id) 1269 | if err != nil { 1270 | slog.Error("Couldn't move pointer to window", "error:", err) 1271 | } 1272 | } else { 1273 | if err := wm.pointerToWindow(wm.currMonitor.CurrWorkspace.windowList[i-1].id); err != nil { 1274 | slog.Error("Couldn't move pointer to window", "error:", err) 1275 | } 1276 | } 1277 | break focusLeft 1278 | } 1279 | } 1280 | } 1281 | case "reload-config": 1282 | cfg := wm.createConfig(false) 1283 | wm.config = cfg 1284 | if len(wm.config.Monitors) != 0 { 1285 | wm.positionMonitors() 1286 | } 1287 | wm.reload(start) 1288 | mMask = wm.mod 1289 | 1290 | case "next-layout": 1291 | windowNum := len(wm.currMonitor.CurrWorkspace.windowList) 1292 | if windowNum < 1 { 1293 | break 1294 | } 1295 | totalLen := len(wm.config.lyts[windowNum]) - 1 1296 | if wm.currMonitor.CurrWorkspace.layoutIndex == totalLen { 1297 | wm.currMonitor.CurrWorkspace.layoutIndex = 0 1298 | } else { 1299 | wm.currMonitor.CurrWorkspace.layoutIndex++ 1300 | } 1301 | wm.currMonitor.layoutIndex = wm.currMonitor.CurrWorkspace.layoutIndex 1302 | wm.currMonitor.CurrWorkspace.resized = false 1303 | wm.currMonitor.CurrWorkspace.resizedLayout = ResizeLayout{} 1304 | wm.fitToLayout() 1305 | case "increase-gap": 1306 | wm.config.Gap++ 1307 | wm.fitToLayout() 1308 | case "decrease-gap": 1309 | if wm.config.Gap > 0 { 1310 | wm.config.Gap-- 1311 | } 1312 | wm.fitToLayout() 1313 | } 1314 | switch kb.Key { 1315 | case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9": 1316 | // if shift is pressed we want to move the window to the next workspace, so delete it from 1317 | // the record of the current workspace so when they unmap all the other windows (giving the 1318 | // illusion of changing workspace) this one stays then afterwards reparent it to the 1319 | // workspace that has been changed to 1320 | w := ev.Child 1321 | var window Window 1322 | shiftok := false 1323 | if kb.Shift { 1324 | if _, ok := wm.windows[w]; ok { 1325 | shiftok = ok 1326 | window = *wm.windows[w] 1327 | fmt.Println("moving window") 1328 | xproto.ConfigureWindow( 1329 | wm.conn, 1330 | w, 1331 | xproto.ConfigWindowStackMode, 1332 | []uint32{xproto.StackModeAbove}, 1333 | ) 1334 | remove(&wm.currMonitor.CurrWorkspace.windowList, w) 1335 | } 1336 | } 1337 | switch kb.Key { 1338 | case "1": 1339 | wm.switchWorkspace(0) 1340 | case "2": 1341 | wm.switchWorkspace(1) 1342 | case "3": 1343 | wm.switchWorkspace(2) 1344 | case "4": 1345 | wm.switchWorkspace(3) 1346 | case "5": 1347 | wm.switchWorkspace(4) 1348 | case "6": 1349 | wm.switchWorkspace(5) 1350 | case "7": 1351 | wm.switchWorkspace(6) 1352 | case "8": 1353 | wm.switchWorkspace(7) 1354 | case "9": 1355 | wm.switchWorkspace(8) 1356 | case "0": 1357 | wm.switchWorkspace(9) 1358 | } 1359 | if kb.Shift && shiftok { 1360 | wm.currMonitor.CurrWorkspace.windowList = append(wm.currMonitor.CurrWorkspace.windowList, &window) 1361 | wm.setWindowDesktop(w, uint32(wm.currMonitor.workspaceIndex)) 1362 | } 1363 | wm.fitToLayout() 1364 | } 1365 | } 1366 | } 1367 | } 1368 | 1369 | case xproto.ClientMessageEvent: 1370 | fmt.Println("client message") 1371 | 1372 | atomName, _ := xproto.GetAtomName(wm.conn, ev.Type).Reply() 1373 | fmt.Println("ClientMessage atom:", atomName.Name) 1374 | 1375 | if atomName.Name == "_NET_CURRENT_DESKTOP" { 1376 | desktop := int(ev.Data.Data32[0]) 1377 | wm.switchWorkspace(desktop) 1378 | } 1379 | 1380 | if atomName.Name == "_NET_WM_STATE" && wm.config.AutoFullscreen { 1381 | fullscreenAtom, _ := wm.internAtom("_NET_WM_STATE_FULLSCREEN") 1382 | maxHorzAtom, _ := wm.internAtom("_NET_WM_STATE_MAXIMIZED_HORZ") 1383 | maxVertAtom, _ := wm.internAtom("_NET_WM_STATE_MAXIMIZED_VERT") 1384 | 1385 | action := ev.Data.Data32[0] // 0 = remove, 1 = add, 2 = toggle 1386 | prop1 := ev.Data.Data32[1] 1387 | prop2 := ev.Data.Data32[2] 1388 | 1389 | if _, ok := wm.windows[ev.Window]; !ok { 1390 | break 1391 | } 1392 | 1393 | if prop1 == uint32(maxHorzAtom) || prop2 == uint32(maxHorzAtom) || 1394 | prop1 == uint32(maxVertAtom) || prop2 == uint32(maxVertAtom) { 1395 | fmt.Println("maximized called, action", action) 1396 | switch action { 1397 | case 0: // remove 1398 | wm.disableFullscreen(wm.windows[ev.Window], ev.Window) 1399 | case 1: // add 1400 | wm.fullscreen(wm.windows[ev.Window], ev.Window) 1401 | case 2: // toggle 1402 | wm.toggleFullScreen(ev.Window) 1403 | } 1404 | break 1405 | } 1406 | if prop1 == uint32(fullscreenAtom) || prop2 == uint32(fullscreenAtom) { 1407 | fmt.Println("Fullscreen request! Action:", action) 1408 | 1409 | switch action { 1410 | case 0: // remove 1411 | wm.disableFullscreen(wm.windows[ev.Window], ev.Window) 1412 | case 1: // add 1413 | wm.fullscreen(wm.windows[ev.Window], ev.Window) 1414 | case 2: // toggle 1415 | wm.toggleFullScreen(ev.Window) 1416 | } 1417 | } 1418 | } 1419 | 1420 | default: 1421 | fmt.Println("event: " + event.String()) 1422 | fmt.Println(event.Bytes()) //nolint:staticcheck 1423 | } 1424 | } 1425 | } 1426 | 1427 | func (wm *WindowManager) resizeTiledX(increase bool, ev xproto.KeyPressEvent) bool { //nolint:unparam 1428 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 1429 | if err != nil { 1430 | return false 1431 | } 1432 | X := uint16(geom.X - int16(wm.config.Gap) - int16(wm.currMonitor.TilingSpace.X)) 1433 | W := geom.Width + uint16(wm.config.Gap*2) 1434 | if math.Abs(float64(uint16(wm.currMonitor.TilingSpace.X+wm.currMonitor.TilingSpace.Width)-(X+W))) <= 10 { 1435 | return false 1436 | } 1437 | 1438 | var resizeLayout ResizeLayout 1439 | ok := true 1440 | for _, win := range wm.currMonitor.CurrWorkspace.windowList { 1441 | geomwin, err := xproto.GetGeometry(wm.conn, xproto.Drawable(win.id)).Reply() 1442 | if err != nil { 1443 | continue 1444 | } 1445 | winX := uint16(geomwin.X - int16(wm.config.Gap) - int16(wm.currMonitor.TilingSpace.X)) 1446 | winY := uint16(geomwin.Y - int16(wm.config.Gap) - int16(wm.currMonitor.TilingSpace.Y)) 1447 | winH := geomwin.Height + uint16(wm.config.Gap*2) 1448 | winW := geomwin.Width + uint16(wm.config.Gap*2) 1449 | // if diff between ends of windows it less than five, they are same column 1450 | if math.Abs(float64((X+W)-(winX+winW))) <= 10 { 1451 | if increase { 1452 | winW += uint16(wm.config.Resize) 1453 | } else { 1454 | winW -= uint16(wm.config.Resize) 1455 | } 1456 | fmt.Println(winW) 1457 | } else if math.Abs(float64(int(winX)-(int(X)+int(W)))) <= 10 { 1458 | if increase { 1459 | winX += uint16(wm.config.Resize) 1460 | winW -= uint16(wm.config.Resize) 1461 | if winW < 50 { 1462 | ok = false 1463 | break 1464 | } 1465 | } else { 1466 | winX -= uint16(wm.config.Resize) 1467 | winW += uint16(wm.config.Resize) 1468 | if W < 50 { 1469 | ok = false 1470 | break 1471 | } 1472 | } 1473 | } 1474 | 1475 | fmt.Println(winX, winY, winW, winH) 1476 | resizeLayout.Windows = append(resizeLayout.Windows, RLayoutWindow{ 1477 | X: winX, 1478 | Y: winY, 1479 | Width: winW, 1480 | Height: winH, 1481 | }) 1482 | } 1483 | 1484 | if ok { 1485 | wm.currMonitor.CurrWorkspace.resized = true 1486 | wm.currMonitor.CurrWorkspace.resizedLayout = resizeLayout 1487 | wm.fitToLayout() 1488 | return true 1489 | } 1490 | return false 1491 | } 1492 | 1493 | func (wm *WindowManager) resizeTiledY(increase bool, ev xproto.KeyPressEvent) bool { //nolint:unparam 1494 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(ev.Child)).Reply() 1495 | if err != nil { 1496 | return false 1497 | } 1498 | Y := uint16(geom.Y - int16(wm.config.Gap) - int16(wm.currMonitor.TilingSpace.Y)) 1499 | H := geom.Height + uint16(wm.config.Gap*2) 1500 | if math.Abs(float64(uint16(wm.currMonitor.TilingSpace.X+wm.currMonitor.TilingSpace.Height)-(Y+H))) <= 10 { 1501 | return false 1502 | } 1503 | 1504 | var resizeLayout ResizeLayout 1505 | ok := true 1506 | for _, win := range wm.currMonitor.CurrWorkspace.windowList { 1507 | geomwin, err := xproto.GetGeometry(wm.conn, xproto.Drawable(win.id)).Reply() 1508 | if err != nil { 1509 | continue 1510 | } 1511 | winX := uint16(geomwin.X - int16(wm.config.Gap) - int16(wm.currMonitor.TilingSpace.X)) 1512 | winY := uint16(geomwin.Y - int16(wm.config.Gap) - int16(wm.currMonitor.TilingSpace.Y)) 1513 | winH := geomwin.Height + uint16(wm.config.Gap*2) 1514 | winW := geomwin.Width + uint16(wm.config.Gap*2) 1515 | // if diff between ends of windows it less than five, they are same column 1516 | if math.Abs(float64((int(Y)+int(H))-(int(winY)+int(winH)))) <= 10 { 1517 | if increase { 1518 | winH += uint16(wm.config.Resize) 1519 | } else { 1520 | winH -= uint16(wm.config.Resize) 1521 | } 1522 | fmt.Println(winW) 1523 | } else if math.Abs(float64(int(winY)-(int(Y)+int(H)))) <= 10 { 1524 | if increase { 1525 | winY += uint16(wm.config.Resize) 1526 | winH -= uint16(wm.config.Resize) 1527 | if winH < 50 { 1528 | ok = false 1529 | break 1530 | } 1531 | } else { 1532 | winY -= uint16(wm.config.Resize) 1533 | winH += uint16(wm.config.Resize) 1534 | if H < 50 { 1535 | ok = false 1536 | break 1537 | } 1538 | } 1539 | } 1540 | 1541 | fmt.Println(winX, winY, winW, winH) 1542 | resizeLayout.Windows = append(resizeLayout.Windows, RLayoutWindow{ 1543 | X: winX, 1544 | Y: winY, 1545 | Width: winW, 1546 | Height: winH, 1547 | }) 1548 | } 1549 | 1550 | if ok { 1551 | wm.currMonitor.CurrWorkspace.resized = true 1552 | wm.currMonitor.CurrWorkspace.resizedLayout = resizeLayout 1553 | wm.fitToLayout() 1554 | return true 1555 | } 1556 | return false 1557 | } 1558 | 1559 | func (wm *WindowManager) internAtom(name string) (xproto.Atom, error) { 1560 | reply, err := xproto.InternAtom(wm.conn, true, uint16(len(name)), name).Reply() 1561 | if err != nil { 1562 | return 0, err 1563 | } 1564 | return reply.Atom, nil 1565 | } 1566 | 1567 | func (wm *WindowManager) declareSupportedAtoms() { 1568 | // List the names of EWMH atoms your WM supports 1569 | atomNames := []string{ 1570 | "_NET_SUPPORTED", 1571 | "_NET_WM_STATE", 1572 | "_NET_WM_NAME", 1573 | "_WM_NAME", 1574 | "_NET_WM_STATE_FULLSCREEN", 1575 | "_NET_CURRENT_DESKTOP", 1576 | "_NET_NUMBER_OF_DESKTOPS", 1577 | "_NET_ACTIVE_WINDOW", 1578 | "_NET_WM_DESKTOP", 1579 | "_NET_CLIENT_LIST", 1580 | "_NET_CLOSE_WINDOW", 1581 | "_NET_WM_MOVERESIZE", 1582 | "_NET_WM_STATE_MAXIMIZED_HORZ", 1583 | "_NET_WM_STATE_MAXIMIZED_VERT", 1584 | } 1585 | 1586 | atoms := make([]xproto.Atom, 0, len(atomNames)) 1587 | for _, name := range atomNames { 1588 | atom, err := xproto.InternAtom(wm.conn, false, uint16(len(name)), name).Reply() 1589 | if err != nil { 1590 | slog.Error("Intern atom", "name", name, "err", err) 1591 | continue 1592 | } 1593 | wm.atoms[name] = atom.Atom 1594 | atoms = append(atoms, atom.Atom) 1595 | } 1596 | 1597 | // Build the property data 1598 | data := make([]byte, 4*len(atoms)) 1599 | for i, atom := range atoms { 1600 | binary.LittleEndian.PutUint32(data[i*4:], uint32(atom)) 1601 | } 1602 | 1603 | // Set the _NET_SUPPORTED property 1604 | err := xproto.ChangePropertyChecked( 1605 | wm.conn, 1606 | xproto.PropModeReplace, 1607 | wm.root, 1608 | wm.atoms["_NET_SUPPORTED"], 1609 | xproto.AtomAtom, 1610 | 32, 1611 | uint32(len(atoms)), 1612 | data, 1613 | ).Check() 1614 | if err != nil { 1615 | slog.Error("Couldn't set _NET_SUPPORTED", "err", err) 1616 | } 1617 | } 1618 | 1619 | func focusWindow(conn *xgb.Conn, win xproto.Window) { 1620 | err := xproto.SetInputFocusChecked( 1621 | conn, 1622 | xproto.InputFocusPointerRoot, // or InputFocusNone / InputFocusParent 1623 | win, 1624 | xproto.TimeCurrentTime, 1625 | ).Check() 1626 | if err != nil { 1627 | fmt.Println("Error focusing window:", err) 1628 | } 1629 | } 1630 | 1631 | func swapWindows(arr *[]*Window, first int, last int) { 1632 | (*arr)[first], (*arr)[last] = (*arr)[last], (*arr)[first] 1633 | } 1634 | 1635 | func swapWindowsID(arr *[]*Window, first xproto.Window, last xproto.Window) { 1636 | var res1 int 1637 | var res2 int 1638 | for i, win := range *arr { 1639 | if win.id == first { //nolint:staticcheck 1640 | res1 = i 1641 | } else if win.id == last { 1642 | res2 = i 1643 | } 1644 | } 1645 | swapWindows(arr, res1, res2) 1646 | } 1647 | 1648 | func remove(arr *[]*Window, id xproto.Window) { 1649 | if len(*arr) == 1 { 1650 | *arr = []*Window{} 1651 | } 1652 | for index := range *arr { 1653 | if (*arr)[index].id == id { 1654 | *arr = append((*arr)[:index], (*arr)[index+1:]...) 1655 | return 1656 | } 1657 | } 1658 | } 1659 | 1660 | func runCommand(cmdStr string) { 1661 | parser := shellwords.NewParser() 1662 | args, err := parser.Parse(cmdStr) 1663 | if err != nil { 1664 | slog.Error("Parsing error:", "error:", err) 1665 | return 1666 | } 1667 | if len(args) == 0 { 1668 | return 1669 | } 1670 | if len(args) < 2 { 1671 | cmd := exec.Command(args[0]) 1672 | _ = cmd.Start() 1673 | return 1674 | } 1675 | cmd := exec.Command(args[0], args[1:]...) 1676 | _ = cmd.Start() 1677 | } 1678 | 1679 | func (wm *WindowManager) getBar(vals []byte) (int, int, int, int) { 1680 | // calculates where the bar is (more explanatary in createTilingSpace) 1681 | 1682 | var maxLeft, maxRight, maxTop, maxBottom int 1683 | left := int(binary.LittleEndian.Uint32(vals[0:4])) 1684 | right := int(binary.LittleEndian.Uint32(vals[4:8])) 1685 | top := int(binary.LittleEndian.Uint32(vals[8:12])) 1686 | bottom := int(binary.LittleEndian.Uint32(vals[12:16])) 1687 | 1688 | if left > maxLeft { 1689 | maxLeft = left 1690 | } 1691 | if right > maxRight { 1692 | maxRight = right 1693 | } 1694 | if top > maxTop { 1695 | maxTop = top 1696 | } 1697 | if bottom > maxBottom { 1698 | maxBottom = bottom 1699 | } 1700 | return maxLeft, maxRight, maxTop, maxBottom 1701 | } 1702 | 1703 | func (wm *WindowManager) createTilingSpace() { 1704 | // look at all windows and if it has the property _NET_WM_STRUT_PARTIAL (what most bars have) it means that it 1705 | // should be worked around 1706 | windows, _ := xproto.QueryTree(wm.conn, wm.root).Reply() 1707 | X := 0 1708 | Y := 0 1709 | width := wm.currMonitor.Width 1710 | height := wm.currMonitor.Height 1711 | fmt.Println("CURR MON VALS X", wm.currMonitor.X, "Y", wm.currMonitor.Y) 1712 | 1713 | for _, window := range windows.Children { 1714 | geom, err := xproto.GetGeometry(wm.conn, xproto.Drawable(window)).Reply() 1715 | if err != nil { 1716 | continue 1717 | } 1718 | 1719 | mon := wm.currMonitor 1720 | if geom.X < mon.X || geom.X > mon.X+int16(mon.Width) || geom.Y < mon.Y || geom.Y > mon.Y+int16(mon.Height) { 1721 | continue 1722 | } 1723 | 1724 | attributes, err := xproto.GetWindowAttributes(wm.conn, window).Reply() 1725 | if err != nil { 1726 | continue 1727 | } 1728 | if attributes.MapState == xproto.MapStateViewable { 1729 | atom := wm.atoms["_NET_WM_STRUT_PARTIAL"] 1730 | prop, err := xproto.GetProperty(wm.conn, false, window, atom, xproto.AtomCardinal, 0, 12). 1731 | Reply() 1732 | 1733 | if err != nil || prop == nil || prop.ValueLen < 4 { 1734 | continue 1735 | } 1736 | 1737 | vals := prop.Value 1738 | if len(vals) < 16 { 1739 | continue // need at least 4 uint32s 1740 | } 1741 | left, right, top, bottom := wm.getBar(vals) 1742 | 1743 | // create space to work around bar (if there is one) 1744 | X = left 1745 | Y = top 1746 | width = uint16(int(wm.currMonitor.Width) - left - right) 1747 | height = uint16(int(wm.currMonitor.Height) - top - bottom) 1748 | 1749 | // TODO: support multiple bars 1750 | break 1751 | } 1752 | } 1753 | 1754 | fmt.Println("tiling container:", "X:", X, "Y:", Y, "Width:", width, "Height:", height) 1755 | wm.currMonitor.TilingSpace = Space{ 1756 | X: int(wm.currMonitor.X) + X + int(wm.config.OuterGap), 1757 | Y: int(wm.currMonitor.Y) + Y + int(wm.config.OuterGap), 1758 | Width: int(width-6) - (int(wm.config.OuterGap) * 2), 1759 | Height: int(height-6) - (int(wm.config.OuterGap) * 2), 1760 | } 1761 | } 1762 | 1763 | func (wm *WindowManager) fitToLayout() { 1764 | if !wm.currMonitor.CurrWorkspace.tiling { 1765 | return 1766 | } 1767 | // if there are more than 4 windows then just don't do it 1768 | 1769 | windowNum := len(wm.currMonitor.CurrWorkspace.windowList) 1770 | 1771 | if _, ok := wm.config.lyts[windowNum]; !ok { 1772 | return 1773 | } 1774 | 1775 | if len(wm.config.lyts[windowNum])-1 < wm.currMonitor.layoutIndex && 1776 | len(wm.config.lyts[windowNum]) > 0 { 1777 | wm.currMonitor.CurrWorkspace.layoutIndex = 0 1778 | wm.currMonitor.layoutIndex = 0 1779 | } 1780 | 1781 | if windowNum > len(wm.config.lyts) || windowNum < 1 || 1782 | windowNum > len(wm.config.lyts[windowNum][wm.currMonitor.layoutIndex].Windows) { 1783 | fmt.Println( 1784 | "too many or too few windows to fit to layout in workspace", 1785 | wm.currMonitor.workspaceIndex+1, 1786 | ) 1787 | return 1788 | } 1789 | wm.createTilingSpace() 1790 | layout := wm.config.lyts[windowNum][wm.currMonitor.layoutIndex] 1791 | if wm.currMonitor.CurrWorkspace.resized && len(wm.currMonitor.CurrWorkspace.resizedLayout.Windows) != windowNum { 1792 | wm.currMonitor.CurrWorkspace.resized = false 1793 | wm.currMonitor.CurrWorkspace.resizedLayout = ResizeLayout{} 1794 | } 1795 | fmt.Println("fit to layout") 1796 | fmt.Println(wm.currMonitor.CurrWorkspace.windowList) 1797 | // fmt.Println(wm.currMonitor.CurrWorkspace.windows) 1798 | // fmt.Println(len(wm.currMonitor.CurrWorkspace.windows)) 1799 | // for each window put it in its place and size specified by that layout 1800 | fullscreen := []xproto.Window{} 1801 | for i, WindowData := range wm.currMonitor.CurrWorkspace.windowList { 1802 | fmt.Println(WindowData) 1803 | if WindowData.Fullscreen { 1804 | fullscreen = append(fullscreen, WindowData.id) 1805 | continue 1806 | } 1807 | if wm.currMonitor.CurrWorkspace.resized { 1808 | layoutWindow := wm.currMonitor.CurrWorkspace.resizedLayout.Windows[i] 1809 | X := uint32(layoutWindow.X) + wm.config.Gap + uint32(wm.currMonitor.TilingSpace.X) 1810 | Y := uint32(layoutWindow.Y) + wm.config.Gap + uint32(wm.currMonitor.TilingSpace.Y) 1811 | Width := uint32(layoutWindow.Width) - (wm.config.Gap * 2) 1812 | Height := uint32(layoutWindow.Height) - (wm.config.Gap * 2) 1813 | fmt.Println( 1814 | "window:", 1815 | WindowData.id, 1816 | "X:", 1817 | X, 1818 | "rounded:", 1819 | "Y:", 1820 | Y, 1821 | "Width:", 1822 | Width, 1823 | "Height:", 1824 | Height, 1825 | ) 1826 | wm.configureWindow(WindowData.id, int(X), int(Y), int(Width), int(Height)) 1827 | } else { 1828 | layoutWindow := layout.Windows[i] 1829 | // because we use percentages we have to times the width and height of the tiling space to get the raw 1830 | // value, it is simple maths to do the gap, I shouldn't have to explain it 1831 | X := wm.currMonitor.TilingSpace.X + int((float64(wm.currMonitor.TilingSpace.Width) * layoutWindow.XPercentage)) + int(wm.config.Gap) 1832 | Y := wm.currMonitor.TilingSpace.Y + int((float64(wm.currMonitor.TilingSpace.Height) * layoutWindow.YPercentage)) + int(wm.config.Gap) 1833 | Width := (float64(wm.currMonitor.TilingSpace.Width) * layoutWindow.WidthPercentage) - float64(wm.config.Gap*2) 1834 | Height := (float64(wm.currMonitor.TilingSpace.Height) * layoutWindow.HeightPercentage) - float64(wm.config.Gap*2) 1835 | fmt.Println("window:", WindowData.id, "X:", X, "rounded:", int(math.Round(Width)), 1836 | "Y:", Y, "Width:", Width, "Height:", Height) 1837 | wm.configureWindow(WindowData.id, X, Y, int(math.Round(Width)), int(math.Round(Height))) 1838 | } 1839 | } 1840 | if len(fullscreen) > 0 { 1841 | for _, win := range fullscreen { 1842 | xproto.ConfigureWindow( 1843 | wm.conn, 1844 | win, 1845 | xproto.ConfigWindowStackMode, 1846 | []uint32{xproto.StackModeAbove}, 1847 | ) 1848 | wm.fullscreen(wm.windows[win], win) 1849 | } 1850 | } 1851 | } 1852 | 1853 | func (wm *WindowManager) configureWindow(frame xproto.Window, x, y, width, height int) { 1854 | // configure the window to how it wants to be 1855 | _ = xproto.ConfigureWindowChecked( 1856 | wm.conn, 1857 | frame, 1858 | xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, 1859 | []uint32{ 1860 | uint32(x), uint32(y), uint32(width), uint32(height), 1861 | }, 1862 | ). 1863 | Check() 1864 | } 1865 | 1866 | func (wm *WindowManager) toggleTiling() { 1867 | if !wm.currMonitor.CurrWorkspace.detachTiling { 1868 | if !wm.currMonitor.tiling { 1869 | wm.currMonitor.tiling = true 1870 | wm.enableTiling() 1871 | } else { 1872 | wm.currMonitor.tiling = false 1873 | wm.disableTiling() 1874 | } 1875 | } else { 1876 | if !wm.currMonitor.CurrWorkspace.tiling { 1877 | wm.enableTiling() 1878 | } else { 1879 | wm.disableTiling() 1880 | } 1881 | } 1882 | } 1883 | 1884 | func (wm *WindowManager) disableTiling() { 1885 | wm.currMonitor.CurrWorkspace.tiling = false 1886 | fmt.Println("DISABLED TILING") 1887 | // restore windows to there previous state (before tiling) 1888 | for _, window := range wm.currMonitor.CurrWorkspace.windowList { 1889 | wm.configureWindow(window.id, window.X, window.Y, window.Width, window.Height) 1890 | } 1891 | wm.setNetWorkArea() 1892 | } 1893 | 1894 | func (wm *WindowManager) enableTiling() { 1895 | wm.currMonitor.CurrWorkspace.tiling = true 1896 | // make sure no windows are fullscreened and that there state is saved (so it can be restored later if/when the user 1897 | // disables tiling) 1898 | for i, window := range wm.currMonitor.CurrWorkspace.windowList { 1899 | fmt.Println(window.id) 1900 | attr, _ := xproto.GetGeometry(wm.conn, xproto.Drawable(window.id)).Reply() 1901 | wm.currMonitor.CurrWorkspace.windowList[i] = &Window{ 1902 | id: window.id, 1903 | X: int(attr.X), 1904 | Y: int(attr.Y), 1905 | Width: int(attr.Width), 1906 | Height: int(attr.Height), 1907 | Fullscreen: false, 1908 | Client: window.Client, 1909 | } 1910 | } 1911 | fmt.Println("tiling") 1912 | // put the windows in the right tiling layout in the right space 1913 | wm.createTilingSpace() 1914 | wm.fitToLayout() 1915 | wm.setNetWorkArea() 1916 | } 1917 | 1918 | func (wm *WindowManager) toggleFullScreen(child xproto.Window) { 1919 | win := wm.windows[child] 1920 | if win != nil { 1921 | if win.Fullscreen { 1922 | wm.disableFullscreen(win, child) 1923 | } else { 1924 | wm.fullscreen(win, child) 1925 | } 1926 | } 1927 | } 1928 | 1929 | func (wm *WindowManager) disableFullscreen(win *Window, child xproto.Window) { 1930 | fmt.Println("DISABLING FULL SCREEN") 1931 | wm.windows[child].Fullscreen = false 1932 | for i, window := range wm.currMonitor.CurrWorkspace.windowList { 1933 | if window.id == child { 1934 | wm.currMonitor.CurrWorkspace.windowList[i].Fullscreen = false 1935 | } 1936 | fmt.Println(window.Fullscreen) 1937 | } 1938 | // set the frame back to what it used to be same with the client, but sort out tiling layout anyway just in case 1939 | err := xproto.ConfigureWindowChecked( 1940 | wm.conn, 1941 | child, 1942 | xproto.ConfigWindowX|xproto.ConfigWindowY| 1943 | xproto.ConfigWindowWidth|xproto.ConfigWindowHeight|xproto.ConfigWindowBorderWidth, 1944 | []uint32{ 1945 | uint32(win.X), 1946 | uint32(win.Y), 1947 | uint32(win.Width), 1948 | uint32(win.Height), 1949 | wm.config.BorderWidth, 1950 | }, 1951 | ).Check() 1952 | if err != nil { 1953 | slog.Error("Couldn't un-fullscreen window", "error: ", err) 1954 | } 1955 | wm.removeFullScreenEWMH(child) 1956 | wm.fitToLayout() 1957 | } 1958 | 1959 | func (wm *WindowManager) setFullScreenEWMH(win xproto.Window) { 1960 | // Intern the atoms we need 1961 | netWmState := wm.atoms["_NET_WM_STATE"] 1962 | netWmStateFullscreen := wm.atoms["_NET_WM_STATE_FULLSCREEN"] 1963 | 1964 | data := make([]byte, 4) 1965 | binary.LittleEndian.PutUint32(data, uint32(netWmStateFullscreen)) 1966 | // Change the window property to include fullscreen 1967 | err := xproto.ChangePropertyChecked( 1968 | wm.conn, 1969 | xproto.PropModeReplace, 1970 | win, 1971 | netWmState, 1972 | xproto.AtomAtom, 1973 | 32, 1974 | 1, 1975 | data, 1976 | ).Check() 1977 | if err != nil { 1978 | return 1979 | } 1980 | } 1981 | 1982 | func (wm *WindowManager) removeFullScreenEWMH(win xproto.Window) { 1983 | netWmState := wm.atoms["_NET_WM_STATE"] 1984 | // Change the window property to remove fullscreen 1985 | err := xproto.ChangePropertyChecked( 1986 | wm.conn, 1987 | xproto.PropModeReplace, 1988 | win, 1989 | netWmState, 1990 | xproto.AtomAtom, 1991 | 32, 1992 | 0, 1993 | nil, 1994 | ).Check() 1995 | if err != nil { 1996 | return 1997 | } 1998 | } 1999 | 2000 | func (wm *WindowManager) fullscreen(_ *Window, child xproto.Window) { 2001 | // set window state so it can be restored later then configure window to be full width and height, sam with client, 2002 | // also take away border 2003 | wm.windows[child].Fullscreen = true 2004 | for i, window := range wm.currMonitor.CurrWorkspace.windowList { 2005 | if window.id == child { 2006 | wm.currMonitor.CurrWorkspace.windowList[i].Fullscreen = true 2007 | } 2008 | } 2009 | xproto.ConfigureWindow( 2010 | wm.conn, 2011 | child, 2012 | xproto.ConfigWindowStackMode, 2013 | []uint32{xproto.StackModeAbove}, 2014 | ) 2015 | attr, _ := xproto.GetGeometry(wm.conn, xproto.Drawable(child)).Reply() 2016 | win := wm.windows[child] 2017 | win.X = int(attr.X) 2018 | win.Y = int(attr.Y) 2019 | win.Width = int(attr.Width) 2020 | win.Height = int(attr.Height) 2021 | err := xproto.ConfigureWindowChecked( 2022 | wm.conn, 2023 | child, 2024 | xproto.ConfigWindowX|xproto.ConfigWindowY| 2025 | xproto.ConfigWindowWidth|xproto.ConfigWindowHeight|xproto.ConfigWindowBorderWidth, 2026 | []uint32{ 2027 | uint32(wm.currMonitor.X), 2028 | uint32(wm.currMonitor.Y), 2029 | uint32(wm.currMonitor.Width), 2030 | uint32(wm.currMonitor.Height), 2031 | 0, 2032 | }, 2033 | ).Check() 2034 | if err != nil { 2035 | slog.Error("Couldn't fullscreen window", "error:", err) 2036 | } 2037 | wm.setFullScreenEWMH(child) 2038 | } 2039 | 2040 | func (wm *WindowManager) broadcastWorkspaceCount() { 2041 | // EMWH things for bars to show workspaces 2042 | count := wm.currMonitor.workspaceIndex + 1 2043 | otherCount := 0 2044 | for i, workspace := range wm.currMonitor.Workspaces { 2045 | if len(workspace.windowList) > 0 { 2046 | otherCount = i 2047 | } 2048 | } 2049 | otherCount++ 2050 | if otherCount > count { 2051 | count = otherCount 2052 | } 2053 | data := make([]byte, 4) 2054 | binary.LittleEndian.PutUint32(data, uint32(count)) 2055 | 2056 | netNumberAtom, _ := xproto.InternAtom( 2057 | wm.conn, 2058 | true, 2059 | uint16(len("_NET_NUMBER_OF_DESKTOPS")), 2060 | "_NET_NUMBER_OF_DESKTOPS", 2061 | ). 2062 | Reply() 2063 | cardinalAtom, _ := xproto.InternAtom(wm.conn, true, uint16(len("CARDINAL")), "CARDINAL").Reply() 2064 | 2065 | _ = xproto.ChangePropertyChecked( 2066 | wm.conn, 2067 | xproto.PropModeReplace, 2068 | wm.root, 2069 | netNumberAtom.Atom, 2070 | cardinalAtom.Atom, 2071 | 32, 2072 | 1, 2073 | data, 2074 | ).Check() 2075 | } 2076 | 2077 | func (wm *WindowManager) broadcastWorkspace(num int) { 2078 | // EMWH thing for bars to show workspaces 2079 | data := make([]byte, 4) 2080 | binary.LittleEndian.PutUint32(data, uint32(num)) 2081 | 2082 | netCurrentDesktopAtom, err := xproto.InternAtom( 2083 | wm.conn, 2084 | false, 2085 | uint16(len("_NET_CURRENT_DESKTOP")), 2086 | "_NET_CURRENT_DESKTOP", 2087 | ). 2088 | Reply() 2089 | if err != nil { 2090 | slog.Error("Intern _NET_CURRENT_DESKTOP", "error:", err) 2091 | return 2092 | } 2093 | 2094 | cardinalAtom, err := xproto.InternAtom(wm.conn, true, uint16(len("CARDINAL")), "CARDINAL"). 2095 | Reply() 2096 | if err != nil { 2097 | slog.Error("Intern CARDINAL", "error:", err) 2098 | return 2099 | } 2100 | fmt.Println(netCurrentDesktopAtom.Atom) 2101 | fmt.Println(cardinalAtom.Atom) 2102 | err = xproto.ChangePropertyChecked( 2103 | wm.conn, 2104 | xproto.PropModeReplace, 2105 | wm.root, 2106 | netCurrentDesktopAtom.Atom, // must not be 0 2107 | cardinalAtom.Atom, // must not be 0 2108 | 32, 2109 | 1, 2110 | data, 2111 | ).Check() 2112 | if err != nil { 2113 | slog.Error("Couldn't set _NET_CURRENT_DESKTOP", "error:", err) 2114 | } 2115 | 2116 | wm.broadcastWorkspaceCount() 2117 | } 2118 | 2119 | func (wm *WindowManager) switchWorkspace(workspace int) { 2120 | if workspace > len(wm.currMonitor.Workspaces) { 2121 | return 2122 | } 2123 | 2124 | if !wm.config.WorkspaceAutoBackAndForth && workspace == wm.currMonitor.workspaceIndex { 2125 | return 2126 | } 2127 | 2128 | // WorkspaceAutoBackAndForth is enabled 2129 | if workspace == wm.currMonitor.workspaceIndex { 2130 | // switch back to last workspace 2131 | workspace = wm.currMonitor.lastWorkspaceIndex 2132 | slog.Debug("Switch back to", "workspace", workspace) 2133 | } else { 2134 | // remember last workspace for next switch 2135 | slog.Debug("Remember", "workspace", wm.currMonitor.workspaceIndex) 2136 | } 2137 | wm.currMonitor.lastWorkspaceIndex = wm.currMonitor.workspaceIndex 2138 | 2139 | // unmap all windows in current workspace 2140 | for _, frame := range wm.currMonitor.CurrWorkspace.windowList { 2141 | xproto.UnmapWindowChecked(wm.conn, frame.id) 2142 | } 2143 | 2144 | // swap workspace 2145 | wm.currMonitor.CurrWorkspace = &wm.currMonitor.Workspaces[workspace] 2146 | wm.currMonitor.workspaceIndex = workspace 2147 | 2148 | // map all the windows in the other workspace 2149 | for _, frame := range wm.currMonitor.CurrWorkspace.windowList { 2150 | xproto.MapWindowChecked(wm.conn, frame.id) 2151 | } 2152 | 2153 | wm.conn.Sync() 2154 | 2155 | // update tiling 2156 | if !wm.currMonitor.CurrWorkspace.detachTiling { 2157 | if wm.currMonitor.tiling && !wm.currMonitor.CurrWorkspace.tiling { 2158 | wm.enableTiling() 2159 | } else if !wm.currMonitor.tiling && wm.currMonitor.CurrWorkspace.tiling { 2160 | wm.disableTiling() 2161 | } 2162 | } 2163 | wm.broadcastWorkspace(workspace) 2164 | wm.currMonitor.layoutIndex = wm.currMonitor.CurrWorkspace.layoutIndex 2165 | } 2166 | 2167 | func (wm *WindowManager) sendWmDelete(conn *xgb.Conn, window xproto.Window) error { 2168 | // polite EMWH way of telling the window to delete itself 2169 | wmProtocolsAtom, _ := xproto.InternAtom(conn, true, uint16(len("WM_PROTOCOLS")), "WM_PROTOCOLS"). 2170 | Reply() 2171 | wmDeleteAtom, _ := xproto.InternAtom(conn, true, uint16(len("WM_DELETE_WINDOW")), "WM_DELETE_WINDOW"). 2172 | Reply() 2173 | 2174 | prop, err := xproto.GetProperty(conn, false, window, wmProtocolsAtom.Atom, xproto.AtomAtom, 0, (1<<32)-1). 2175 | Reply() 2176 | if err != nil || prop.Format != 32 { 2177 | return fmt.Errorf("could not get WM_PROTOCOLS %w", err) 2178 | } 2179 | 2180 | supportsDelete := false 2181 | for i := range int(prop.ValueLen) { 2182 | atom := xgb.Get32(prop.Value[i*4:]) 2183 | if xproto.Atom(atom) == wmDeleteAtom.Atom { 2184 | supportsDelete = true 2185 | break 2186 | } 2187 | } 2188 | 2189 | if !supportsDelete { 2190 | return errors.New("WM_DELETE_WINDOW not supported") 2191 | } 2192 | 2193 | ev := xproto.ClientMessageEvent{ 2194 | Format: 32, 2195 | Window: window, 2196 | Type: wmProtocolsAtom.Atom, 2197 | Data: xproto.ClientMessageDataUnionData32New( 2198 | []uint32{ 2199 | uint32(wmDeleteAtom.Atom), 2200 | uint32(xproto.TimeCurrentTime), 2201 | 0, 0, 0, 2202 | }, 2203 | ), 2204 | } 2205 | 2206 | return xproto.SendEventChecked( 2207 | conn, 2208 | false, 2209 | window, 2210 | xproto.EventMaskNoEvent, 2211 | string(ev.Bytes()), 2212 | ).Check() 2213 | } 2214 | 2215 | func (wm *WindowManager) onLeaveNotify(event xproto.LeaveNotifyEvent) { 2216 | // change border color when you leave a window 2217 | Col := wm.config.BorderUnactive 2218 | 2219 | err := xproto.ChangeWindowAttributesChecked( 2220 | wm.conn, 2221 | event.Event, 2222 | xproto.CwBorderPixel, 2223 | []uint32{ 2224 | Col, // border color 2225 | }, 2226 | ).Check() 2227 | if err != nil { 2228 | slog.Error("Couldn't remove focus from window", "error:", err) 2229 | } 2230 | } 2231 | 2232 | func setFrameWindowType(conn *xgb.Conn, win xproto.Window) { 2233 | atomWindowType, _ := xproto.InternAtom(conn, true, uint16(len("_NET_WM_WINDOW_TYPE")), "_NET_WM_WINDOW_TYPE"). 2234 | Reply() 2235 | atomNormal, _ := xproto.InternAtom( 2236 | conn, 2237 | true, 2238 | uint16(len("_NET_WM_WINDOW_TYPE_NORMAL")), 2239 | "_NET_WM_WINDOW_TYPE_NORMAL", 2240 | ). 2241 | Reply() 2242 | 2243 | xproto.ChangeProperty(conn, 2244 | xproto.PropModeReplace, 2245 | win, 2246 | atomWindowType.Atom, 2247 | xproto.AtomAtom, 2248 | 32, 2249 | 1, 2250 | []byte{ 2251 | byte(atomNormal.Atom), 2252 | byte(atomNormal.Atom >> 8), 2253 | byte(atomNormal.Atom >> 16), 2254 | byte(atomNormal.Atom >> 24), 2255 | }, 2256 | ) 2257 | } 2258 | 2259 | func (wm *WindowManager) setNetActiveWindow(win xproto.Window) { 2260 | atomActiveWin, _ := xproto.InternAtom(wm.conn, true, uint16(len("_NET_ACTIVE_WINDOW")), "_NET_ACTIVE_WINDOW"). 2261 | Reply() 2262 | 2263 | // Convert uint32 to []byte 2264 | buf := new(bytes.Buffer) 2265 | _ = binary.Write(buf, binary.LittleEndian, win) 2266 | 2267 | xproto.ChangeProperty(wm.conn, 2268 | xproto.PropModeReplace, 2269 | wm.root, // Set on the root window 2270 | atomActiveWin.Atom, // _NET_ACTIVE_WINDOW 2271 | xproto.AtomWindow, // Type: WINDOW 2272 | 32, // Format: 32-bit 2273 | 1, // Only one window 2274 | buf.Bytes(), // Here's the []byte version 2275 | ) 2276 | } 2277 | 2278 | func (wm *WindowManager) setNetWorkArea() { 2279 | atomWorkArea, err := xproto.InternAtom(wm.conn, true, uint16(len("_NET_WORKAREA")), "_NET_WORKAREA"). 2280 | Reply() 2281 | if err != nil { 2282 | // handle error properly here 2283 | return 2284 | } 2285 | 2286 | buf := new(bytes.Buffer) 2287 | 2288 | spaceX, spaceY, spaceWidth, spaceHeight := wm.currMonitor.TilingSpace.X, wm.currMonitor.TilingSpace.Y, wm.currMonitor.TilingSpace.Width, wm.currMonitor.TilingSpace.Height //nolint: lll 2289 | 2290 | for _, wksp := range wm.currMonitor.Workspaces { 2291 | if !wksp.tiling { 2292 | _ = binary.Write(buf, binary.LittleEndian, uint32(0)) 2293 | _ = binary.Write(buf, binary.LittleEndian, uint32(0)) 2294 | _ = binary.Write(buf, binary.LittleEndian, uint32(wm.currMonitor.Width)) 2295 | _ = binary.Write(buf, binary.LittleEndian, uint32(wm.currMonitor.Height)) 2296 | } else { 2297 | _ = binary.Write(buf, binary.LittleEndian, uint32(spaceX)) 2298 | _ = binary.Write(buf, binary.LittleEndian, uint32(spaceY)) 2299 | _ = binary.Write(buf, binary.LittleEndian, uint32(spaceWidth)) 2300 | _ = binary.Write(buf, binary.LittleEndian, uint32(spaceHeight)) 2301 | } 2302 | } 2303 | 2304 | // Number of 32-bit CARDINAL values: 4 values per workspace 2305 | numValues := uint32(4 * len(wm.currMonitor.Workspaces)) 2306 | 2307 | err = xproto.ChangePropertyChecked( 2308 | wm.conn, 2309 | xproto.PropModeReplace, 2310 | wm.root, 2311 | atomWorkArea.Atom, 2312 | xproto.AtomCardinal, 2313 | 32, 2314 | numValues, 2315 | buf.Bytes(), 2316 | ).Check() 2317 | if err != nil { 2318 | slog.Error("Couldn't set the work area", "error:", err) 2319 | } 2320 | } 2321 | 2322 | func (wm *WindowManager) setNetClientList() { 2323 | atomClientList, _ := xproto.InternAtom(wm.conn, true, uint16(len("_NET_CLIENT_LIST")), "_NET_CLIENT_LIST"). 2324 | Reply() 2325 | 2326 | buf := new(bytes.Buffer) 2327 | for _, info := range wm.windows { 2328 | _ = binary.Write(buf, binary.LittleEndian, info.Client) 2329 | } 2330 | 2331 | xproto.ChangeProperty(wm.conn, 2332 | xproto.PropModeReplace, 2333 | wm.root, 2334 | atomClientList.Atom, 2335 | xproto.AtomWindow, 2336 | 32, 2337 | uint32(len(wm.windows)), 2338 | buf.Bytes(), 2339 | ) 2340 | } 2341 | 2342 | func (wm *WindowManager) onEnterNotify(event xproto.EnterNotifyEvent) { 2343 | // set focus when we enter a window and change border color 2344 | err := xproto.SetInputFocusChecked(wm.conn, xproto.InputFocusPointerRoot, event.Event, xproto.TimeCurrentTime). 2345 | Check() 2346 | if err != nil { 2347 | slog.Error("Couldn't set input focus", "error", err) 2348 | } 2349 | Col := wm.config.BorderActive 2350 | err = xproto.ChangeWindowAttributesChecked( 2351 | wm.conn, 2352 | event.Event, 2353 | xproto.CwBorderPixel, 2354 | []uint32{ 2355 | Col, // border color 2356 | }, 2357 | ).Check() 2358 | if err != nil { 2359 | slog.Error("Couldn't set focus on window", "error:", err) 2360 | } 2361 | wm.setNetActiveWindow(event.Event) 2362 | } 2363 | 2364 | func (wm *WindowManager) findWindow(window xproto.Window) (bool, int, xproto.Window) { //nolint:unparam 2365 | fmt.Println("FINDING WINDOW", window) 2366 | // look through all workspaces and windows to find a window (this is for if a window is deleted by a window from 2367 | // another workspace, we need to search for it) 2368 | for i, workspace := range wm.currMonitor.Workspaces { 2369 | if i == wm.currMonitor.workspaceIndex { 2370 | continue 2371 | } 2372 | 2373 | for _, frame := range workspace.windowList { 2374 | fmt.Println(frame.Width, frame.id) 2375 | if frame.id == window { 2376 | return true, i, frame.id 2377 | } 2378 | } 2379 | } 2380 | return false, 0, 0 2381 | } 2382 | 2383 | func (wm *WindowManager) onUnmapNotify(event xproto.UnmapNotifyEvent) { 2384 | if event.Event == wm.root { 2385 | slog.Info("Ignore UnmapNotify for reparented pre-existing window") 2386 | fmt.Println(event.Window) 2387 | return 2388 | } 2389 | 2390 | found := false 2391 | for _, win := range wm.currMonitor.CurrWorkspace.windowList { 2392 | if win.id == event.Window { 2393 | found = true 2394 | break 2395 | } 2396 | } 2397 | if !found { 2398 | fmt.Println("IN UNMAPPING COULDNT FIND WIN NOW SEARCHING") 2399 | ok, index, _ := wm.findWindow(event.Window) 2400 | if !ok { 2401 | slog.Info("Couldn't unmap since window wasn't in clients") 2402 | fmt.Println(event.Window) 2403 | return 2404 | } 2405 | wm.currMonitor.CurrWorkspace = &wm.currMonitor.Workspaces[index] 2406 | fmt.Println("IN WORKSPACE", index) 2407 | wm.unFrame(event.Window, false) 2408 | wm.currMonitor.CurrWorkspace = &wm.currMonitor.Workspaces[wm.currMonitor.workspaceIndex] 2409 | return 2410 | } 2411 | wm.unFrame(event.Window, false) 2412 | wm.fitToLayout() 2413 | } 2414 | 2415 | func (wm *WindowManager) remDestroyedWin(window xproto.Window) { 2416 | found := false 2417 | for _, win := range wm.currMonitor.CurrWorkspace.windowList { 2418 | if win.id == window { 2419 | found = true 2420 | break 2421 | } 2422 | } 2423 | if !found { 2424 | fmt.Println("IN UNMAPPING COULDNT FIND WIN NOW SEARCHING") 2425 | ok, index, _ := wm.findWindow(window) 2426 | if !ok { 2427 | slog.Info("Couldn't unmap since window wasn't in clients") 2428 | fmt.Println(window) 2429 | return 2430 | } 2431 | wm.currMonitor.CurrWorkspace = &wm.currMonitor.Workspaces[index] 2432 | fmt.Println("IN WORKSPACE", index, wm.currMonitor.CurrWorkspace.windowList) 2433 | wm.unFrame(window, false) 2434 | wm.currMonitor.CurrWorkspace = &wm.currMonitor.Workspaces[wm.currMonitor.workspaceIndex] 2435 | return 2436 | } 2437 | 2438 | wm.unFrame(window, false) 2439 | wm.fitToLayout() 2440 | } 2441 | 2442 | func (wm *WindowManager) unFrame(w xproto.Window, _ bool) { 2443 | // if it is already unmapped then no need to do it again 2444 | err := xproto.UnmapWindowChecked( 2445 | wm.conn, 2446 | w, 2447 | ).Check() 2448 | if err != nil { 2449 | slog.Error("Couldn't unmap frame", "error:", err.Error()) 2450 | } 2451 | // remove window and frame from current workspace record 2452 | remove(&wm.currMonitor.CurrWorkspace.windowList, w) 2453 | delete(wm.windows, w) 2454 | wm.setNetClientList() 2455 | 2456 | // delete window from x11 set 2457 | err = xproto.ChangeSaveSetChecked( 2458 | wm.conn, 2459 | xproto.SetModeDelete, 2460 | w, 2461 | ).Check() 2462 | if err != nil { 2463 | slog.Error("Couldn't remove window from save", "error:", err.Error()) 2464 | } 2465 | 2466 | // destroy frame 2467 | err = xproto.DestroyWindowChecked( 2468 | wm.conn, 2469 | w, 2470 | ).Check() 2471 | if err != nil { 2472 | slog.Error("Couldn't destroy frame", "error:", err.Error()) 2473 | return 2474 | } 2475 | 2476 | slog.Info("Unmapped", "window", w) 2477 | } 2478 | 2479 | func (wm *WindowManager) setWindowDesktop(win xproto.Window, desktop uint32) { 2480 | atomWmDesktop, _ := xproto.InternAtom(wm.conn, true, uint16(len("_NET_WM_DESKTOP")), "_NET_WM_DESKTOP"). 2481 | Reply() 2482 | 2483 | buf := new(bytes.Buffer) 2484 | _ = binary.Write(buf, binary.LittleEndian, desktop) 2485 | 2486 | xproto.ChangeProperty(wm.conn, 2487 | xproto.PropModeReplace, 2488 | win, // client window 2489 | atomWmDesktop.Atom, // _NET_WM_DESKTOP 2490 | xproto.AtomCardinal, // CARDINAL 2491 | 32, 2492 | 1, 2493 | buf.Bytes(), 2494 | ) 2495 | } 2496 | 2497 | func shouldIgnoreWindow(conn *xgb.Conn, win xproto.Window) bool { 2498 | // some windows don't want to be registered by the WM so we check that 2499 | 2500 | // Intern the _NET_WM_WINDOW_TYPE atom 2501 | typeAtom, err := xproto.InternAtom(conn, false, uint16(len("_NET_WM_WINDOW_TYPE")), "_NET_WM_WINDOW_TYPE"). 2502 | Reply() 2503 | if err != nil { 2504 | slog.Error("Error getting _NET_WM_WINDOW_TYPE atom", "error", err) 2505 | return false 2506 | } 2507 | 2508 | // Get the _NET_WM_WINDOW_TYPE property for the window 2509 | actualType, err := xproto.GetProperty(conn, false, win, typeAtom.Atom, xproto.AtomAtom, 0, 1). 2510 | Reply() 2511 | if err != nil { 2512 | slog.Error("Error getting _NET_WM_WINDOW_TYPE property", "error", err) 2513 | return false 2514 | } 2515 | 2516 | if len(actualType.Value) == 0 { 2517 | return false 2518 | } 2519 | 2520 | // Check if the window has the _NET_WM_WINDOW_TYPE_SPLASH, _NET_WM_WINDOW_TYPE_DIALOG, 2521 | // _NET_WM_WINDOW_TYPE_NOTIFICATION, or _NET_WM_WINDOW_TYPE_DOCK 2522 | netWmSplash, err := xproto.InternAtom( 2523 | conn, 2524 | false, 2525 | uint16(len("_NET_WM_WINDOW_TYPE_SPLASH")), 2526 | "_NET_WM_WINDOW_TYPE_SPLASH", 2527 | ). 2528 | Reply() 2529 | if err != nil { 2530 | slog.Error("Error getting _NET_WM_WINDOW_TYPE_SPLASH atom", "error", err) 2531 | return false 2532 | } 2533 | netWmPanel, err := xproto.InternAtom( 2534 | conn, 2535 | false, 2536 | uint16(len("_NET_WM_WINDOW_TYPE_PANEL")), 2537 | "_NET_WM_WINDOW_TYPE_PANEL", 2538 | ). 2539 | Reply() 2540 | if err != nil { 2541 | slog.Error("Error getting _NET_WM_WINDOW_TYPE_PANEL atom", "error", err) 2542 | return false 2543 | } 2544 | 2545 | netWmTooltip, err := xproto.InternAtom( 2546 | conn, 2547 | false, 2548 | uint16(len("_NET_WM_WINDOW_TYPE_TOOLTIP")), 2549 | "_NET_WM_WINDOW_TYPE_TOOLTIP", 2550 | ). 2551 | Reply() 2552 | if err != nil { 2553 | slog.Error("Error getting _NET_WM_WINDOW_TYPE_PANEL atom", "error", err) 2554 | return false 2555 | } 2556 | 2557 | netWmDialog, err := xproto.InternAtom( 2558 | conn, 2559 | false, 2560 | uint16(len("_NET_WM_WINDOW_TYPE_DIALOG")), 2561 | "_NET_WM_WINDOW_TYPE_DIALOG", 2562 | ). 2563 | Reply() 2564 | if err != nil { 2565 | slog.Error("Error getting _NET_WM_WINDOW_TYPE_DIALOG atom", "error", err) 2566 | return false 2567 | } 2568 | 2569 | netWmNotification, err := xproto.InternAtom( 2570 | conn, 2571 | false, 2572 | uint16(len("_NET_WM_WINDOW_TYPE_NOTIFICATION")), 2573 | "_NET_WM_WINDOW_TYPE_NOTIFICATION", 2574 | ). 2575 | Reply() 2576 | if err != nil { 2577 | slog.Error("Error getting _NET_WM_WINDOW_TYPE_NOTIFICATION atom", "error", err) 2578 | return false 2579 | } 2580 | 2581 | netWmDock, err := xproto.InternAtom( 2582 | conn, 2583 | false, 2584 | uint16(len("_NET_WM_WINDOW_TYPE_DOCK")), 2585 | "_NET_WM_WINDOW_TYPE_DOCK", 2586 | ). 2587 | Reply() 2588 | if err != nil { 2589 | slog.Error("Error getting _NET_WM_WINDOW_TYPE_DOCK atom", "error", err) 2590 | return false 2591 | } 2592 | 2593 | // Check if the window type matches any of the "ignore" types 2594 | windowType := xproto.Atom(binary.LittleEndian.Uint32(actualType.Value)) 2595 | 2596 | if windowType == netWmSplash.Atom || windowType == netWmDialog.Atom || 2597 | windowType == netWmNotification.Atom || 2598 | windowType == netWmDock.Atom || 2599 | windowType == netWmPanel.Atom || 2600 | windowType == netWmTooltip.Atom { 2601 | return true 2602 | } 2603 | 2604 | return false 2605 | } 2606 | 2607 | func (wm *WindowManager) isAbove(w xproto.Window) { 2608 | fmt.Println(wm.atoms) 2609 | stateAtom, ok := wm.atoms["_NET_WM_STATE"] 2610 | if ok { 2611 | stateAboveAtom, ok := wm.atoms["_NET_WM_STATE_ABOVE"] 2612 | if ok { 2613 | // Get property 2614 | prop, err := xproto.GetProperty(wm.conn, false, w, stateAtom, 2615 | xproto.AtomAtom, 0, 1024).Reply() 2616 | if err != nil { 2617 | slog.Error("Error getting _NET_WM_STATE", "error:", err) 2618 | return 2619 | } 2620 | 2621 | // Iterate through atoms in the property 2622 | for i := 0; i+4 <= len(prop.Value); i += 4 { 2623 | atom := xproto.Atom(uint32(prop.Value[i]) | 2624 | uint32(prop.Value[i+1])<<8 | 2625 | uint32(prop.Value[i+2])<<16 | 2626 | uint32(prop.Value[i+3])<<24) 2627 | 2628 | if atom == stateAboveAtom { 2629 | xproto.ConfigureWindow( 2630 | wm.conn, 2631 | w, 2632 | xproto.ConfigWindowStackMode, 2633 | []uint32{xproto.StackModeAbove}, 2634 | ) 2635 | break 2636 | } 2637 | } 2638 | } 2639 | } 2640 | } 2641 | 2642 | func (wm *WindowManager) onMapRequest(event xproto.MapRequestEvent) { 2643 | // if there is a window to be ignored then we just map it but don't handle it 2644 | if shouldIgnoreWindow(wm.conn, event.Window) { 2645 | fmt.Println("ignored window since it is either dock, splash, dialog or notify") 2646 | err := xproto.MapWindowChecked( 2647 | wm.conn, 2648 | event.Window, 2649 | ).Check() 2650 | if err != nil { 2651 | slog.Error("Couldn't create new window id", "error:", err.Error()) 2652 | } 2653 | return 2654 | } 2655 | 2656 | // frame the window and make sure to work out the new tiling layout 2657 | wm.frame(event.Window, false) 2658 | if wm.currMonitor.CurrWorkspace.tiling { 2659 | wm.fitToLayout() 2660 | } 2661 | 2662 | wm.setWindowDesktop(event.Window, uint32(wm.currMonitor.workspaceIndex)) 2663 | } 2664 | 2665 | func (wm *WindowManager) frame(w xproto.Window, createdBeforeWM bool) { 2666 | if _, exists := wm.windows[w]; exists { 2667 | fmt.Println("Already framed", w) 2668 | return 2669 | } 2670 | BorderWidth := wm.config.BorderWidth 2671 | Col := wm.config.BorderUnactive 2672 | 2673 | // get the geometry of the window so we can match the frame to it 2674 | geometry, err := xproto.GetGeometry(wm.conn, xproto.Drawable(w)).Reply() 2675 | if err != nil { 2676 | slog.Error("Couldn't get window geometry", "error:", err.Error()) 2677 | return 2678 | } 2679 | 2680 | attribs, err := xproto.GetWindowAttributes( 2681 | wm.conn, 2682 | w, 2683 | ).Reply() 2684 | if err != nil { 2685 | slog.Error("Couldn't get window attributes", "error:", err.Error()) 2686 | return 2687 | } 2688 | 2689 | wm.isAbove(w) 2690 | 2691 | // skips 2692 | if attribs.OverrideRedirect { 2693 | fmt.Println("Skipping override-redirect window", w) 2694 | return 2695 | } 2696 | 2697 | if createdBeforeWM && attribs.MapState != xproto.MapStateViewable { 2698 | fmt.Println("Skipping unmapped pre-existing window", w) 2699 | return 2700 | } 2701 | 2702 | // map the frame 2703 | _ = xproto.MapWindowChecked( 2704 | wm.conn, 2705 | w, 2706 | ).Check() 2707 | 2708 | // center it 2709 | windowMidX := math.Round(float64(geometry.Width) / 2) 2710 | windowMidY := math.Round(float64(geometry.Height) / 2) 2711 | screenMidX := math.Round(float64(wm.currMonitor.Width) / 2) 2712 | screenMidY := math.Round(float64(wm.currMonitor.Height) / 2) 2713 | topLeftX := float64(wm.currMonitor.X) + (screenMidX - windowMidX) 2714 | topLeftY := float64(wm.currMonitor.Y) + (screenMidY - windowMidY) 2715 | 2716 | err = xproto.ConfigureWindowChecked( 2717 | wm.conn, 2718 | w, 2719 | xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, 2720 | []uint32{ 2721 | uint32(topLeftX), 2722 | uint32(topLeftY), 2723 | uint32(geometry.Width), 2724 | uint32(geometry.Height), 2725 | }, 2726 | ). 2727 | Check() 2728 | if err != nil { 2729 | slog.Error("Couldn't create new window", "error:", err.Error()) 2730 | return 2731 | } 2732 | 2733 | _ = xproto.ConfigureWindowChecked( 2734 | wm.conn, 2735 | w, 2736 | xproto.ConfigWindowBorderWidth, 2737 | []uint32{BorderWidth}, 2738 | ) 2739 | 2740 | err = xproto.ChangeWindowAttributesChecked( 2741 | wm.conn, 2742 | w, 2743 | xproto.CwBorderPixel|xproto.CwEventMask, 2744 | []uint32{ 2745 | Col, // border color 2746 | xproto.EventMaskSubstructureRedirect | 2747 | xproto.EventMaskSubstructureNotify | xproto.EventMaskKeyPress | xproto.EventMaskKeyRelease, 2748 | }, 2749 | ).Check() 2750 | if err != nil { 2751 | slog.Error("Couldn't save window attributes", "error:", err.Error()) 2752 | return 2753 | } 2754 | // add it to the x11 save set 2755 | err = xproto.ChangeSaveSetChecked( 2756 | wm.conn, 2757 | xproto.SetModeInsert, // add to save set 2758 | w, // the client's window ID 2759 | ).Check() 2760 | if err != nil { 2761 | slog.Error("Couldn't save window to set", "error:", err.Error()) 2762 | return 2763 | } 2764 | 2765 | err = xproto.ChangeWindowAttributesChecked(wm.conn, w, xproto.CwEventMask, []uint32{ 2766 | xproto.EventMaskEnterWindow | xproto.EventMaskLeaveWindow, 2767 | }).Check() 2768 | if err != nil { 2769 | slog.Error("Failed to set event mask on window", "error:", err) 2770 | } 2771 | 2772 | setFrameWindowType(wm.conn, w) 2773 | 2774 | wins, err := xproto.QueryTree(wm.conn, wm.root).Reply() 2775 | if err == nil { 2776 | for _, win := range wins.Children { 2777 | wm.isAbove(win) 2778 | } 2779 | } 2780 | 2781 | // add all of this to the current workspace record 2782 | wm.currMonitor.CurrWorkspace.windowList = append(wm.currMonitor.CurrWorkspace.windowList, &Window{ 2783 | X: int(topLeftX), 2784 | Y: int(topLeftY), 2785 | Width: int(geometry.Width), 2786 | Height: int(geometry.Height), 2787 | Fullscreen: false, 2788 | id: w, 2789 | Client: w, 2790 | }) 2791 | wm.windows[w] = wm.currMonitor.CurrWorkspace.windowList[len(wm.currMonitor.CurrWorkspace.windowList)-1] 2792 | wm.setNetClientList() 2793 | fmt.Println("Framed window" + strconv.Itoa(int(w)) + "[" + strconv.Itoa(int(w)) + "]") 2794 | } 2795 | 2796 | func (wm *WindowManager) onConfigureRequest(event xproto.ConfigureRequestEvent) { 2797 | if _, ok := wm.windows[event.Window]; ok { 2798 | if wm.currMonitor.tiling { 2799 | return 2800 | } 2801 | } 2802 | changes := createChanges(event) 2803 | 2804 | fmt.Println(event.ValueMask) 2805 | fmt.Println(changes) 2806 | 2807 | err := xproto.ConfigureWindowChecked( 2808 | wm.conn, 2809 | event.Window, 2810 | event.ValueMask, 2811 | changes, 2812 | ).Check() 2813 | if err != nil { 2814 | slog.Error("Couldn't configure window", "error:", err.Error()) 2815 | } 2816 | } 2817 | 2818 | func createChanges(event xproto.ConfigureRequestEvent) []uint32 { 2819 | // selecting the right values that the window has asked to configure 2820 | 2821 | changes := make([]uint32, 0, 7) 2822 | 2823 | if event.ValueMask&xproto.ConfigWindowX != 0 { 2824 | changes = append(changes, uint32(event.X)) 2825 | } 2826 | if event.ValueMask&xproto.ConfigWindowY != 0 { 2827 | changes = append(changes, uint32(event.Y)) 2828 | } 2829 | if event.ValueMask&xproto.ConfigWindowWidth != 0 { 2830 | changes = append(changes, uint32(event.Width)) 2831 | } 2832 | if event.ValueMask&xproto.ConfigWindowHeight != 0 { 2833 | changes = append(changes, uint32(event.Height)) 2834 | } 2835 | if event.ValueMask&xproto.ConfigWindowBorderWidth != 0 { 2836 | changes = append(changes, uint32(event.BorderWidth)) 2837 | } 2838 | if event.ValueMask&xproto.ConfigWindowSibling != 0 { 2839 | changes = append(changes, uint32(event.Sibling)) 2840 | } 2841 | if event.ValueMask&xproto.ConfigWindowStackMode != 0 { 2842 | changes = append(changes, uint32(event.StackMode)) 2843 | } 2844 | 2845 | return changes 2846 | } 2847 | 2848 | // Close closes the window manager. 2849 | func (wm *WindowManager) Close() { 2850 | // close the connection 2851 | if wm.conn != nil { 2852 | wm.conn.Close() 2853 | } 2854 | } 2855 | 2856 | func (wm *WindowManager) setKeyBinds() { 2857 | // workspace keybinds, ik not very idiomatic but its fine :) 2858 | wm.config.Keybinds = append(wm.config.Keybinds, []Keybind{ 2859 | wm.createKeybind(&Keybind{Key: "0", Shift: false, Keycode: 0}), 2860 | wm.createKeybind(&Keybind{Key: "1", Shift: false, Keycode: 0}), 2861 | wm.createKeybind(&Keybind{Key: "2", Shift: false, Keycode: 0}), 2862 | wm.createKeybind(&Keybind{Key: "3", Shift: false, Keycode: 0}), 2863 | wm.createKeybind(&Keybind{Key: "4", Shift: false, Keycode: 0}), 2864 | wm.createKeybind(&Keybind{Key: "5", Shift: false, Keycode: 0}), 2865 | wm.createKeybind(&Keybind{Key: "6", Shift: false, Keycode: 0}), 2866 | wm.createKeybind(&Keybind{Key: "7", Shift: false, Keycode: 0}), 2867 | wm.createKeybind(&Keybind{Key: "8", Shift: false, Keycode: 0}), 2868 | wm.createKeybind(&Keybind{Key: "9", Shift: false, Keycode: 0}), 2869 | wm.createKeybind(&Keybind{Key: "0", Shift: true, Keycode: 0}), 2870 | wm.createKeybind(&Keybind{Key: "1", Shift: true, Keycode: 0}), 2871 | wm.createKeybind(&Keybind{Key: "2", Shift: true, Keycode: 0}), 2872 | wm.createKeybind(&Keybind{Key: "3", Shift: true, Keycode: 0}), 2873 | wm.createKeybind(&Keybind{Key: "4", Shift: true, Keycode: 0}), 2874 | wm.createKeybind(&Keybind{Key: "5", Shift: true, Keycode: 0}), 2875 | wm.createKeybind(&Keybind{Key: "6", Shift: true, Keycode: 0}), 2876 | wm.createKeybind(&Keybind{Key: "7", Shift: true, Keycode: 0}), 2877 | wm.createKeybind(&Keybind{Key: "8", Shift: true, Keycode: 0}), 2878 | wm.createKeybind(&Keybind{Key: "9", Shift: true, Keycode: 0}), 2879 | wm.createKeybind(&Keybind{Key: "r", Shift: true, Keycode: 0, Role: "reload-config"}), 2880 | }...) 2881 | } 2882 | 2883 | // The end. 2884 | --------------------------------------------------------------------------------