├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── client ├── client.go ├── client_test.go ├── config.go ├── titlebar.go ├── x11.go └── x11_test.go ├── cmd └── marwm │ └── main.go ├── config.go ├── go.mod ├── go.sum ├── keysym ├── keymap.go └── keysym.go ├── wm ├── actions.go ├── column.go ├── config.go ├── event_handler.go ├── focus.go ├── frame.go ├── manage.go ├── move.go ├── output.go ├── render.go ├── wm.go └── workspace.go └── x11 ├── atom.go ├── connection.go ├── desktop.go ├── ewmh.go ├── ewmh_supported.go ├── graphics.go ├── window.go └── xprop.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Built binaries 2 | /bin 3 | 4 | # Editor files 5 | .idea 6 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - golint 4 | - gocyclo 5 | - gofmt 6 | - goimports 7 | - gocognit 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Patryk Kalinowski 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 | PKG := github.com/patrislav/marwind 2 | SOURCES := $(shell find . -name '*.go') 3 | VERSION := $(shell git describe --always --long --dirty) 4 | BUILDTIME := $(shell date +'%Y-%m-%d_%T') 5 | LDFLAGS := 6 | 7 | .PHONY: all 8 | all: bin/marwm 9 | 10 | bin/marwm: $(SOURCES) 11 | go build -o bin/marwm \ 12 | -trimpath \ 13 | -ldflags="-X main.version=$(VERSION) -X main.buildTime=$(BUILDTIME) $(LDFLAGS)" \ 14 | $(PKG)/cmd/marwm 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Marwind WM 2 | 3 | Marwind is a simple X11 tiling window manager written in Go. It was inspired by the [i3 window manager](https://i3wm.org/) and the [acme editor](http://acme.cat-v.org/) for Plan 9 and aims to combine the good parts of both for the best experience. 4 | 5 | **Important! The project is under active development and is *not* stable. Use at your own risk.** 6 | 7 | ## Goals 8 | 9 | - Sane defaults. It should be possible to install the WM and be immediately productive without spending hours on configuration 10 | - Keyboard-driven without sacrificing the mouse. Marwind is focused on the keyboard not unlike most tiling managers, however mouse also has its place. Common actions - such as moving, resizing, or closing windows - should be possible using either of the input methods 11 | - Dynamically reconfigurable. Provide standard HTTP / gRPC endpoints for on-the-fly configuration, without the need to reload the entire WM. These endpoints will also serve as points of communication with external applications. 12 | - Clean code and documentation 13 | 14 | ## Limitations 15 | 16 | This is a list of features that are planned but still missing in the software: 17 | 18 | - There are no tests and no documentation yet 19 | - No window decorations (e.g. title bars) 20 | - No multi-monitor support 21 | - No mouse support 22 | - No floating windows 23 | - No configuration available 24 | 25 | ## Installation 26 | 27 | ### From source 28 | 29 | With Go environment set up and the repository cloned, it's enough to run: 30 | 31 | ```bash 32 | make 33 | ``` 34 | 35 | The compiled binaries will be located in the `bin` directory. The window manager can be started using: 36 | 37 | ```bash 38 | ./bin/marwm 39 | ``` 40 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/BurntSushi/xgb/xproto" 8 | ) 9 | 10 | type Geom struct { 11 | X, Y int16 12 | W, H uint16 13 | } 14 | 15 | type Type uint8 16 | 17 | const ( 18 | TypeUnknown Type = iota 19 | TypeNormal 20 | TypeDock 21 | ) 22 | 23 | type Client struct { 24 | x11 x11 25 | window xproto.Window 26 | parent xproto.Window 27 | mapped bool 28 | 29 | geom Geom 30 | cfg *Config 31 | typ Type 32 | 33 | title string 34 | } 35 | 36 | func New(x11 x11, cfg *Config, window xproto.Window, typ Type) (*Client, error) { 37 | c := &Client{x11: x11, cfg: cfg, window: window, typ: typ} 38 | 39 | if typ == TypeNormal { 40 | parent, err := c.createParent() 41 | if err != nil { 42 | return nil, fmt.Errorf("failed to create parent: %w", err) 43 | } 44 | if err := c.reparent(parent); err != nil { 45 | return nil, err 46 | } 47 | c.updateTitleProperty() 48 | } 49 | 50 | return c, nil 51 | } 52 | 53 | func (c *Client) Type() Type { return c.typ } 54 | func (c *Client) Window() xproto.Window { return c.window } 55 | func (c *Client) Parent() xproto.Window { return c.parent } 56 | func (c *Client) Geom() Geom { return c.geom } 57 | func (c *Client) Mapped() bool { return c.mapped } 58 | func (c *Client) SetGeom(geom Geom) { c.geom = geom } 59 | 60 | func (c *Client) Draw() error { 61 | return c.drawTitlebar() 62 | } 63 | 64 | // Update compares the desired state of the client against the actual state and executes updates 65 | // aimed at reaching the desired state 66 | func (c *Client) Update() error { 67 | return nil 68 | } 69 | 70 | // Map causes both the client window and the frame (parent) to be mapped 71 | func (c *Client) Map() error { 72 | if c.parent != 0 { 73 | if err := c.x11.MapWindow(c.parent); err != nil { 74 | return fmt.Errorf("could not map parent: %w", err) 75 | } 76 | } 77 | if err := c.x11.MapWindow(c.window); err != nil { 78 | return fmt.Errorf("could not map window: %w", err) 79 | } 80 | c.mapped = true 81 | return nil 82 | } 83 | 84 | // Unmap causes the client window to be unmapped. This in turn sends the UnmapNotify event 85 | // that is then handled by (*Client).OnUnmap 86 | func (c *Client) Unmap() error { 87 | if err := c.x11.UnmapWindow(c.window); err != nil { 88 | return fmt.Errorf("could not unmap window: %w", err) 89 | } 90 | return nil 91 | } 92 | 93 | // OnDestroy is called when the WM receives the DestroyNotify event 94 | func (c *Client) OnDestroy() error { 95 | if c.parent != 0 { 96 | if err := c.x11.DestroyWindow(c.parent); err != nil { 97 | return fmt.Errorf("could not destroy parent: %w", err) 98 | } 99 | } 100 | return nil 101 | } 102 | 103 | // OnUnmap is called when the WM receives the UnmapNotify event (e.g. when the client window 104 | // is closed by user action or when requested by the program itself) 105 | func (c *Client) OnUnmap() error { 106 | if !c.mapped { 107 | return nil 108 | } 109 | if c.parent != 0 { 110 | if err := c.x11.UnmapWindow(c.parent); err != nil { 111 | return fmt.Errorf("could not unmap parent: %w", err) 112 | } 113 | } 114 | c.mapped = false 115 | return nil 116 | } 117 | 118 | func (c *Client) OnProperty(atom xproto.Atom) { 119 | switch atom { 120 | case c.x11.Atom("_NET_WM_NAME"): 121 | c.updateTitleProperty() 122 | } 123 | } 124 | 125 | // createParent generates an X window and sets it up so that it can be used for reparenting 126 | func (c *Client) createParent() (xproto.Window, error) { 127 | return c.x11.CreateWindow(c.x11.GetRootWindow(), 128 | 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 129 | xproto.CwBackPixel|xproto.CwOverrideRedirect|xproto.CwEventMask, 130 | []uint32{ 131 | 0xffa1d1cf, 132 | 1, 133 | xproto.EventMaskSubstructureRedirect | 134 | xproto.EventMaskExposure | 135 | xproto.EventMaskButtonPress | 136 | xproto.EventMaskButtonRelease | 137 | xproto.EventMaskFocusChange, 138 | }, 139 | ) 140 | } 141 | 142 | func (c *Client) reparent(parent xproto.Window) error { 143 | if err := c.x11.ReparentWindow(c.window, parent, 0, 0); err != nil { 144 | return fmt.Errorf("could not reparent window: %w", err) 145 | } 146 | c.parent = parent 147 | return nil 148 | } 149 | 150 | func (c *Client) updateTitleProperty() { 151 | if v, err := c.x11.GetWindowTitle(c.window); err == nil { 152 | c.title = v 153 | if err := c.drawTitlebar(); err != nil { 154 | log.Printf("Failed to draw titlebar of client %v: %v\n", c.window, err) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/BurntSushi/xgb/xproto" 8 | ) 9 | 10 | func TestNew(t *testing.T) { 11 | window := xproto.Window(50) 12 | t.Run("TypeNormal", func(t *testing.T) { 13 | x11 := &mockX11{t: t} 14 | cfg := &Config{} 15 | _, err := New(x11, cfg, window, TypeNormal) 16 | if err != nil { 17 | t.Errorf("unexpected error: %v", err) 18 | } 19 | 20 | if len(x11.reparentedWins) != 1 { 21 | t.Fatalf("expected the window to be reparented") 22 | } 23 | 24 | got := x11.reparentedWins[0] 25 | want := mockReparented{w: window, p: 1, x: 0, y: 0} 26 | if !reflect.DeepEqual(got, want) { 27 | t.Errorf("got = %v, want = %v", got, want) 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | type Config struct { 4 | TitlebarHeight uint8 5 | BorderWidth uint8 6 | BgColor uint32 7 | FontColor uint32 8 | FontSize float64 9 | } 10 | -------------------------------------------------------------------------------- /client/titlebar.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "image/draw" 7 | 8 | "github.com/BurntSushi/freetype-go/freetype" 9 | "github.com/BurntSushi/xgbutil/xgraphics" 10 | "golang.org/x/image/font/gofont/goregular" 11 | ) 12 | 13 | func (c *Client) drawTitlebar() error { 14 | width := c.geom.W 15 | bg := color.RGBA{ 16 | A: uint8((c.cfg.BgColor & 0xFF000000) >> 24), 17 | R: uint8((c.cfg.BgColor & 0x00FF0000) >> 16), 18 | G: uint8((c.cfg.BgColor & 0x0000FF00) >> 8), 19 | B: uint8(c.cfg.BgColor & 0x000000FF), 20 | } 21 | fg := color.RGBA{ 22 | A: uint8((c.cfg.FontColor & 0xFF000000) >> 24), 23 | R: uint8((c.cfg.FontColor & 0x00FF0000) >> 16), 24 | G: uint8((c.cfg.FontColor & 0x0000FF00) >> 8), 25 | B: uint8(c.cfg.FontColor & 0x000000FF), 26 | } 27 | 28 | // title should never be zero-length 29 | if len(c.title) == 0 { 30 | c.title = " " 31 | } 32 | 33 | img := c.x11.NewImage(image.Rect(0, 0, int(width), int(c.cfg.TitlebarHeight))) 34 | defer img.Destroy() 35 | img.ForExp(func(x, y int) (uint8, uint8, uint8, uint8) { 36 | return bg.R, bg.G, bg.B, bg.A 37 | }) 38 | 39 | font, err := freetype.ParseFont(goregular.TTF) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | // Over estimate the extents 45 | ew, eh := xgraphics.Extents(font, c.cfg.FontSize, c.title) 46 | 47 | // Create an image using the overestimated extents 48 | text := c.x11.NewImage(image.Rect(0, 0, ew, eh)) 49 | defer text.Destroy() 50 | text.ForExp(func(x, y int) (uint8, uint8, uint8, uint8) { 51 | return bg.R, bg.G, bg.B, bg.A 52 | }) 53 | _, _, err = text.Text(0, 0, fg, c.cfg.FontSize, font, c.title) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | bounds := text.Bounds().Size() 59 | w, h := bounds.X, bounds.Y 60 | x := int(width/2) - w/2 61 | y := int(c.cfg.TitlebarHeight/2) - h/2 62 | dstRect := image.Rect(x, y, x+w, y+h) 63 | draw.Draw(img, dstRect, text, image.Point{}, draw.Src) 64 | 65 | if err := img.CreatePixmap(); err != nil { 66 | return err 67 | } 68 | img.XDraw() 69 | img.XExpPaint(c.parent, int(c.cfg.BorderWidth), int(c.cfg.BorderWidth)) 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /client/x11.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/BurntSushi/xgb/xproto" 7 | "github.com/BurntSushi/xgbutil/xgraphics" 8 | ) 9 | 10 | type x11 interface { 11 | GetRootWindow() xproto.Window 12 | CreateWindow( 13 | parent xproto.Window, 14 | x int16, y int16, width uint16, height uint16, 15 | borderWidth uint16, 16 | class uint16, valueMask uint32, valueList []uint32, 17 | ) (xproto.Window, error) 18 | 19 | MapWindow(window xproto.Window) error 20 | UnmapWindow(window xproto.Window) error 21 | DestroyWindow(window xproto.Window) error 22 | ReparentWindow(window, parent xproto.Window, x, y int16) error 23 | 24 | GetWindowTitle(window xproto.Window) (string, error) 25 | Atom(name string) xproto.Atom 26 | 27 | NewImage(rect image.Rectangle) *xgraphics.Image 28 | } 29 | -------------------------------------------------------------------------------- /client/x11_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "github.com/BurntSushi/xgb/xproto" 8 | "github.com/BurntSushi/xgbutil/xgraphics" 9 | ) 10 | 11 | type mockReparented struct { 12 | w, p xproto.Window 13 | x, y int16 14 | } 15 | 16 | type mockX11 struct { 17 | t *testing.T 18 | 19 | // mappedWins []xproto.Window 20 | // unmappedWins []xproto.Window 21 | // destroyedWins []xproto.Window 22 | reparentedWins []mockReparented 23 | } 24 | 25 | func (mx *mockX11) GetRootWindow() xproto.Window { 26 | return 0 27 | } 28 | func (mx *mockX11) CreateWindow( 29 | parent xproto.Window, 30 | x int16, y int16, width uint16, height uint16, 31 | borderWidth uint16, 32 | class uint16, valueMask uint32, valueList []uint32, 33 | ) (xproto.Window, error) { 34 | return 1, nil 35 | } 36 | 37 | func (mx *mockX11) MapWindow(window xproto.Window) error { 38 | return nil 39 | } 40 | func (mx *mockX11) UnmapWindow(window xproto.Window) error { 41 | return nil 42 | } 43 | func (mx *mockX11) DestroyWindow(window xproto.Window) error { 44 | return nil 45 | } 46 | func (mx *mockX11) ReparentWindow(window, parent xproto.Window, x, y int16) error { 47 | mx.reparentedWins = append(mx.reparentedWins, mockReparented{ 48 | w: window, 49 | p: parent, 50 | x: x, 51 | y: y, 52 | }) 53 | return nil 54 | } 55 | 56 | func (mx *mockX11) GetWindowTitle(window xproto.Window) (string, error) { 57 | return "", nil 58 | } 59 | func (mx *mockX11) Atom(name string) xproto.Atom { 60 | return 0 61 | } 62 | 63 | func (mx *mockX11) NewImage(rect image.Rectangle) *xgraphics.Image { 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /cmd/marwm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | 10 | flag "github.com/spf13/pflag" 11 | 12 | "github.com/patrislav/marwind" 13 | "github.com/patrislav/marwind/wm" 14 | ) 15 | 16 | var ( 17 | version string // program version 18 | buildTime string // when the executable was built 19 | ) 20 | 21 | var ( 22 | flagVersion bool 23 | initCmd string 24 | ) 25 | 26 | func main() { 27 | flag.BoolVar(&flagVersion, "version", false, "show version and exit") 28 | flag.StringVar(&initCmd, "init", "", "run this executable at startup") 29 | flag.Parse() 30 | 31 | if flagVersion { 32 | fmt.Printf("marwm version:\t%s (%s)\n", version, buildTime) 33 | fmt.Printf("go version:\t%s\n", runtime.Version()) 34 | os.Exit(0) 35 | } 36 | 37 | mgr, err := wm.New(marwind.Config) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | defer mgr.Close() 42 | if err := mgr.Init(); err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | if initCmd != "" { 47 | cmd := exec.Command(initCmd) 48 | err = cmd.Start() 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | go func() { 53 | _ = cmd.Wait() 54 | }() 55 | } 56 | 57 | if err := mgr.Run(); err != nil { 58 | log.Fatal(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package marwind 2 | 3 | import ( 4 | "github.com/BurntSushi/xgb/xproto" 5 | "github.com/patrislav/marwind/keysym" 6 | "github.com/patrislav/marwind/wm" 7 | ) 8 | 9 | var Config = wm.Config{ 10 | InnerGap: 4, 11 | OuterGap: 4, 12 | Shell: "/bin/sh", 13 | LauncherCommand: "rofi -show drun", 14 | TerminalCommand: "alacritty", 15 | BorderWidth: 0, 16 | BorderColor: 0xffa1d1cf, 17 | TitleBarHeight: 18, 18 | TitleBarBgColor: 0xffa1d1cf, 19 | TitleBarFontColorActive: 0xff000000, 20 | TitleBarFontSize: 12, 21 | Keybindings: map[xproto.Keysym]string{ 22 | // Brightness control 23 | keysym.XF86MonBrightnessDown: "light -U 5", 24 | keysym.XF86MonBrightnessUp: "light -A 5", 25 | // Volume control 26 | keysym.XF86AudioMute: "pactl set-sink-mute @DEFAULT_SINK@ toggle", 27 | keysym.XF86AudioLowerVolume: "pactl set-sink-volume @DEFAULT_SINK@ -5%", 28 | keysym.XF86AudioRaiseVolume: "pactl set-sink-volume @DEFAULT_SINK@ +5%", 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/patrislav/marwind 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 7 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect 8 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 9 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 10 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 11 | github.com/spf13/pflag v1.0.3 12 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA= 2 | github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= 3 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= 4 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= 5 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= 6 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 7 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= 8 | github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= 9 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 10 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 11 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 12 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 13 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0= 14 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 15 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 16 | -------------------------------------------------------------------------------- /keysym/keymap.go: -------------------------------------------------------------------------------- 1 | package keysym 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/BurntSushi/xgb" 7 | "github.com/BurntSushi/xgb/xproto" 8 | ) 9 | 10 | type Keymap [256][]xproto.Keysym 11 | 12 | const ( 13 | loKey = 8 14 | hiKey = 255 15 | ) 16 | 17 | func LoadKeyMapping(xc *xgb.Conn) (*Keymap, error) { 18 | m := xproto.GetKeyboardMapping(xc, loKey, hiKey-loKey+1) 19 | reply, err := m.Reply() 20 | if err != nil { 21 | return nil, err 22 | } 23 | if reply == nil { 24 | return nil, errors.New("could not load keysym map") 25 | } 26 | 27 | var keymap Keymap 28 | for i := 0; i < hiKey-loKey+1; i++ { 29 | keymap[loKey+i] = reply.Keysyms[i*int(reply.KeysymsPerKeycode) : (i+1)*int(reply.KeysymsPerKeycode)] 30 | } 31 | return &keymap, nil 32 | } 33 | -------------------------------------------------------------------------------- /keysym/keysym.go: -------------------------------------------------------------------------------- 1 | package keysym 2 | 3 | // Known KeySyms from /usr/include/X11/keysymdef.h 4 | // Copied from https://github.com/driusan/dewm/blob/master/keysym/keysym.go 5 | const ( 6 | // TTY function keys, cleverly chosen to map to ASCII, for convenience of 7 | // programming, but could have been arbitrary (at the cost of lookup 8 | // tables in client code). 9 | XKBackSpace = 0xff08 // Back space, back char 10 | XKTab = 0xff09 11 | XKLinefeed = 0xff0a // Linefeed, LF 12 | XKClear = 0xff0b 13 | XKReturn = 0xff0d // Return, enter 14 | XKPause = 0xff13 // Pause, hold 15 | XKScrollLock = 0xff14 16 | XKSysReq = 0xff15 17 | XKEscape = 0xff1b 18 | XKDelete = 0xffff // Delete, rubout 19 | // Latin 1 20 | // (ISO/IEC 8859-1 = Unicode U+0020..U+00FF) 21 | // Byte 3 = 0 22 | XKSpace = 0x0020 // U+0020 SPACE 23 | XKExclam = 0x0021 // U+0021 EXCLAMATION MARK 24 | XKQuotedbl = 0x0022 // U+0022 QUOTATION MARK 25 | XKNumberSign = 0x0023 // U+0023 NUMBER SIGN 26 | XKDollar = 0x0024 // U+0024 DOLLAR SIGN 27 | XKPercent = 0x0025 // U+0025 PERCENT SIGN 28 | XKAmpersand = 0x0026 // U+0026 AMPERSAND 29 | XKApostrophe = 0x0027 // U+0027 APOSTROPHE 30 | XKQuoteRight = 0x0027 // deprecated 31 | XKParenLeft = 0x0028 // U+0028 LEFT PARENTHESIS 32 | XKParenRight = 0x0029 // U+0029 RIGHT PARENTHESIS 33 | XKAsterisk = 0x002a // U+002A ASTERISK 34 | XKPlus = 0x002b // U+002B PLUS SIGN 35 | XKComma = 0x002c // U+002C COMMA 36 | XKMinus = 0x002d // U+002D HYPHEN-MINUS 37 | XKPeriod = 0x002e // U+002E FULL STOP 38 | XKSlash = 0x002f // U+002F SOLIDUS 39 | XK0 = 0x0030 // U+0030 DIGIT ZERO 40 | XK1 = 0x0031 // U+0031 DIGIT ONE 41 | XK2 = 0x0032 // U+0032 DIGIT TWO 42 | XK3 = 0x0033 // U+0033 DIGIT THREE 43 | XK4 = 0x0034 // U+0034 DIGIT FOUR 44 | XK5 = 0x0035 // U+0035 DIGIT FIVE 45 | XK6 = 0x0036 // U+0036 DIGIT SIX 46 | XK7 = 0x0037 // U+0037 DIGIT SEVEN 47 | XK8 = 0x0038 // U+0038 DIGIT EIGHT 48 | XK9 = 0x0039 // U+0039 DIGIT NINE 49 | XKColon = 0x003a // U+003A COLON 50 | XKSemicolon = 0x003b // U+003B SEMICOLON 51 | XKLess = 0x003c // U+003C LESS-THAN SIGN 52 | XKEqual = 0x003d // U+003D EQUALS SIGN 53 | XKGreater = 0x003e // U+003E GREATER-THAN SIGN 54 | XKQuestion = 0x003f // U+003F QUESTION MARK 55 | XKAt = 0x0040 // U+0040 COMMERCIAL AT 56 | XKA = 0x0041 // U+0041 LATIN CAPITAL LETTER A 57 | XKB = 0x0042 // U+0042 LATIN CAPITAL LETTER B 58 | XKC = 0x0043 // U+0043 LATIN CAPITAL LETTER C 59 | XKD = 0x0044 // U+0044 LATIN CAPITAL LETTER D 60 | XKE = 0x0045 // U+0045 LATIN CAPITAL LETTER E 61 | XKF = 0x0046 // U+0046 LATIN CAPITAL LETTER F 62 | XKG = 0x0047 // U+0047 LATIN CAPITAL LETTER G 63 | XKH = 0x0048 // U+0048 LATIN CAPITAL LETTER H 64 | XKI = 0x0049 // U+0049 LATIN CAPITAL LETTER I 65 | XKJ = 0x004a // U+004A LATIN CAPITAL LETTER J 66 | XKK = 0x004b // U+004B LATIN CAPITAL LETTER K 67 | XKL = 0x004c // U+004C LATIN CAPITAL LETTER L 68 | XKM = 0x004d // U+004D LATIN CAPITAL LETTER M 69 | XKN = 0x004e // U+004E LATIN CAPITAL LETTER N 70 | XKO = 0x004f // U+004F LATIN CAPITAL LETTER O 71 | XKP = 0x0050 // U+0050 LATIN CAPITAL LETTER P 72 | XKQ = 0x0051 // U+0051 LATIN CAPITAL LETTER Q 73 | XKR = 0x0052 // U+0052 LATIN CAPITAL LETTER R 74 | XKS = 0x0053 // U+0053 LATIN CAPITAL LETTER S 75 | XKT = 0x0054 // U+0054 LATIN CAPITAL LETTER T 76 | XKU = 0x0055 // U+0055 LATIN CAPITAL LETTER U 77 | XKV = 0x0056 // U+0056 LATIN CAPITAL LETTER V 78 | XKW = 0x0057 // U+0057 LATIN CAPITAL LETTER W 79 | XKX = 0x0058 // U+0058 LATIN CAPITAL LETTER X 80 | XKY = 0x0059 // U+0059 LATIN CAPITAL LETTER Y 81 | XKZ = 0x005a // U+005A LATIN CAPITAL LETTER Z 82 | XKBracketLeft = 0x005b // U+005B LEFT SQUARE BRACKET 83 | XKBackslash = 0x005c // U+005C REVERSE SOLIDUS 84 | XKBracketRight = 0x005d // U+005D RIGHT SQUARE BRACKET 85 | XKAsciiCircum = 0x005e // U+005E CIRCUMFLEX ACCENT 86 | XKUnderscore = 0x005f // U+005F LOW LINE 87 | XKGrave = 0x0060 // U+0060 GRAVE ACCENT 88 | XKQuoteLeft = 0x0060 // deprecated 89 | XKa = 0x0061 // U+0061 LATIN SMALL LETTER A 90 | XKb = 0x0062 // U+0062 LATIN SMALL LETTER B 91 | XKc = 0x0063 // U+0063 LATIN SMALL LETTER C 92 | XKd = 0x0064 // U+0064 LATIN SMALL LETTER D 93 | XKe = 0x0065 // U+0065 LATIN SMALL LETTER E 94 | XKf = 0x0066 // U+0066 LATIN SMALL LETTER F 95 | XKg = 0x0067 // U+0067 LATIN SMALL LETTER G 96 | XKh = 0x0068 // U+0068 LATIN SMALL LETTER H 97 | XKi = 0x0069 // U+0069 LATIN SMALL LETTER I 98 | XKj = 0x006a // U+006A LATIN SMALL LETTER J 99 | XKk = 0x006b // U+006B LATIN SMALL LETTER K 100 | XKl = 0x006c // U+006C LATIN SMALL LETTER L 101 | XKm = 0x006d // U+006D LATIN SMALL LETTER M 102 | XKn = 0x006e // U+006E LATIN SMALL LETTER N 103 | XKo = 0x006f // U+006F LATIN SMALL LETTER O 104 | XKp = 0x0070 // U+0070 LATIN SMALL LETTER P 105 | XKq = 0x0071 // U+0071 LATIN SMALL LETTER Q 106 | XKr = 0x0072 // U+0072 LATIN SMALL LETTER R 107 | XKs = 0x0073 // U+0073 LATIN SMALL LETTER S 108 | XKt = 0x0074 // U+0074 LATIN SMALL LETTER T 109 | XKu = 0x0075 // U+0075 LATIN SMALL LETTER U 110 | XKv = 0x0076 // U+0076 LATIN SMALL LETTER V 111 | XKw = 0x0077 // U+0077 LATIN SMALL LETTER W 112 | XKx = 0x0078 // U+0078 LATIN SMALL LETTER X 113 | XKy = 0x0079 // U+0079 LATIN SMALL LETTER Y 114 | XKz = 0x007a // U+007A LATIN SMALL LETTER Z 115 | XKBraceLeft = 0x007b // U+007B LEFT CURLY BRACKET 116 | XKBar = 0x007c // U+007C VERTICAL LINE 117 | XKBraceRight = 0x007d // U+007D RIGHT CURLY BRACKET 118 | XKAsciiTilde = 0x007e // U+007E TILDE 119 | 120 | // Cursor control & motion 121 | XKHome = 0xff50 122 | XKLeft = 0xff51 // Move left, left arrow 123 | XKUp = 0xff52 // Move up, up arrow 124 | XKRight = 0xff53 // Move right, right arrow 125 | XKDown = 0xff54 // Move down, down arrow 126 | XKPrior = 0xff55 // Prior, previous 127 | XKPageUp = 0xff55 128 | XKNext = 0xff56 // Next 129 | XKPageDown = 0xff56 130 | XKEnd = 0xff57 // EOL 131 | XKBegin = 0xff58 // BOL 132 | 133 | XF86MonBrightnessUp = 0x1008ff02 134 | XF86MonBrightnessDown = 0x1008ff03 135 | XF86AudioLowerVolume = 0x1008ff11 136 | XF86AudioMute = 0x1008ff12 137 | XF86AudioRaiseVolume = 0x1008ff13 138 | ) 139 | -------------------------------------------------------------------------------- /wm/actions.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/BurntSushi/xgb/xproto" 9 | "github.com/patrislav/marwind/keysym" 10 | ) 11 | 12 | type action struct { 13 | sym xproto.Keysym 14 | modifiers int 15 | codes []xproto.Keycode 16 | act func() error 17 | } 18 | 19 | func initActions(wm *WM) []*action { 20 | mod := xproto.ModMask4 21 | shift := xproto.ModMaskShift 22 | actions := []*action{ 23 | { 24 | sym: keysym.XKq, 25 | modifiers: mod | shift, 26 | act: func() error { 27 | return handleRemoveWindow(wm) 28 | }, 29 | }, 30 | { 31 | sym: keysym.XKt, 32 | modifiers: mod | shift | xproto.ModMask1, 33 | act: func() error { 34 | os.Exit(1) 35 | return nil 36 | }, 37 | }, 38 | { 39 | sym: keysym.XKd, 40 | modifiers: mod, 41 | act: func() error { 42 | cmd := exec.Command(wm.config.Shell, "-c", wm.config.LauncherCommand) 43 | go func() { 44 | if err := cmd.Run(); err != nil { 45 | log.Println("Failed to open launcher:", err) 46 | } 47 | }() 48 | return nil 49 | }, 50 | }, 51 | { 52 | sym: keysym.XKReturn, 53 | modifiers: mod | shift, 54 | act: func() error { 55 | cmd := exec.Command(wm.config.Shell, "-c", wm.config.TerminalCommand) 56 | go func() { 57 | if err := cmd.Run(); err != nil { 58 | log.Println("Failed to open terminal:", err) 59 | } 60 | }() 61 | return nil 62 | }, 63 | }, 64 | { 65 | sym: keysym.XKh, 66 | modifiers: mod | shift, 67 | act: func() error { return handleMoveWindow(wm, MoveLeft) }, 68 | }, 69 | { 70 | sym: keysym.XKj, 71 | modifiers: mod | shift, 72 | act: func() error { return handleMoveWindow(wm, MoveDown) }, 73 | }, 74 | { 75 | sym: keysym.XKk, 76 | modifiers: mod | shift, 77 | act: func() error { return handleMoveWindow(wm, MoveUp) }, 78 | }, 79 | { 80 | sym: keysym.XKl, 81 | modifiers: mod | shift, 82 | act: func() error { return handleMoveWindow(wm, MoveRight) }, 83 | }, 84 | { 85 | sym: keysym.XKy, 86 | modifiers: mod | shift, 87 | act: func() error { return handleResizeWindow(wm, ResizeHoriz, -5) }, 88 | }, 89 | { 90 | sym: keysym.XKu, 91 | modifiers: mod | shift, 92 | act: func() error { return handleResizeWindow(wm, ResizeVert, 5) }, 93 | }, 94 | { 95 | sym: keysym.XKi, 96 | modifiers: mod | shift, 97 | act: func() error { return handleResizeWindow(wm, ResizeVert, -5) }, 98 | }, 99 | { 100 | sym: keysym.XKo, 101 | modifiers: mod | shift, 102 | act: func() error { return handleResizeWindow(wm, ResizeHoriz, 5) }, 103 | }, 104 | } 105 | actions = appendWorkspaceActions(wm, actions, mod, mod|shift) 106 | 107 | for sym, command := range wm.config.Keybindings { 108 | cmd := command 109 | actions = append(actions, &action{ 110 | sym: sym, 111 | act: func() error { 112 | cmd := exec.Command(wm.config.Shell, "-c", cmd) 113 | go func() { 114 | if err := cmd.Run(); err != nil { 115 | log.Printf("Failed to run command (%s): %v\n", cmd, err) 116 | } 117 | }() 118 | return nil 119 | }, 120 | }) 121 | } 122 | 123 | for i, syms := range wm.keymap { 124 | for _, sym := range syms { 125 | for c := range actions { 126 | if actions[c].sym == sym { 127 | actions[c].codes = append(actions[c].codes, xproto.Keycode(i)) 128 | } 129 | } 130 | } 131 | } 132 | return actions 133 | } 134 | 135 | func appendWorkspaceActions(wm *WM, actions []*action, switchMod int, moveMod int) []*action { 136 | for i := 0; i < maxWorkspaces; i++ { 137 | var sym xproto.Keysym 138 | if i == 9 { 139 | sym = keysym.XK0 140 | } else { 141 | sym = xproto.Keysym(keysym.XK1 + i) 142 | } 143 | wsID := i 144 | actions = append(actions, &action{ 145 | sym: sym, 146 | modifiers: switchMod, 147 | act: func() error { 148 | return handleSwitchWorkspace(wm, uint8(wsID)) 149 | }, 150 | }, &action{ 151 | sym: sym, 152 | modifiers: moveMod, 153 | act: func() error { 154 | return handleMoveWindowToWorkspace(wm, uint8(wsID)) 155 | }, 156 | }) 157 | } 158 | return actions 159 | } 160 | 161 | func handleRemoveWindow(wm *WM) error { 162 | frm := wm.findFrame(func(f *frame) bool { return f.cli.Window() == wm.activeWin }) 163 | if frm == nil { 164 | log.Printf("WARNING: handleRemoveWindow: could not find frame with window %d\n", wm.activeWin) 165 | return nil 166 | } 167 | return wm.xc.GracefullyDestroyWindow(frm.cli.Window()) 168 | } 169 | 170 | func handleMoveWindow(wm *WM, dir MoveDirection) error { 171 | frm := wm.findFrame(func(f *frame) bool { return f.cli.Window() == wm.activeWin }) 172 | if frm == nil { 173 | log.Printf("WARNING: handleMoveWindow: could not find frame with window %d\n", wm.activeWin) 174 | return nil 175 | } 176 | if err := frm.workspace().moveFrame(frm, dir); err != nil { 177 | return err 178 | } 179 | if err := wm.renderWorkspace(frm.workspace()); err != nil { 180 | return err 181 | } 182 | return wm.warpPointerToFrame(frm) 183 | } 184 | 185 | func handleResizeWindow(wm *WM, dir ResizeDirection, pct int) error { 186 | frm := wm.findFrame(func(f *frame) bool { return f.cli.Window() == wm.activeWin }) 187 | if frm == nil { 188 | log.Printf("WARNING: handleResizeWindow: could not find frame with window %d\n", wm.activeWin) 189 | return nil 190 | } 191 | if err := frm.workspace().resizeFrame(frm, dir, pct); err != nil { 192 | return err 193 | } 194 | if err := wm.renderWorkspace(frm.workspace()); err != nil { 195 | return err 196 | } 197 | return wm.warpPointerToFrame(frm) 198 | } 199 | 200 | func handleSwitchWorkspace(wm *WM, wsID uint8) error { 201 | return wm.switchWorkspace(wsID) 202 | } 203 | 204 | func handleMoveWindowToWorkspace(wm *WM, wsID uint8) error { 205 | frm := wm.findFrame(func(f *frame) bool { return f.cli.Window() == wm.activeWin }) 206 | if frm == nil { 207 | log.Printf("WARNING: handleMoveWindowToWorkspace: could not find frame with window %d\n", wm.activeWin) 208 | return nil 209 | } 210 | if err := wm.moveFrameToWorkspace(frm, wsID); err != nil { 211 | return err 212 | } 213 | return nil 214 | } 215 | -------------------------------------------------------------------------------- /wm/column.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | type column struct { 4 | ws *workspace 5 | frames []*frame 6 | width uint16 7 | } 8 | 9 | func (c *column) addFrame(frm *frame, after *frame) { 10 | frm.col = c 11 | wsHeight := c.ws.area().H 12 | if len(c.frames) > 0 { 13 | frm.height = wsHeight / uint16(len(c.frames)+1) 14 | remHeight := float32(wsHeight - frm.height) 15 | leftHeight := uint16(remHeight) 16 | for _, f := range c.frames { 17 | f.height = uint16((float32(f.height) / float32(wsHeight)) * remHeight) 18 | leftHeight -= f.height 19 | } 20 | if leftHeight != 0 { 21 | frm.height += leftHeight 22 | } 23 | } else { 24 | frm.height = wsHeight 25 | } 26 | c.frames = append(c.frames, frm) 27 | } 28 | 29 | func (c *column) deleteFrame(frm *frame) { 30 | idx := c.findFrameIndex(func(f *frame) bool { return f == frm }) 31 | if idx < 0 { 32 | return 33 | } 34 | c.frames = append(c.frames[:idx], c.frames[idx+1:]...) 35 | c.updateTiling() 36 | } 37 | 38 | func (c *column) updateTiling() { 39 | wsHeight := c.ws.area().H 40 | // TODO: assign the heights proportional to the original height/totalHeight ratio 41 | for _, f := range c.frames { 42 | f.height = wsHeight / uint16(len(c.frames)) 43 | } 44 | } 45 | 46 | func (c *column) findFrameIndex(predicate func(*frame) bool) int { 47 | for i, f := range c.frames { 48 | if predicate(f) { 49 | return i 50 | } 51 | } 52 | return -1 53 | } 54 | -------------------------------------------------------------------------------- /wm/config.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "github.com/BurntSushi/xgb/xproto" 5 | ) 6 | 7 | type Config struct { 8 | InnerGap uint16 // Gap around each window, in pixels 9 | OuterGap uint16 // Additional gap around the entire workspace, in pixels 10 | 11 | Shell string // Name of the program to use for executing commands ("/bin/sh" by default) 12 | 13 | // Shell command to execute after using the "Launcher" binding (Win + D by default) 14 | LauncherCommand string 15 | // Shell command to execute after using the "Terminal" binding (Win + Shift + Enter by default) 16 | TerminalCommand string 17 | 18 | BorderWidth uint8 19 | BorderColor uint32 20 | 21 | TitleBarHeight uint8 22 | TitleBarBgColor uint32 23 | TitleBarFontColorActive uint32 24 | TitleBarFontColorInactive uint32 25 | TitleBarFontSize float64 26 | 27 | Keybindings map[xproto.Keysym]string 28 | } 29 | -------------------------------------------------------------------------------- /wm/event_handler.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/BurntSushi/xgb/xproto" 7 | ) 8 | 9 | type eventHandler struct { 10 | wm *WM 11 | } 12 | 13 | func (h eventHandler) eventLoop() { 14 | for { 15 | xev, err := h.wm.xc.X().WaitForEvent() 16 | if err != nil { 17 | log.Println(err) 18 | continue 19 | } 20 | switch e := xev.(type) { 21 | case xproto.KeyPressEvent: 22 | h.keyPress(e) 23 | case xproto.EnterNotifyEvent: 24 | h.enterNotify(e) 25 | case xproto.ConfigureRequestEvent: 26 | h.configureRequest(e) 27 | case xproto.MapNotifyEvent: 28 | h.mapNotify(e) 29 | case xproto.MapRequestEvent: 30 | h.mapRequest(e) 31 | case xproto.UnmapNotifyEvent: 32 | h.unmapNotify(e) 33 | case xproto.DestroyNotifyEvent: 34 | h.destroyNotify(e) 35 | case xproto.PropertyNotifyEvent: 36 | h.propertyNotify(e) 37 | case xproto.ClientMessageEvent: 38 | h.clientMessage(e) 39 | case xproto.ExposeEvent: 40 | h.expose(e) 41 | } 42 | } 43 | } 44 | 45 | func (h eventHandler) keyPress(e xproto.KeyPressEvent) { 46 | if err := h.wm.handleKeyPressEvent(e); err != nil { 47 | log.Println(err) 48 | } 49 | } 50 | 51 | func (h eventHandler) enterNotify(e xproto.EnterNotifyEvent) { 52 | f := h.wm.findFrame(func(frm *frame) bool { return frm.cli.Window() == e.Event }) 53 | if f != nil { 54 | if err := h.wm.setFocus(e.Event, e.Time); err != nil { 55 | log.Println("Failed to set focus:", err) 56 | } 57 | } 58 | } 59 | 60 | func (h eventHandler) configureRequest(e xproto.ConfigureRequestEvent) { 61 | if err := h.wm.handleConfigureRequest(e); err != nil { 62 | log.Println("Failed to configure window:", err) 63 | } 64 | } 65 | 66 | func (h eventHandler) mapNotify(e xproto.MapNotifyEvent) { 67 | f := h.wm.findFrame(func(frm *frame) bool { return frm.cli.Window() == e.Window }) 68 | if f != nil { 69 | if err := h.wm.configureNotify(f); err != nil { 70 | log.Printf("Failed to send ConfigureNotify event to %d: %v\n", e.Window, err) 71 | } 72 | } 73 | } 74 | 75 | func (h eventHandler) mapRequest(e xproto.MapRequestEvent) { 76 | f := h.wm.findFrame(func(frm *frame) bool { return frm.cli.Window() == e.Window }) 77 | if f != nil { 78 | log.Printf("Skipping MapRequest of an already mapped window %d\n", e.Window) 79 | return 80 | } 81 | if attr, err := xproto.GetWindowAttributes(h.wm.xc.X(), e.Window).Reply(); err != nil || !attr.OverrideRedirect { 82 | if err := h.wm.manageWindow(e.Window); err != nil { 83 | log.Println("Failed to manage a window:", err) 84 | } 85 | } 86 | if err := h.wm.updateDesktopHints(); err != nil { 87 | log.Printf("Failed to update desktop hints: %v", err) 88 | } 89 | } 90 | 91 | func (h eventHandler) unmapNotify(e xproto.UnmapNotifyEvent) { 92 | f := h.wm.findFrame(func(frm *frame) bool { return frm.cli.Window() == e.Window }) 93 | if f != nil { 94 | if err := f.cli.OnUnmap(); err != nil { 95 | log.Println("Failed to unmap frame's parent:", err) 96 | return 97 | } 98 | } 99 | } 100 | 101 | func (h eventHandler) destroyNotify(e xproto.DestroyNotifyEvent) { 102 | f := h.wm.findFrame(func(frm *frame) bool { return frm.cli.Window() == e.Window }) 103 | if f != nil { 104 | if err := f.cli.OnDestroy(); err != nil { 105 | log.Println("Failed to destroy frame's parent:", err) 106 | return 107 | } 108 | if err := h.wm.deleteFrame(f); err != nil { 109 | log.Println("Failed to delete the frame:", err) 110 | } 111 | if err := h.wm.updateDesktopHints(); err != nil { 112 | log.Printf("Failed to update desktop hints: %v", err) 113 | } 114 | } 115 | } 116 | 117 | func (h eventHandler) propertyNotify(e xproto.PropertyNotifyEvent) { 118 | f := h.wm.findFrame(func(frm *frame) bool { return frm.cli.Window() == e.Window }) 119 | if f != nil { 120 | f.cli.OnProperty(e.Atom) 121 | } 122 | } 123 | 124 | func (h eventHandler) clientMessage(e xproto.ClientMessageEvent) { 125 | switch e.Type { 126 | case h.wm.xc.Atom("_NET_CURRENT_DESKTOP"): 127 | out := h.wm.outputs[0] 128 | index := int(e.Data.Data32[0]) 129 | if index < len(out.workspaces) { 130 | ws := out.workspaces[index] 131 | if err := h.wm.switchWorkspace(ws.id); err != nil { 132 | log.Printf("Failed to switch workspace: %v", err) 133 | } 134 | } 135 | } 136 | } 137 | 138 | func (h eventHandler) expose(e xproto.ExposeEvent) { 139 | f := h.wm.findFrame(func(frm *frame) bool { 140 | return frm.cli.Parent() == e.Window || frm.cli.Window() == e.Window 141 | }) 142 | if f != nil { 143 | if err := f.cli.Draw(); err != nil { 144 | log.Println("Failed to draw client:", err) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /wm/focus.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "github.com/BurntSushi/xgb/xproto" 5 | "github.com/patrislav/marwind/client" 6 | ) 7 | 8 | func (wm *WM) setFocus(win xproto.Window, time xproto.Timestamp) error { 9 | frm := wm.findFrame(func(f *frame) bool { return f.cli.Window() == win && f.cli.Type() == client.TypeNormal }) 10 | if frm == nil && win != wm.xc.GetRootWindow() { 11 | return nil 12 | } 13 | wm.activeWin = win 14 | cookie := xproto.GetProperty(wm.xc.X(), false, win, wm.xc.Atom("WM_PROTOCOLS"), xproto.GetPropertyTypeAny, 0, 64) 15 | prop, err := cookie.Reply() 16 | if err == nil && wm.takeFocusProp(prop, win, time) { 17 | return wm.xc.SetActiveWindow(win) 18 | } 19 | err = xproto.SetInputFocusChecked(wm.xc.X(), xproto.InputFocusPointerRoot, win, time).Check() 20 | if err != nil { 21 | return err 22 | } 23 | return wm.xc.SetActiveWindow(win) 24 | } 25 | 26 | func (wm *WM) removeFocus() error { 27 | return wm.setFocus(wm.xc.GetRootWindow(), xproto.TimeCurrentTime) 28 | } 29 | 30 | func (wm *WM) takeFocusProp(prop *xproto.GetPropertyReply, win xproto.Window, time xproto.Timestamp) bool { 31 | for v := prop.Value; len(v) >= 4; v = v[4:] { 32 | switch xproto.Atom(uint32(v[0]) | uint32(v[1])<<8 | uint32(v[2])<<16 | uint32(v[3])<<24) { 33 | case wm.xc.Atom("WM_TAKE_FOCUS"): 34 | _ = xproto.SendEventChecked( 35 | wm.xc.X(), 36 | false, 37 | win, 38 | xproto.EventMaskNoEvent, 39 | string(xproto.ClientMessageEvent{ 40 | Format: 32, 41 | Window: win, 42 | Type: wm.xc.Atom("WM_PROTOCOLS"), 43 | Data: xproto.ClientMessageDataUnionData32New([]uint32{ 44 | uint32(wm.xc.Atom("WM_TAKE_FOCUS")), 45 | uint32(time), 46 | 0, 47 | 0, 48 | 0, 49 | }), 50 | }.Bytes()), 51 | ).Check() 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | func (wm *WM) warpPointerToFrame(f *frame) error { 59 | geom := f.cli.Geom() 60 | return wm.xc.WarpPointer(geom.X+int16(geom.W/2), geom.Y+int16(geom.H/2)) 61 | } 62 | -------------------------------------------------------------------------------- /wm/frame.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "github.com/BurntSushi/xgb/xproto" 5 | "github.com/patrislav/marwind/client" 6 | "github.com/patrislav/marwind/x11" 7 | ) 8 | 9 | type frame struct { 10 | col *column 11 | cli *client.Client 12 | height uint16 13 | } 14 | 15 | func (wm *WM) createFrame(win xproto.Window, typ client.Type) (*frame, error) { 16 | c, err := client.New(wm.xc, wm.windowConfig, win, typ) 17 | if err != nil { 18 | return nil, err 19 | } 20 | f := &frame{cli: c} 21 | 22 | return f, nil 23 | } 24 | 25 | func (f *frame) workspace() *workspace { 26 | if f.col != nil { 27 | return f.col.ws 28 | } 29 | return nil 30 | } 31 | 32 | func (wm *WM) getFrameDecorations(f *frame) x11.Dimensions { 33 | if f.cli.Parent() == 0 { 34 | return x11.Dimensions{Top: 0, Left: 0, Right: 0, Bottom: 0} 35 | } 36 | var bar uint32 37 | border := uint32(wm.config.BorderWidth) 38 | if wm.config.TitleBarHeight > 0 { 39 | bar = uint32(wm.config.TitleBarHeight) + 1 40 | } 41 | return x11.Dimensions{ 42 | Top: border + bar, 43 | Right: border, 44 | Bottom: border, 45 | Left: border, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /wm/manage.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/BurntSushi/xgb/xproto" 7 | 8 | "github.com/patrislav/marwind/client" 9 | ) 10 | 11 | func (wm *WM) manageWindow(win xproto.Window) error { 12 | typ, err := wm.getWindowType(win) 13 | if err != nil { 14 | return fmt.Errorf("failed to get window type: %v", err) 15 | } 16 | mask := uint32(xproto.EventMaskStructureNotify | xproto.EventMaskEnterWindow | xproto.EventMaskPropertyChange) 17 | cookie := xproto.ChangeWindowAttributesChecked(wm.xc.X(), win, xproto.CwEventMask, []uint32{mask}) 18 | if err := cookie.Check(); err != nil { 19 | return fmt.Errorf("failed to change window attributes: %v", err) 20 | } 21 | f, err := wm.createFrame(win, typ) 22 | if err != nil { 23 | return fmt.Errorf("failed to frame the window: %v", err) 24 | } 25 | switch f.cli.Type() { 26 | case client.TypeNormal: 27 | ws := wm.outputs[0].activeWs 28 | if err := ws.addFrame(f); err != nil { 29 | return fmt.Errorf("failed to add frame: %v", err) 30 | } 31 | if err := wm.renderWorkspace(ws); err != nil { 32 | return fmt.Errorf("failed to render workspace: %v", err) 33 | } 34 | case client.TypeDock: 35 | if err := wm.outputs[0].addDock(f); err != nil { 36 | return fmt.Errorf("failed to add dock: %v", err) 37 | } 38 | if err := wm.renderOutput(wm.outputs[0]); err != nil { 39 | return fmt.Errorf("failed to render output: %v", err) 40 | } 41 | } 42 | return nil 43 | } 44 | 45 | func (wm *WM) getWindowType(win xproto.Window) (client.Type, error) { 46 | typeAtom := wm.xc.Atom("_NET_WM_WINDOW_TYPE") 47 | dockTypeAtom := wm.xc.Atom("_NET_WM_WINDOW_TYPE_DOCK") 48 | normalTypeAtom := wm.xc.Atom("_NET_WM_WINDOW_TYPE_NORMAL") 49 | prop, err := xproto.GetProperty(wm.xc.X(), false, win, typeAtom, xproto.GetPropertyTypeAny, 0, 64).Reply() 50 | if err != nil { 51 | return client.TypeUnknown, err 52 | } 53 | if prop != nil { 54 | for v := prop.Value; len(v) >= 4; v = v[4:] { 55 | switch xproto.Atom(uint32(v[0]) | uint32(v[1])<<8 | uint32(v[2])<<16 | uint32(v[3])<<24) { 56 | case dockTypeAtom: 57 | return client.TypeDock, nil 58 | case normalTypeAtom: 59 | return client.TypeNormal, nil 60 | } 61 | } 62 | } 63 | return client.TypeNormal, nil 64 | } 65 | -------------------------------------------------------------------------------- /wm/move.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/BurntSushi/xgb/xproto" 7 | ) 8 | 9 | type MoveDirection uint8 10 | 11 | const ( 12 | MoveLeft MoveDirection = iota 13 | MoveRight 14 | MoveUp 15 | MoveDown 16 | ) 17 | 18 | type ResizeDirection uint8 19 | 20 | const ( 21 | ResizeVert ResizeDirection = iota 22 | ResizeHoriz 23 | ) 24 | 25 | func (wm *WM) switchWorkspace(id uint8) error { 26 | ws, err := wm.ensureWorkspace(id) 27 | if err != nil { 28 | return fmt.Errorf("failed to ensure workspace: %v", err) 29 | } 30 | if err := ws.output.switchWorkspace(ws); err != nil { 31 | return fmt.Errorf("output unable to switch workpace: %v", err) 32 | } 33 | if err := wm.renderWorkspace(ws); err != nil { 34 | return fmt.Errorf("wm.renderWorkspace: %w", err) 35 | } 36 | if err := wm.updateDesktopHints(); err != nil { 37 | return fmt.Errorf("failed to update desktop hints: %v", err) 38 | } 39 | if err := wm.removeFocus(); err != nil { 40 | return fmt.Errorf("failed to remove focus: %v", err) 41 | } 42 | 43 | // TODO: temporary solution! Focuses always the first window of the first column 44 | // Better approach: implement a window focus stack for each workspace, on switch focus the top-of-stack window 45 | if len(ws.columns) > 0 && len(ws.columns[0].frames) > 0 { 46 | win := ws.columns[0].frames[0].cli.Window() 47 | if err := wm.setFocus(win, xproto.TimeCurrentTime); err != nil { 48 | return fmt.Errorf("failed to set focus: %w", err) 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | func (wm *WM) moveFrameToWorkspace(f *frame, wsID uint8) error { 55 | current := wm.outputs[0].activeWs 56 | next, err := wm.ensureWorkspace(wsID) 57 | if err != nil { 58 | return err 59 | } 60 | if next == current { 61 | return nil 62 | } 63 | if !current.deleteFrame(f) { 64 | return fmt.Errorf("frame not contained within workspace %d", wsID) 65 | } 66 | if err := next.addFrame(f); err != nil { 67 | return fmt.Errorf("failed to add the frame to the next workspace: %v", err) 68 | } 69 | if err := f.cli.Unmap(); err != nil { 70 | return fmt.Errorf("failed to unmap the frame: %v", err) 71 | } 72 | if err := wm.renderWorkspace(next); err != nil { 73 | return fmt.Errorf("failed to render next workspace: %v", err) 74 | } 75 | if err := wm.renderWorkspace(current); err != nil { 76 | return fmt.Errorf("failed to render previous workspace: %v", err) 77 | } 78 | if err := wm.updateDesktopHints(); err != nil { 79 | return fmt.Errorf("failed to update desktop hints: %v", err) 80 | } 81 | return nil 82 | } 83 | 84 | // ensureWorkspace looks up a workspace by ID, adding it to the current output if needed 85 | func (wm *WM) ensureWorkspace(id uint8) (*workspace, error) { 86 | var nextWs *workspace 87 | for _, ws := range wm.workspaces { 88 | if ws.id == id { 89 | nextWs = ws 90 | break 91 | } 92 | } 93 | if nextWs == nil { 94 | return nil, fmt.Errorf("no workspace with ID %d", id) 95 | } 96 | switch { 97 | case nextWs.output == nil: 98 | if err := wm.outputs[0].addWorkspace(nextWs); err != nil { 99 | return nil, err 100 | } 101 | case nextWs.output != wm.outputs[0]: 102 | return nil, fmt.Errorf("multiple outputs not supported yet") 103 | } 104 | return nextWs, nil 105 | } 106 | -------------------------------------------------------------------------------- /wm/output.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/patrislav/marwind/client" 8 | 9 | "github.com/patrislav/marwind/x11" 10 | ) 11 | 12 | type dockArea uint8 13 | 14 | const ( 15 | dockAreaTop dockArea = 0 16 | dockAreaBottom dockArea = 1 17 | ) 18 | 19 | type output struct { 20 | xc *x11.Connection 21 | geom client.Geom 22 | workspaces []*workspace 23 | activeWs *workspace 24 | dockAreas [2][]*frame 25 | } 26 | 27 | // newOutput creates a new output from the given geometry 28 | func newOutput(xc *x11.Connection, geom client.Geom) *output { 29 | return &output{xc: xc, geom: geom} 30 | } 31 | 32 | // addWorkspace appends the workspace to this output, sorting them, 33 | // and setting the activeWs if it's currently nil 34 | func (o *output) addWorkspace(ws *workspace) error { 35 | ws.setOutput(o) 36 | o.workspaces = append(o.workspaces, ws) 37 | sort.Slice(o.workspaces, func(i, j int) bool { 38 | return o.workspaces[i].id < o.workspaces[j].id 39 | }) 40 | if o.activeWs == nil { 41 | o.activeWs = ws 42 | return ws.show() 43 | } 44 | return nil 45 | } 46 | 47 | func (o *output) switchWorkspace(next *workspace) error { 48 | if next == o.activeWs { 49 | return nil 50 | } 51 | if ch := o.findWorkspace(func(ws *workspace) bool { return ws == next }); ch == nil { 52 | return fmt.Errorf("workspace not part of this output") 53 | } 54 | if err := next.show(); err != nil { 55 | return fmt.Errorf("failed to show next workspace: %v", err) 56 | } 57 | if err := o.activeWs.hide(); err != nil { 58 | return fmt.Errorf("failed to hide previous workspace: %v", err) 59 | } 60 | if len(o.activeWs.columns) == 0 { 61 | o.removeWorkspace(o.activeWs) 62 | } 63 | o.activeWs = next 64 | return nil 65 | } 66 | 67 | func (o *output) findWorkspace(predicate func(*workspace) bool) *workspace { 68 | for _, ws := range o.workspaces { 69 | if predicate(ws) { 70 | return ws 71 | } 72 | } 73 | return nil 74 | } 75 | 76 | func (o *output) removeWorkspace(ws *workspace) { 77 | for i, w := range o.workspaces { 78 | if w == ws { 79 | o.workspaces = append(o.workspaces[:i], o.workspaces[i+1:]...) 80 | ws.output = nil 81 | return 82 | } 83 | } 84 | } 85 | 86 | // addDock appends the frame as a dock of this output 87 | func (o *output) addDock(f *frame) error { 88 | struts, err := o.xc.GetWindowStruts(f.cli.Window()) 89 | if err != nil { 90 | return fmt.Errorf("failed to get struts: %v", err) 91 | } 92 | var area dockArea 93 | switch { 94 | case struts.Top > struts.Bottom: 95 | area = dockAreaTop 96 | f.height = uint16(struts.Top) 97 | case struts.Bottom > struts.Top: 98 | area = dockAreaBottom 99 | f.height = uint16(struts.Bottom) 100 | default: 101 | return fmt.Errorf("could not determine the dock position") 102 | } 103 | o.dockAreas[area] = append(o.dockAreas[area], f) 104 | // TODO map the dock 105 | o.updateTiling() 106 | return f.cli.Map() 107 | } 108 | 109 | // dockHeight returns the height of the entire dock area 110 | func (o *output) dockHeight(area dockArea) uint16 { 111 | var height uint16 112 | for _, f := range o.dockAreas[area] { 113 | height += f.height 114 | } 115 | return height 116 | } 117 | 118 | func (o *output) workspaceArea() client.Geom { 119 | top := o.dockHeight(dockAreaTop) 120 | bottom := o.dockHeight(dockAreaBottom) 121 | return client.Geom{ 122 | X: o.geom.X, 123 | Y: o.geom.Y + int16(top), 124 | W: o.geom.W, 125 | H: o.geom.H - top - bottom, 126 | } 127 | } 128 | 129 | func (o *output) deleteFrame(frm *frame) bool { 130 | for area := range o.dockAreas { 131 | for i, f := range o.dockAreas[area] { 132 | if frm == f { 133 | o.dockAreas[area] = append(o.dockAreas[area][:i], o.dockAreas[area][i+1:]...) 134 | o.updateTiling() 135 | return true 136 | } 137 | } 138 | } 139 | for _, ws := range o.workspaces { 140 | if ws.deleteFrame(frm) { 141 | return true 142 | } 143 | } 144 | return false 145 | } 146 | 147 | func (o *output) updateTiling() { 148 | for _, ws := range o.workspaces { 149 | ws.updateTiling() 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /wm/render.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "github.com/BurntSushi/xgb/xproto" 5 | "github.com/patrislav/marwind/client" 6 | ) 7 | 8 | func (wm *WM) renderOutput(o *output) error { 9 | var err error 10 | if e := wm.renderDock(o, dockAreaTop); e != nil { 11 | err = e 12 | } 13 | if e := wm.renderDock(o, dockAreaBottom); e != nil { 14 | err = e 15 | } 16 | if e := wm.renderWorkspace(o.activeWs); e != nil { 17 | err = e 18 | } 19 | return err 20 | } 21 | 22 | func (wm *WM) renderDock(o *output, area dockArea) error { 23 | var err error 24 | var y int16 25 | switch area { 26 | case dockAreaTop: 27 | y = o.geom.Y 28 | case dockAreaBottom: 29 | y = int16(o.geom.H - o.dockHeight(area)) 30 | } 31 | for _, f := range o.dockAreas[area] { 32 | geom := client.Geom{ 33 | X: o.geom.X, 34 | Y: y, 35 | W: o.geom.W, 36 | H: f.height, 37 | } 38 | err = wm.renderFrame(f, geom) 39 | y += int16(geom.H) 40 | } 41 | return err 42 | } 43 | 44 | func (wm *WM) renderWorkspace(ws *workspace) error { 45 | var err error 46 | if f := ws.singleFrame(); f != nil { 47 | return wm.renderFrame(f, ws.fullArea()) 48 | } 49 | a := ws.area() 50 | x := a.X 51 | for _, col := range ws.columns { 52 | geom := client.Geom{ 53 | X: x, 54 | Y: a.Y, 55 | W: col.width, 56 | H: a.H, 57 | } 58 | if e := wm.renderColumn(col, geom); e != nil { 59 | err = e 60 | } 61 | x += int16(col.width) 62 | } 63 | return err 64 | } 65 | 66 | func (wm *WM) renderColumn(col *column, geom client.Geom) error { 67 | var err error 68 | y := geom.Y 69 | gap := wm.config.InnerGap 70 | for _, f := range col.frames { 71 | fg := client.Geom{ 72 | X: geom.X + int16(gap), 73 | Y: y + int16(gap), 74 | W: geom.W - gap*2, 75 | H: f.height - gap*2, 76 | } 77 | if e := wm.renderFrame(f, fg); e != nil { 78 | err = e 79 | } 80 | y += int16(f.height) 81 | } 82 | return err 83 | } 84 | 85 | func (wm *WM) renderFrame(f *frame, geom client.Geom) error { 86 | if !f.cli.Mapped() { 87 | return nil 88 | } 89 | f.cli.SetGeom(geom) 90 | mask := uint16(xproto.ConfigWindowX | xproto.ConfigWindowY | xproto.ConfigWindowWidth | xproto.ConfigWindowHeight) 91 | parentVals := []uint32{uint32(geom.X), uint32(geom.Y), uint32(geom.W), uint32(geom.H)} 92 | clientVals := parentVals 93 | if f.cli.Parent() != 0 { 94 | if err := xproto.ConfigureWindowChecked(wm.xc.X(), f.cli.Parent(), mask, parentVals).Check(); err != nil { 95 | return err 96 | } 97 | d := wm.getFrameDecorations(f) 98 | clientVals = []uint32{d.Left, d.Top, uint32(geom.W) - d.Left - d.Right, uint32(geom.H) - d.Top - d.Bottom} 99 | } 100 | if err := xproto.ConfigureWindowChecked(wm.xc.X(), f.cli.Window(), mask, clientVals).Check(); err != nil { 101 | return err 102 | } 103 | if err := wm.configureNotify(f); err != nil { 104 | return err 105 | } 106 | return nil 107 | } 108 | 109 | func (wm *WM) configureNotify(f *frame) error { 110 | // Hack for Java applications as described here: 111 | // https://stackoverflow.com/questions/31646544/xlib-reparenting-a-java-window-with-popups-properly-translated 112 | // TODO: when window decorations are added, this should change to include them 113 | geom := f.cli.Geom() 114 | if f.cli.Parent() != 0 { 115 | d := wm.getFrameDecorations(f) 116 | geom = client.Geom{ 117 | X: geom.X + int16(d.Left), 118 | Y: geom.Y + int16(d.Top), 119 | W: geom.W - uint16(d.Left-d.Right), 120 | H: geom.H - uint16(d.Top-d.Bottom), 121 | } 122 | } 123 | ev := xproto.ConfigureNotifyEvent{ 124 | Event: f.cli.Window(), 125 | Window: f.cli.Window(), 126 | X: geom.X, 127 | Y: geom.Y, 128 | Width: geom.W, 129 | Height: geom.H, 130 | BorderWidth: 0, 131 | AboveSibling: 0, 132 | OverrideRedirect: true, 133 | } 134 | evCookie := xproto.SendEventChecked(wm.xc.X(), false, f.cli.Window(), xproto.EventMaskStructureNotify, string(ev.Bytes())) 135 | if err := evCookie.Check(); err != nil { 136 | return err 137 | } 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /wm/wm.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/BurntSushi/xgb/xproto" 8 | "github.com/patrislav/marwind/client" 9 | "github.com/patrislav/marwind/keysym" 10 | "github.com/patrislav/marwind/x11" 11 | ) 12 | 13 | const maxWorkspaces = 10 14 | 15 | // WM is a struct representing the Window Manager 16 | type WM struct { 17 | xc *x11.Connection 18 | outputs []*output 19 | keymap keysym.Keymap 20 | actions []*action 21 | config Config 22 | workspaces [maxWorkspaces]*workspace 23 | activeWin xproto.Window 24 | windowConfig *client.Config 25 | } 26 | 27 | // New initializes a WM and creates an X11 connection 28 | func New(config Config) (*WM, error) { 29 | wc := &client.Config{ 30 | BgColor: config.BorderColor, 31 | TitlebarHeight: config.TitleBarHeight, 32 | FontColor: config.TitleBarFontColorActive, 33 | FontSize: config.TitleBarFontSize, 34 | BorderWidth: config.BorderWidth, 35 | } 36 | xconn, err := x11.Connect() 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to create WM: %v", err) 39 | } 40 | wm := &WM{xc: xconn, config: config, windowConfig: wc} 41 | return wm, nil 42 | } 43 | 44 | // Init initializes the WM 45 | func (wm *WM) Init() error { 46 | if err := wm.xc.Init(); err != nil { 47 | return fmt.Errorf("failed to init WM: %v", err) 48 | } 49 | if err := wm.becomeWM(); err != nil { 50 | if _, ok := err.(xproto.AccessError); ok { 51 | return fmt.Errorf("could not become WM, possibly another WM is already running") 52 | } 53 | return fmt.Errorf("could not become WM: %v", err) 54 | } 55 | km, err := keysym.LoadKeyMapping(wm.xc.X()) 56 | if err != nil { 57 | return fmt.Errorf("failed to load key mapping: %v", err) 58 | } 59 | wm.keymap = *km 60 | wm.actions = initActions(wm) 61 | if err := wm.grabKeys(); err != nil { 62 | return fmt.Errorf("failed to grab keys: %v", err) 63 | } 64 | 65 | o := newOutput(wm.xc, client.Geom{ 66 | X: 0, Y: 0, 67 | W: wm.xc.Screen().WidthInPixels, 68 | H: wm.xc.Screen().HeightInPixels, 69 | }) 70 | for i := 0; i < maxWorkspaces; i++ { 71 | wm.workspaces[i] = newWorkspace(uint8(i), workspaceConfig{gap: wm.config.OuterGap}) 72 | } 73 | if err := o.addWorkspace(wm.workspaces[0]); err != nil { 74 | return fmt.Errorf("failed to add workspace to output: %v", err) 75 | } 76 | wm.outputs = append(wm.outputs, o) 77 | 78 | if err := wm.xc.SetWMName("Marwind"); err != nil { 79 | return fmt.Errorf("failed to set WM name: %v", err) 80 | } 81 | if err := wm.manageExistingClients(); err != nil { 82 | return fmt.Errorf("failed to manage existing clients: %v", err) 83 | } 84 | return nil 85 | } 86 | 87 | // Close cleans up the WM's resources 88 | func (wm *WM) Close() { 89 | if wm.xc != nil { 90 | wm.xc.Close() 91 | } 92 | } 93 | 94 | // Run starts the WM's X event loop 95 | func (wm *WM) Run() error { 96 | if err := wm.updateDesktopHints(); err != nil { 97 | return err 98 | } 99 | handler := eventHandler{wm: wm} 100 | handler.eventLoop() 101 | return nil 102 | } 103 | 104 | // becomeWM updates the X root window's attributes in an attempt to manage other windows 105 | func (wm *WM) becomeWM() error { 106 | evtMask := []uint32{ 107 | xproto.EventMaskKeyPress | 108 | xproto.EventMaskKeyRelease | 109 | xproto.EventMaskButtonPress | 110 | xproto.EventMaskButtonRelease | 111 | xproto.EventMaskPropertyChange | 112 | xproto.EventMaskFocusChange | 113 | xproto.EventMaskStructureNotify | 114 | xproto.EventMaskSubstructureRedirect, 115 | } 116 | return xproto.ChangeWindowAttributesChecked(wm.xc.X(), wm.xc.GetRootWindow(), xproto.CwEventMask, evtMask).Check() 117 | } 118 | 119 | // grabKeys attempts to get a sole ownership of certain key combinations 120 | func (wm *WM) grabKeys() error { 121 | for _, action := range wm.actions { 122 | for _, code := range action.codes { 123 | cookie := xproto.GrabKeyChecked( 124 | wm.xc.X(), 125 | false, 126 | wm.xc.GetRootWindow(), 127 | uint16(action.modifiers), 128 | code, 129 | xproto.GrabModeAsync, 130 | xproto.GrabModeAsync, 131 | ) 132 | if err := cookie.Check(); err != nil { 133 | return err 134 | } 135 | } 136 | } 137 | return nil 138 | } 139 | 140 | func (wm *WM) findFrame(predicate func(*frame) bool) *frame { 141 | for _, ws := range wm.workspaces { 142 | for _, col := range ws.columns { 143 | for _, f := range col.frames { 144 | if predicate(f) { 145 | return f 146 | } 147 | } 148 | } 149 | } 150 | for _, o := range wm.outputs { 151 | for area := range o.dockAreas { 152 | for _, f := range o.dockAreas[area] { 153 | if predicate(f) { 154 | return f 155 | } 156 | } 157 | } 158 | } 159 | return nil 160 | } 161 | 162 | func (wm *WM) deleteFrame(f *frame) error { 163 | for _, o := range wm.outputs { 164 | if o.deleteFrame(f) { 165 | if err := wm.removeFocus(); err != nil { 166 | return err 167 | } 168 | return wm.renderOutput(o) 169 | } 170 | } 171 | return fmt.Errorf("could not find frame to delete: %v", f) 172 | } 173 | 174 | func (wm *WM) handleKeyPressEvent(e xproto.KeyPressEvent) error { 175 | sym := wm.keymap[e.Detail][0] 176 | for _, action := range wm.actions { 177 | if sym == action.sym && e.State == uint16(action.modifiers) { 178 | return action.act() 179 | } 180 | } 181 | return nil 182 | } 183 | 184 | // TODO: avoid updating all hints at once 185 | func (wm *WM) updateDesktopHints() error { 186 | out := wm.outputs[0] 187 | wsWins := make([][]xproto.Window, len(out.workspaces)) 188 | names := make([]string, len(out.workspaces)) 189 | current := 0 190 | for i, ws := range out.workspaces { 191 | names[i] = fmt.Sprintf("%d", ws.id+1) 192 | for _, col := range ws.columns { 193 | for _, f := range col.frames { 194 | wsWins[i] = append(wsWins[i], f.cli.Window()) 195 | } 196 | } 197 | if ws == out.activeWs { 198 | current = i 199 | for area := range out.dockAreas { 200 | for _, f := range out.dockAreas[area] { 201 | wsWins[i] = append(wsWins[i], f.cli.Window()) 202 | } 203 | } 204 | } 205 | } 206 | windows := make([]xproto.Window, 0) 207 | for _, wins := range wsWins { 208 | windows = append(windows, wins...) 209 | } 210 | if err := wm.xc.SetDesktopHints(names, current, windows); err != nil { 211 | return err 212 | } 213 | var err error 214 | for i, wins := range wsWins { 215 | for _, win := range wins { 216 | if e := wm.xc.SetWindowDesktop(win, i); e != nil { 217 | err = e 218 | } 219 | } 220 | } 221 | return err 222 | } 223 | 224 | func (wm *WM) handleConfigureRequest(e xproto.ConfigureRequestEvent) error { 225 | f := wm.findFrame(func(frm *frame) bool { return frm.cli.Window() == e.Window }) 226 | if f != nil { 227 | if err := wm.configureNotify(f); err != nil { 228 | return fmt.Errorf("failed to send ConfigureNotify event to %d: %v", e.Window, err) 229 | } 230 | return nil 231 | } 232 | ev := xproto.ConfigureNotifyEvent{ 233 | Event: e.Window, 234 | Window: e.Window, 235 | AboveSibling: 0, 236 | X: e.X, 237 | Y: e.Y, 238 | Width: e.Width, 239 | Height: e.Height, 240 | BorderWidth: 0, 241 | OverrideRedirect: false, 242 | } 243 | xproto.SendEventChecked(wm.xc.X(), false, e.Window, xproto.EventMaskStructureNotify, string(ev.Bytes())) 244 | return nil 245 | } 246 | 247 | func (wm *WM) manageExistingClients() error { 248 | tree, err := xproto.QueryTree(wm.xc.X(), wm.xc.GetRootWindow()).Reply() 249 | if err != nil { 250 | return err 251 | } 252 | for _, win := range tree.Children { 253 | attrs, err := xproto.GetWindowAttributes(wm.xc.X(), win).Reply() 254 | if err != nil { 255 | continue 256 | } 257 | if attrs.MapState == xproto.MapStateUnmapped || attrs.OverrideRedirect { 258 | continue 259 | } 260 | if err := wm.manageWindow(win); err != nil { 261 | log.Println("Failed to manage an existing window:", err) 262 | } 263 | } 264 | if err := wm.updateDesktopHints(); err != nil { 265 | return err 266 | } 267 | return wm.renderOutput(wm.outputs[0]) 268 | } 269 | -------------------------------------------------------------------------------- /wm/workspace.go: -------------------------------------------------------------------------------- 1 | package wm 2 | 3 | import ( 4 | "github.com/patrislav/marwind/client" 5 | ) 6 | 7 | type workspaceConfig struct { 8 | gap uint16 9 | } 10 | 11 | type workspace struct { 12 | id uint8 13 | columns []*column 14 | output *output 15 | config workspaceConfig 16 | } 17 | 18 | func newWorkspace(id uint8, config workspaceConfig) *workspace { 19 | return &workspace{id: id, config: config} 20 | } 21 | 22 | func (ws *workspace) setOutput(o *output) { 23 | ws.output = o 24 | } 25 | 26 | // addFrame appends the given frame to the last column in the workspace 27 | func (ws *workspace) addFrame(f *frame) error { 28 | var col *column 29 | if len(ws.columns) < 2 { 30 | col = ws.createColumn(false) 31 | } 32 | if col == nil { 33 | col = ws.columns[len(ws.columns)-1] 34 | } 35 | col.addFrame(f, nil) 36 | if ws.output.activeWs == ws { 37 | return f.cli.Map() 38 | } 39 | return nil 40 | } 41 | 42 | // deleteFrame deletes the frame from any column that contains it 43 | func (ws *workspace) deleteFrame(f *frame) bool { 44 | if f.col == nil || f.col.ws != ws { 45 | return false 46 | } 47 | col := f.col 48 | col.deleteFrame(f) 49 | if len(col.frames) == 0 { 50 | ws.deleteColumn(col) 51 | } 52 | return true 53 | } 54 | 55 | // moveFrame changes the position of a frame within a column or moves it between columns 56 | func (ws *workspace) moveFrame(f *frame, dir MoveDirection) error { 57 | switch dir { 58 | case MoveLeft: 59 | i := ws.findColumnIndex(func(c *column) bool { return c == f.col }) 60 | origCol := f.col 61 | origCol.deleteFrame(f) 62 | if i == 0 { 63 | col := ws.createColumn(true) 64 | col.addFrame(f, nil) 65 | } else { 66 | col := ws.columns[i-1] 67 | col.addFrame(f, nil) 68 | } 69 | if len(origCol.frames) == 0 { 70 | ws.deleteColumn(origCol) 71 | } 72 | case MoveRight: 73 | i := ws.findColumnIndex(func(c *column) bool { return c == f.col }) 74 | origCol := f.col 75 | origCol.deleteFrame(f) 76 | if i == len(ws.columns)-1 { 77 | col := ws.createColumn(false) 78 | col.addFrame(f, nil) 79 | } else { 80 | col := ws.columns[i+1] 81 | col.addFrame(f, nil) 82 | } 83 | if len(origCol.frames) == 0 { 84 | ws.deleteColumn(origCol) 85 | } 86 | case MoveUp: 87 | col := f.col 88 | i := col.findFrameIndex(func(frm *frame) bool { return f == frm }) 89 | if i > 0 { 90 | other := col.frames[i-1] 91 | col.frames[i-1] = f 92 | col.frames[i] = other 93 | } 94 | case MoveDown: 95 | col := f.col 96 | i := col.findFrameIndex(func(frm *frame) bool { return f == frm }) 97 | if i < len(col.frames)-1 { 98 | other := col.frames[i+1] 99 | col.frames[i+1] = f 100 | col.frames[i] = other 101 | } 102 | } 103 | return nil 104 | } 105 | 106 | // resizeFrame changes the size of the frame by the given percent 107 | func (ws *workspace) resizeFrame(f *frame, dir ResizeDirection, pct int) error { 108 | switch dir { 109 | case ResizeHoriz: 110 | if len(ws.columns) < 2 { 111 | return nil 112 | } 113 | min := uint16(float32(ws.area().W) * 0.1) 114 | dwFull := int(float32(ws.area().W) * (float32(pct) / 100)) 115 | if uint16(int(f.col.width)+dwFull) < min { 116 | return nil 117 | } 118 | dwPart := dwFull/len(ws.columns) - 1 119 | dwFinal := 0 120 | for _, col := range ws.columns { 121 | if col != f.col { 122 | next := uint16(int(col.width) - dwPart) 123 | if next >= min { 124 | col.width = next 125 | dwFinal += dwPart 126 | } 127 | } 128 | } 129 | f.col.width = uint16(int(f.col.width) + dwFinal) 130 | case ResizeVert: 131 | col := f.col 132 | if len(col.frames) < 2 { 133 | return nil 134 | } 135 | min := uint16(float32(ws.area().H) * 0.1) 136 | dhFull := int(float32(ws.area().H) * (float32(pct) / 100)) 137 | if uint16(int(f.height)+dhFull) < min { 138 | return nil 139 | } 140 | dhPart := dhFull/len(col.frames) - 1 141 | dhFinal := 0 142 | for _, other := range col.frames { 143 | if f != other { 144 | next := uint16(int(other.height) - dhPart) 145 | if next >= min { 146 | other.height = next 147 | dhFinal += dhPart 148 | } 149 | } 150 | } 151 | f.height = uint16(int(f.height) + dhFinal) 152 | } 153 | return nil 154 | } 155 | 156 | // show maps all the frames of the workspace 157 | func (ws *workspace) show() error { 158 | var err error 159 | for _, col := range ws.columns { 160 | for _, f := range col.frames { 161 | if e := f.cli.Map(); e != nil { 162 | err = e 163 | } 164 | } 165 | } 166 | return err 167 | } 168 | 169 | // hide unmaps all the frames of the workspace 170 | func (ws *workspace) hide() error { 171 | var err error 172 | for _, col := range ws.columns { 173 | for _, f := range col.frames { 174 | if e := f.cli.Unmap(); e != nil { 175 | err = e 176 | } 177 | } 178 | } 179 | return err 180 | } 181 | 182 | // createColumn creates a new empty column either at the start (if the start argument is true) 183 | // or the end of the workspace area. 184 | func (ws *workspace) createColumn(start bool) *column { 185 | wsWidth := ws.area().W 186 | origLen := len(ws.columns) 187 | col := &column{ws: ws, width: ws.area().W / uint16(origLen+1)} 188 | if origLen > 0 { 189 | col.width = wsWidth / uint16(origLen+1) 190 | remWidth := float32(wsWidth - col.width) 191 | leftWidth := uint16(remWidth) 192 | for _, c := range ws.columns { 193 | c.width = uint16((float32(c.width) / float32(wsWidth)) * remWidth) 194 | leftWidth -= c.width 195 | } 196 | if leftWidth != 0 { 197 | col.width += leftWidth 198 | } 199 | } else { 200 | col.width = wsWidth 201 | } 202 | if start { 203 | ws.columns = append([]*column{col}, ws.columns...) 204 | } else { 205 | ws.columns = append(ws.columns, col) 206 | } 207 | return col 208 | } 209 | 210 | func (ws *workspace) deleteColumn(col *column) { 211 | i := ws.findColumnIndex(func(c *column) bool { return c == col }) 212 | if i < 0 { 213 | return 214 | } 215 | wsWidth := ws.area().W 216 | // TODO: assign the widths proportional to the original width/totalWidth ratio 217 | // origLen = len(ws.columns) 218 | ws.columns = append(ws.columns[:i], ws.columns[i+1:]...) 219 | for _, c := range ws.columns { 220 | c.width = wsWidth / uint16(len(ws.columns)) 221 | } 222 | } 223 | 224 | func (ws *workspace) findColumnIndex(predicate func(*column) bool) int { 225 | for i, col := range ws.columns { 226 | if predicate(col) { 227 | return i 228 | } 229 | } 230 | return -1 231 | } 232 | 233 | func (ws *workspace) updateTiling() { 234 | for _, col := range ws.columns { 235 | col.updateTiling() 236 | } 237 | } 238 | 239 | func (ws *workspace) fullArea() client.Geom { return ws.output.workspaceArea() } 240 | 241 | func (ws *workspace) area() client.Geom { 242 | a := ws.fullArea() 243 | return client.Geom{ 244 | X: a.X + int16(ws.config.gap), 245 | Y: a.Y + int16(ws.config.gap), 246 | W: a.W - ws.config.gap*2, 247 | H: a.H - ws.config.gap*2, 248 | } 249 | } 250 | 251 | // singleFrame returns a single frame if there's only one in the workspace, nil otherwise 252 | func (ws *workspace) singleFrame() *frame { 253 | if ws.countAllFrames() == 1 { 254 | return ws.columns[0].frames[0] 255 | } 256 | return nil 257 | } 258 | 259 | func (ws *workspace) countAllFrames() int { 260 | count := 0 261 | for _, col := range ws.columns { 262 | count += len(col.frames) 263 | } 264 | return count 265 | } 266 | -------------------------------------------------------------------------------- /x11/atom.go: -------------------------------------------------------------------------------- 1 | package x11 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/BurntSushi/xgb/xproto" 7 | ) 8 | 9 | // Atom returns the X11 atom of the given name 10 | func (xc *Connection) Atom(name string) xproto.Atom { 11 | if atom, ok := xc.atoms[name]; ok { 12 | return atom 13 | } 14 | reply, err := xproto.InternAtom(xc.conn, false, uint16(len(name)), name).Reply() 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | if reply == nil { 19 | return 0 20 | } 21 | xc.atoms[name] = reply.Atom 22 | return reply.Atom 23 | } 24 | -------------------------------------------------------------------------------- /x11/connection.go: -------------------------------------------------------------------------------- 1 | package x11 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/BurntSushi/xgb" 8 | "github.com/BurntSushi/xgb/xproto" 9 | "github.com/BurntSushi/xgbutil" 10 | ) 11 | 12 | type Connection struct { 13 | conn *xgb.Conn 14 | util *xgbutil.XUtil 15 | screen xproto.ScreenInfo 16 | atoms map[string]xproto.Atom 17 | } 18 | 19 | func Connect() (*Connection, error) { 20 | atoms := make(map[string]xproto.Atom) 21 | xconn, err := xgb.NewConn() 22 | if err != nil { 23 | return nil, fmt.Errorf("failed to connect: %w", err) 24 | } 25 | xutil, err := xgbutil.NewConnXgb(xconn) 26 | if err != nil { 27 | return nil, fmt.Errorf("failed to create XUtil connection: %w", err) 28 | } 29 | return &Connection{conn: xconn, util: xutil, atoms: atoms}, nil 30 | } 31 | 32 | func (xc *Connection) X() *xgb.Conn { return xc.conn } 33 | func (xc *Connection) Screen() xproto.ScreenInfo { return xc.screen } 34 | 35 | func (xc *Connection) Init() error { 36 | conninfo := xproto.Setup(xc.conn) 37 | if conninfo == nil { 38 | return errors.New("could not parse X connection info") 39 | } 40 | if len(conninfo.Roots) != 1 { 41 | return errors.New("wrong number of roots, possibly xinerama did not initialize properly") 42 | } 43 | xc.screen = conninfo.Roots[0] 44 | 45 | err := xc.setHints() 46 | if err != nil { 47 | return err 48 | } 49 | err = xc.initDesktop() 50 | if err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | 56 | func (xc *Connection) Close() { 57 | xc.conn.Close() 58 | } 59 | -------------------------------------------------------------------------------- /x11/desktop.go: -------------------------------------------------------------------------------- 1 | package x11 2 | 3 | import ( 4 | "github.com/BurntSushi/xgb/xproto" 5 | ) 6 | 7 | const ( 8 | leftPtr = 68 9 | ) 10 | 11 | func (xc *Connection) initDesktop() error { 12 | cursor, err := xc.createCursor(leftPtr) 13 | if err != nil { 14 | return err 15 | } 16 | if err := xproto.ChangeWindowAttributesChecked( 17 | xc.conn, 18 | xc.screen.Root, 19 | xproto.CwCursor, 20 | []uint32{ 21 | uint32(cursor), 22 | }, 23 | ).Check(); err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | 29 | func (xc *Connection) createCursor(cursor uint16) (xproto.Cursor, error) { 30 | fontID, err := xproto.NewFontId(xc.conn) 31 | if err != nil { 32 | return 0, err 33 | } 34 | 35 | cursorID, err := xproto.NewCursorId(xc.conn) 36 | if err != nil { 37 | return 0, err 38 | } 39 | 40 | err = xproto.OpenFontChecked(xc.conn, fontID, 41 | uint16(len("cursor")), "cursor").Check() 42 | if err != nil { 43 | return 0, err 44 | } 45 | 46 | err = xproto.CreateGlyphCursorChecked(xc.conn, cursorID, fontID, fontID, 47 | cursor, cursor+1, 48 | 0, 0, 0, 49 | 0xffff, 0xffff, 0xffff).Check() 50 | if err != nil { 51 | return 0, err 52 | } 53 | 54 | err = xproto.CloseFontChecked(xc.conn, fontID).Check() 55 | if err != nil { 56 | return 0, err 57 | } 58 | 59 | return cursorID, nil 60 | } 61 | 62 | // WarpPointer moves the pointer to an x, y point on the screen 63 | func (xc *Connection) WarpPointer(x, y int16) error { 64 | return xproto.WarpPointerChecked( 65 | xc.conn, xproto.WindowNone, xc.screen.Root, 66 | 0, 0, 0, 0, 67 | x, y, 68 | ).Check() 69 | } 70 | -------------------------------------------------------------------------------- /x11/ewmh.go: -------------------------------------------------------------------------------- 1 | package x11 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/BurntSushi/xgb/xproto" 7 | ) 8 | 9 | // Struts represents the values of the _NET_WM_STRUT/_NET_WM_STRUT_PARTIAL properties. 10 | // The extended _NET_WM_STRUT_PARTIAL values are ignored - the WM will fill the entire width of the screen instead. 11 | type Struts struct { 12 | Left, Right, Top, Bottom uint32 13 | } 14 | 15 | // GetWindowStruts returns the values of the window's _NET_WM_STRUT(_PARTIAL) property 16 | func (xc *Connection) GetWindowStruts(win xproto.Window) (*Struts, error) { 17 | // TODO: support _NET_WM_STRUT_PARTIAL as well 18 | propName := "_NET_WM_STRUT" 19 | atom := xc.Atom(propName) 20 | prop, err := xproto.GetProperty(xc.conn, false, win, atom, xproto.AtomCardinal, 2, 32).Reply() 21 | if err != nil { 22 | return nil, err 23 | } 24 | if prop == nil { 25 | return nil, fmt.Errorf("xproto.GetProperty returned a nil reply") 26 | } 27 | values := make([]uint32, len(prop.Value)/4) 28 | for v := prop.Value; len(v) >= 4; v = v[4:] { 29 | values = append(values, uint32(v[0])|uint32(v[1])<<8|uint32(v[2])<<16|uint32(v[3])<<24) 30 | } 31 | if len(values) < 4 { 32 | return nil, fmt.Errorf("not enough values returned by property %s", propName) 33 | } 34 | return &Struts{ 35 | Left: values[0], 36 | Right: values[1], 37 | Top: values[2], 38 | Bottom: values[3], 39 | }, nil 40 | } 41 | 42 | func (xc *Connection) SetWMName(name string) error { 43 | buf := make([]byte, 0) 44 | buf = append(buf, name...) 45 | buf = append(buf, 0) 46 | return xc.changeProp(xc.screen.Root, 8, "_NET_WM_NAME", xproto.AtomString, buf) 47 | } 48 | 49 | func (xc *Connection) GetWindowTitle(window xproto.Window) (string, error) { 50 | reply, err := xc.getProp(window, "_NET_WM_NAME") 51 | if err != nil { 52 | return "", err 53 | } 54 | return string(reply.Value), nil 55 | } 56 | 57 | func (xc *Connection) SetActiveWindow(win xproto.Window) error { 58 | if win == xc.screen.Root { 59 | win = 0 60 | } 61 | return xc.changeProp32(xc.screen.Root, "_NET_ACTIVE_WINDOW", xproto.AtomWindow, uint32(win)) 62 | } 63 | 64 | func (xc *Connection) SetNumberOfDesktops(num int) error { 65 | return xc.changeProp32(xc.screen.Root, "_NET_NUMBER_OF_DESKTOPS", xproto.AtomCardinal, uint32(num)) 66 | } 67 | 68 | func (xc *Connection) SetCurrentDesktop(index int) error { 69 | return xc.changeProp32(xc.screen.Root, "_NET_CURRENT_DESKTOP", xproto.AtomCardinal, uint32(index)) 70 | } 71 | 72 | func (xc *Connection) SetDesktopViewport(num int) error { 73 | vals := make([]uint32, num*2) 74 | return xc.changeProp32(xc.screen.Root, "_NET_DESKTOP_VIEWPORT", xproto.AtomCardinal, vals...) 75 | } 76 | 77 | func (xc *Connection) SetDesktopNames(names []string) error { 78 | buf := make([]byte, 0) 79 | for _, name := range names { 80 | buf = append(buf, name...) 81 | buf = append(buf, 0) 82 | } 83 | return xc.changeProp(xc.screen.Root, 8, "_NET_DESKTOP_NAMES", xc.Atom("UTF8_STRING"), buf) 84 | } 85 | 86 | func (xc *Connection) SetClientList(windows []xproto.Window) error { 87 | vals := make([]uint32, len(windows)) 88 | for i, win := range windows { 89 | vals[i] = uint32(win) 90 | } 91 | return xc.changeProp32(xc.screen.Root, "_NET_CLIENT_LIST", xproto.AtomWindow, vals...) 92 | } 93 | 94 | func (xc *Connection) SetDesktopHints(names []string, index int, windows []xproto.Window) error { 95 | var err error 96 | err = xc.SetNumberOfDesktops(len(names)) 97 | if err != nil { 98 | return err 99 | } 100 | err = xc.SetDesktopViewport(len(names)) 101 | if err != nil { 102 | return err 103 | } 104 | err = xc.SetDesktopNames(names) 105 | if err != nil { 106 | return err 107 | } 108 | err = xc.SetCurrentDesktop(index) 109 | if err != nil { 110 | return err 111 | } 112 | err = xc.SetClientList(windows) 113 | if err != nil { 114 | return err 115 | } 116 | return nil 117 | } 118 | 119 | func (xc *Connection) SetWindowDesktop(win xproto.Window, desktop int) error { 120 | return xc.changeProp32(win, "_NET_WM_DESKTOP", xproto.AtomCardinal, uint32(desktop)) 121 | } 122 | 123 | func (xc *Connection) setHints() error { 124 | atoms := make([]uint32, len(ewmhSupported)) 125 | for i, s := range ewmhSupported { 126 | atoms[i] = uint32(xc.Atom(s)) 127 | } 128 | return xc.changeProp32(xc.screen.Root, "_NET_SUPPORTED", xproto.AtomAtom, atoms...) 129 | } 130 | -------------------------------------------------------------------------------- /x11/ewmh_supported.go: -------------------------------------------------------------------------------- 1 | package x11 2 | 3 | var ewmhSupported = []string{ 4 | "_NET_SUPPORTED", 5 | "_NET_ACTIVE_WINDOW", 6 | "_NET_CURRENT_DESKTOP", 7 | "_NET_DESKTOP_NAMES", 8 | "_NET_DESKTOP_VIEWPORT", 9 | "_NET_NUMBER_OF_DESKTOPS", 10 | "_NET_CLIENT_LIST", 11 | "_NET_WM_STRUT", 12 | // "_NET_WM_STRUT_PARTIAL", 13 | } 14 | -------------------------------------------------------------------------------- /x11/graphics.go: -------------------------------------------------------------------------------- 1 | package x11 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/BurntSushi/xgbutil/xgraphics" 7 | ) 8 | 9 | type Dimensions struct { 10 | Top, Left, Right, Bottom uint32 11 | } 12 | 13 | func (xc *Connection) NewImage(rect image.Rectangle) *xgraphics.Image { 14 | return xgraphics.New(xc.util, rect) 15 | } 16 | -------------------------------------------------------------------------------- /x11/window.go: -------------------------------------------------------------------------------- 1 | package x11 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/BurntSushi/xgb/xproto" 8 | ) 9 | 10 | func (xc *Connection) GracefullyDestroyWindow(win xproto.Window) error { 11 | protos, err := xc.getProps32(win, "WM_PROTOCOLS") 12 | if err != nil { 13 | return fmt.Errorf("could not close window: %v", err) 14 | } 15 | for _, p := range protos { 16 | if xproto.Atom(p) == xc.Atom("WM_DELETE_WINDOW") { 17 | t := time.Now().Unix() 18 | return xproto.SendEventChecked( 19 | xc.conn, 20 | false, 21 | win, 22 | xproto.EventMaskNoEvent, 23 | string(xproto.ClientMessageEvent{ 24 | Format: 32, 25 | Window: win, 26 | Type: xc.Atom("WM_PROTOCOLS"), 27 | Data: xproto.ClientMessageDataUnionData32New([]uint32{ 28 | uint32(xc.Atom("WM_DELETE_WINDOW")), 29 | uint32(t), 30 | 0, 31 | 0, 32 | 0, 33 | }), 34 | }.Bytes()), 35 | ).Check() 36 | } 37 | } 38 | // The window does not follow ICCCM - just destroy it 39 | return xproto.DestroyWindowChecked(xc.conn, win).Check() 40 | } 41 | 42 | func (xc *Connection) GetRootWindow() xproto.Window { 43 | return xc.screen.Root 44 | } 45 | 46 | func (xc *Connection) CreateWindow(parent xproto.Window, x, y int16, width, height, borderWidth, 47 | class uint16, valueMask uint32, valueList []uint32) (xproto.Window, error) { 48 | 49 | id, err := xproto.NewWindowId(xc.conn) 50 | if err != nil { 51 | return 0, err 52 | } 53 | visual := xc.screen.RootVisual 54 | vdepth := xc.screen.RootDepth 55 | err = xproto.CreateWindowChecked(xc.conn, vdepth, id, parent, x, y, width, height, 56 | borderWidth, class, visual, valueMask, valueList).Check() 57 | if err != nil { 58 | return 0, fmt.Errorf("could not create window: %s", err) 59 | } 60 | return id, nil 61 | } 62 | 63 | func (xc *Connection) MapWindow(window xproto.Window) error { 64 | return xproto.MapWindowChecked(xc.conn, window).Check() 65 | } 66 | 67 | func (xc *Connection) UnmapWindow(window xproto.Window) error { 68 | return xproto.UnmapWindowChecked(xc.conn, window).Check() 69 | } 70 | 71 | func (xc *Connection) DestroyWindow(window xproto.Window) error { 72 | return xproto.DestroyWindowChecked(xc.conn, window).Check() 73 | } 74 | 75 | func (xc *Connection) ReparentWindow(window, parent xproto.Window, x, y int16) error { 76 | return xproto.ReparentWindowChecked(xc.conn, window, parent, x, y).Check() 77 | } 78 | -------------------------------------------------------------------------------- /x11/xprop.go: -------------------------------------------------------------------------------- 1 | package x11 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/BurntSushi/xgb" 7 | "github.com/BurntSushi/xgb/xproto" 8 | ) 9 | 10 | func (xc *Connection) getProp(win xproto.Window, name string) (*xproto.GetPropertyReply, error) { 11 | atom := xc.Atom(name) 12 | cookie := xproto.GetProperty(xc.conn, false, win, atom, xproto.GetPropertyTypeAny, 0, 64) 13 | reply, err := cookie.Reply() 14 | if err != nil { 15 | return nil, fmt.Errorf("error retrieving property %q on window %d: %v", name, win, err) 16 | } 17 | if reply == nil || reply.Format == 0 { 18 | return nil, fmt.Errorf("no such property %q on window %d", name, win) 19 | } 20 | return reply, nil 21 | } 22 | 23 | func (xc *Connection) getProps32(win xproto.Window, name string) ([]uint32, error) { 24 | reply, err := xc.getProp(win, name) 25 | if err != nil { 26 | return nil, err 27 | } 28 | vals := make([]uint32, 0) 29 | for v := reply.Value; len(v) >= 4; v = v[4:] { 30 | vals = append(vals, uint32(v[0])|uint32(v[1])<<8|uint32(v[2])<<16|uint32(v[3])<<24) 31 | } 32 | return vals, nil 33 | } 34 | 35 | func (xc *Connection) changeProp32(win xproto.Window, prop string, typ xproto.Atom, data ...uint32) error { 36 | buf := make([]byte, len(data)*4) 37 | for i, datum := range data { 38 | xgb.Put32(buf[(i*4):], datum) 39 | } 40 | return xc.changeProp(win, 32, prop, typ, buf) 41 | } 42 | 43 | func (xc *Connection) changeProp(win xproto.Window, format byte, prop string, typ xproto.Atom, data []byte) error { 44 | propAtom := xc.Atom(prop) 45 | return xproto.ChangePropertyChecked(xc.conn, xproto.PropModeReplace, win, propAtom, typ, format, 46 | uint32(len(data)/(int(format)/8)), data).Check() 47 | } 48 | --------------------------------------------------------------------------------