├── AUTHORS ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── actions.go ├── config.go ├── doc.go ├── doc ├── screenshot0.png └── screenshot1.png ├── draw.go ├── geom.go ├── go.mod ├── go.sum ├── input.go ├── keysym.go ├── main.go └── xinit.go /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Taowm authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | # Names should be added to this file as 6 | # Name or Organization 7 | # The email address is not required for organizations. 8 | 9 | # Please keep the list sorted. 10 | 11 | Google Inc. 12 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the official list of people who can contribute 2 | # (and typically have contributed) code to the Taowm repository. 3 | # The AUTHORS file lists the copyright holders; this file 4 | # lists people. For example, Google employees are listed here 5 | # but not in AUTHORS, because Google holds the copyright. 6 | # 7 | # The submission process automatically checks to make sure 8 | # that people submitting code are listed in this file (by email address). 9 | # 10 | # Names should be added to this file only after verifying that 11 | # the individual or the individual's organization has agreed to 12 | # the appropriate Contributor License Agreement, found here: 13 | # 14 | # http://code.google.com/legal/individual-cla-v1.0.html 15 | # http://code.google.com/legal/corporate-cla-v1.0.html 16 | # 17 | # The agreement for individuals can be filled out on the web. 18 | # 19 | # When adding J Random Contributor's name to this file, 20 | # either J's name or J's organization's name should be 21 | # added to the AUTHORS file, depending on whether the 22 | # individual or corporate CLA was used. 23 | 24 | # Names should be added to this file like so: 25 | # Name 26 | 27 | # Please keep the list sorted. 28 | 29 | Brad Fitzpatrick 30 | John Lunney 31 | Nigel Tao 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 The Taowm Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Taowm is *The Acutely Opinionated Window Manager*. It is a minimalist, keyboard-driven, low distraction, tiling window manager for someone who uses a computer primarily to run just two GUI programs: a web browser and a terminal emulator. 2 | 3 | # INSTALLATION 4 | To install taowm: 5 | 6 | 1. Install Go (as per [golang.org/doc/install](http://golang.org/doc/install) or get it from your distribution). 7 | 2. Run `go get github.com/nigeltao/taowm`. 8 | 9 | This will install taowm in your `$GOPATH`, or under `$GOROOT/bin` if `$GOPATH` is empty. Run `go help gopath` to read more about `$GOPATH`. 10 | 11 | Taowm is designed to run from an Xsession session. Add this line to the end of your `/.xsession`file: 12 | 13 | ``` 14 | /path/to/your/taowm 15 | ``` 16 | 17 | where the path is wherever `go get` or `go install` wrote to. Again, run `go help gopath` for more information. 18 | 19 | Log out and log back in with the "Xsession" option. Some systems, such as Ubuntu 12.04 "Precise", do not offer an Xsession option by default. To enable it, create a new file `/usr/share/xsessions/custom.desktop` that contains: 20 | 21 | ``` 22 | [Desktop Entry] 23 | Name=Xsession 24 | Exec=/etc/X11/Xsession 25 | ``` 26 | 27 | # USAGE 28 | Taowm starts with each screen divided into two side-by-side frames, outlined in green. Frames can frame windows, but they can also be empty: closing a frame's window will not collapse that frame. The frame that contains the mouse pointer is the focused frame, and its border is brighter than other frames. Its window (if it contains one) will have the keyboard focus. 29 | 30 | Taowm is primarily keyboard-driven, and all keyboard shortcuts involve first holding down the Caps Lock key, similar to how holding down the Control key followed by the 'N' key, in your web browser, creates a new browser window. The default Caps Lock behavior, CHANGING ALL TYPED LETTERS TO UPPER CASE, is disabled. 31 | 32 | * Caps Lock and the Space key will open a new web browser window. 33 | * Caps Lock and the Enter key will open a new terminal emulator window. 34 | * Caps Lock and the Shift key and the '|' pipe key will lock the screen. 35 | * Caps Lock and the Backspace key will close the window in the focused frame. 36 | * Caps Lock and the Tab key will cycle through the frames. 37 | 38 | To quit taowm and return to the log in screen, hold down Caps Lock and the Shift key and hit the Escape key three times in quick succession. Normally, this will quit immediately. Some programs may ask for something before closing, such as a file name to write unsaved data to. In this case, taowm will quit in 60 seconds or whenever all such programs have closed, instead of quitting immediately, and the frame borders will turn red. 39 | 40 | * If there are more windows than frames, then Caps Lock and the 'D' or 'F' key will cycle through hidden windows. 41 | * Caps Lock and a number key like '1', '2', etc. will move the 1st, 2nd, etc. window to the focused frame. 42 | * Caps Lock and the 'A' key will show a list of windows: the one currently in the focused frame is marked with a '+', other windows in other frames are marked with a '-', hidden windows that have not been seen yet are marked with an '@', and hidden windows that have been seen before are unmarked. In particular, newly created windows will not automatically be shown. 43 | 44 | Taowm prevents new windows from popping up and 'stealing' keyboard focus, a problem if the password you are typing into your terminal emulator accidentally gets written to a chat window that popped up at the wrong time. Instead, if there isn't an empty frame to accept a new window, taowm keeps that window hidden (and marked with an '@' in the window list) until you are ready to deal with it. If there are any such windows that have not been seen yet, the green frame borders will pulsate to remind you. Selected windows are also marked with a '#'; selection is described below. 45 | 46 | * Caps Lock and the 'G' key will toggle the focused frame in occupying the entire screen. 47 | * Caps Lock and Shift and the 'G' key will hide the window in the focused frame. 48 | * Caps Lock and the '-' key, the '=' key or Shift and the '+' key will split the current frame horizontally, vertically, or merge a frame to undo a frame split respectively. 49 | 50 | A screen contains workspaces like a frame contains windows. 51 | 52 | * Caps Lock and the 'T' key will create a new workspace, hiding the current one. 53 | * Caps Lock and the 'E' or 'R' key will cycle through hidden workspaces. 54 | * Caps Lock and Shift and the 'T' key will delete the current workspace, provided that it holds no windows and there is another hidden workspace to switch to. 55 | * Caps Lock and the 'Q' key will show a list of workspaces (and their windows). 56 | * Caps Lock and the '`' key will cycle through the screens. 57 | * Caps Lock and the F1 key, F2 key, etc. will move the 1st, 2nd, etc. workspace to the current screen. 58 | * Caps Lock and the 'S' key will select a window, or unselect a selected window. More than one window may be selected at a time. 59 | * Caps Lock and Shift and the 'S' key will select or unselect all windows in the current workspace. 60 | * Caps Lock and the 'W' key will migrate all selected windows to the current workspace and unselect them. 61 | 62 | Taowm also provides alternative ways to navigate within a program's window. 63 | * Caps Lock and the 'H', 'J', 'K' or 'L' keys are equivalent to pressing the Left, Down, Up or Right arrow keys. 64 | * Caps Lock and the 'Y', 'U', 'B' or 'N' keys are equivalent to Home, Page Up, End or Page Down. 65 | * The 'I' or 'M' keys are equivalent to a mouse wheel scrolling up or down, and the ',' or '.' keys are equivalent to the Backspace or Delete keys. 66 | 67 | Taowm provides similar shortcuts for other common actions. 68 | * Caps Lock and the 'O' or 'P' keys will copy or paste, 69 | * '/' or Shift-and-'?' will open or close a tab in the current window, 70 | * 'C' or 'V' will cycle through tabs, 71 | * 'Z' or 'X' will zoom in or out. 72 | * By default, these keys will only work with the google-chrome web browser and the gnome-terminal terminal emulator. Making these work with other programs will require some customization. 73 | 74 | # CUSTOMIZATION 75 | Customizing the keyboard shortcuts, web browser, terminal emulator, colors, etc., is done by editing `config.go` and re-compiling (and re-installing): run `go install github.com/nigeltao/taowm`. 76 | 77 | # DEVELOPMENT 78 | When working on taowm, it can be run in a nested X server such as Xephyr. From the `github.com/nigeltao/taowm` directory under `$GOPATH`: 79 | 80 | ``` 81 | Xephyr :9 2>/dev/null & 82 | DISPLAY=:9 go run *.go 83 | ``` 84 | 85 | # TROUBLESHOOTING 86 | If taowm isn't working, error messages should be logged to the `$HOME/.xsession-errors` or `$HOME/.xsession-errors.old` files. 87 | 88 | # DISCUSSION 89 | The taowm mailing list is at [groups.google.com/group/taowm](http://groups.google.com/group/taowm) 90 | 91 | # LEGAL 92 | Taowm is copyright 2013 The Taowm Authors. All rights reserved. Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. 93 | 94 | # SCREENSHOTS 95 | Taowm is deliberately plain. It starts with each screen divided into two side-by-side frames: 96 | 97 | ![screenshot](https://github.com/nigeltao/taowm/blob/master/doc/screenshot0.png) 98 | 99 | After opening a web browser window, splitting the right frame vertically, and opening a terminal emulator, it could look like this: 100 | 101 | ![screenshot](https://github.com/nigeltao/taowm/blob/master/doc/screenshot1.png) 102 | -------------------------------------------------------------------------------- /actions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | "time" 9 | 10 | xp "github.com/BurntSushi/xgb/xproto" 11 | ) 12 | 13 | func doExec(_ *workspace, cmd1 interface{}) bool { 14 | cmd, ok := cmd1.([]string) 15 | if !ok { 16 | return false 17 | } 18 | if len(cmd) == 0 { 19 | return false 20 | } 21 | go func() { 22 | c := exec.Command(cmd[0], cmd[1:]...) 23 | if err := c.Start(); err != nil { 24 | log.Printf("could not start command %q: %v", cmd, err) 25 | } 26 | // Ignore any error from the program itself. 27 | c.Wait() 28 | }() 29 | return false 30 | } 31 | 32 | func doAudio(k *workspace, cmd1 interface{}) bool { 33 | if !doAudioActions { 34 | return false 35 | } 36 | doExec(k, cmd1) 37 | return true 38 | } 39 | 40 | func doScreen(k *workspace, t1 interface{}) bool { 41 | t, ok := t1.(traversal) 42 | if !ok { 43 | return false 44 | } 45 | i := -1 46 | for j, s := range screens { 47 | if s.workspace == k { 48 | i = j 49 | break 50 | } 51 | } 52 | if i < 0 { 53 | return true 54 | } 55 | if t == next { 56 | i = (i + 1) % len(screens) 57 | } else { 58 | i = (i + len(screens) - 1) % len(screens) 59 | } 60 | warpPointerTo(screens[i].workspace.focusedFrame) 61 | return true 62 | } 63 | 64 | func doFrame(k *workspace, t1 interface{}) bool { 65 | t, ok := t1.(traversal) 66 | if !ok { 67 | return false 68 | } 69 | if k.fullscreen || k.listing != listNone { 70 | return false 71 | } 72 | k.focusedFrame = k.focusedFrame.traverse(t) 73 | warpPointerTo(k.focusedFrame) 74 | return true 75 | } 76 | 77 | func warpPointerTo(f *frame) { 78 | f.workspace.focusFrame(f) 79 | check(xp.WarpPointerChecked(xConn, xp.WindowNone, rootXWin, 0, 0, 0, 0, 80 | f.rect.X+int16(f.rect.Width/2), 81 | f.rect.Y+int16(f.rect.Height/2), 82 | )) 83 | makeLists() 84 | } 85 | 86 | func doWindow(k *workspace, t1 interface{}) bool { 87 | t, ok := t1.(traversal) 88 | if !ok { 89 | return false 90 | } 91 | f0 := k.focusedFrame 92 | dummy := &k.dummyWindow 93 | w0 := f0.window 94 | if w0 == nil { 95 | w0 = dummy 96 | } 97 | w1 := w0 98 | for { 99 | w1 = w1.link[t] 100 | if w1 == w0 { 101 | return true 102 | } 103 | if w1 == dummy { 104 | continue 105 | } 106 | if w1.frame == nil || k.fullscreen { 107 | break 108 | } 109 | } 110 | if w0 == dummy { 111 | w0 = nil 112 | } 113 | changeWindow(f0, w0, w1) 114 | return true 115 | } 116 | 117 | func doWindowN(k *workspace, n1 interface{}) bool { 118 | n, ok := n1.(int) 119 | if !ok { 120 | return false 121 | } 122 | f0, w0 := k.focusedFrame, k.focusedFrame.window 123 | w1 := k.dummyWindow.link[next] 124 | for ; n > 0 && w1 != &k.dummyWindow; n-- { 125 | w1 = w1.link[next] 126 | } 127 | if w1 == &k.dummyWindow || w1 == w0 { 128 | return true 129 | } 130 | changeWindow(f0, w0, w1) 131 | return true 132 | } 133 | 134 | func changeWindow(f0 *frame, w0, w1 *window) { 135 | if f0 == nil || w1 == nil { 136 | return 137 | } 138 | if w0 != w1 { 139 | if f1 := w1.frame; f1 != nil { 140 | if w0 != nil { 141 | f1.window, w0.frame = w0, f1 142 | } else { 143 | f1.window = nil 144 | } 145 | } else if w0 != nil { 146 | w0.frame = nil 147 | } 148 | f0.window, w1.frame = w1, f0 149 | } 150 | w1.configure() 151 | if w0 != nil { 152 | w0.configure() 153 | } 154 | focus(w1) 155 | makeLists() 156 | } 157 | 158 | func doWorkspace(k0 *workspace, t1 interface{}) bool { 159 | t, ok := t1.(traversal) 160 | if !ok { 161 | return false 162 | } 163 | k1 := k0 164 | for { 165 | k1 = k1.link[t] 166 | if k1 == k0 { 167 | return true 168 | } 169 | if k1 == &dummyWorkspace { 170 | continue 171 | } 172 | if k1.screen == nil { 173 | break 174 | } 175 | } 176 | changeWorkspace(k0.screen, k0, k1) 177 | return true 178 | } 179 | 180 | func doWorkspaceN(k0 *workspace, n1 interface{}) bool { 181 | n, ok := n1.(int) 182 | if !ok { 183 | return false 184 | } 185 | k1 := dummyWorkspace.link[next] 186 | for ; n > 0 && k1 != &dummyWorkspace; n-- { 187 | k1 = k1.link[next] 188 | } 189 | if k1 == &dummyWorkspace || k1 == k0 { 190 | return true 191 | } 192 | changeWorkspace(k0.screen, k0, k1) 193 | return true 194 | } 195 | 196 | func doWorkspaceNew(k0 *workspace, _ interface{}) bool { 197 | s := k0.screen 198 | changeWorkspace(s, k0, newWorkspace(s.rect, k0)) 199 | return true 200 | } 201 | 202 | func changeWorkspace(s0 *screen, k0, k1 *workspace) { 203 | if s0 == nil || k0 == nil || k1 == nil { 204 | return 205 | } 206 | k0.listing = listNone 207 | s1 := k1.screen 208 | if k0 != k1 { 209 | if s1 != nil { 210 | s1.workspace, k0.screen = k0, s1 211 | } else { 212 | k0.screen = nil 213 | } 214 | s0.workspace, k1.screen = k1, s0 215 | } 216 | k1.layout() 217 | k0.layout() 218 | p, err := xp.QueryPointer(xConn, rootXWin).Reply() 219 | if err != nil { 220 | log.Println(err) 221 | } 222 | if p == nil { 223 | k1.focusFrame(k1.mainFrame.firstDescendent()) 224 | } else { 225 | k1.focusFrame(k1.frameContaining(p.RootX, p.RootY)) 226 | } 227 | s0.repaint() 228 | if s1 != nil { 229 | s1.repaint() 230 | } 231 | makeLists() 232 | } 233 | 234 | func doWindowDelete(k *workspace, _ interface{}) bool { 235 | w := k.focusedFrame.window 236 | if w != nil && w.wmDeleteWindow { 237 | sendClientMessage(w.xWin, atomWMDeleteWindow) 238 | } 239 | return true 240 | } 241 | 242 | func doWorkspaceDelete(k0 *workspace, _ interface{}) bool { 243 | if k0.dummyWindow.link[next] != &k0.dummyWindow { 244 | // Workspace-delete fails if the workspace contains a window. 245 | return true 246 | } 247 | k1 := k0 248 | for { 249 | k1 = k1.link[next] 250 | if k1 == k0 { 251 | return true 252 | } 253 | if k1 == &dummyWorkspace { 254 | continue 255 | } 256 | if k1.screen == nil { 257 | break 258 | } 259 | } 260 | k0.link[prev].link[next] = k0.link[next] 261 | k0.link[next].link[prev] = k0.link[prev] 262 | s := k0.screen 263 | s.workspace, k1.screen = k1, s 264 | *k0 = workspace{} 265 | k1.layout() 266 | focus(k1.focusedFrame.window) 267 | s.repaint() 268 | makeLists() 269 | return true 270 | } 271 | 272 | func doList(k *workspace, l1 interface{}) bool { 273 | l, ok := l1.(listing) 274 | if !ok { 275 | return false 276 | } 277 | if k.listing != l { 278 | k.listing = l 279 | } else { 280 | k.listing = listNone 281 | } 282 | k.makeList() 283 | k.focusFrame(k.focusedFrame) 284 | return false 285 | } 286 | 287 | func doWindowNudge(k *workspace, t1 interface{}) bool { 288 | t, ok := t1.(traversal) 289 | if !ok { 290 | return false 291 | } 292 | if w := k.focusedFrame.window; w != nil { 293 | wn, wp := w.link[next], w.link[prev] 294 | wn.link[prev] = wp 295 | wp.link[next] = wn 296 | if t == next { 297 | w.link[next] = wn.link[next] 298 | w.link[prev] = wn 299 | } else { 300 | w.link[next] = wp 301 | w.link[prev] = wp.link[prev] 302 | } 303 | w.link[next].link[prev] = w 304 | w.link[prev].link[next] = w 305 | } 306 | makeLists() 307 | return true 308 | } 309 | 310 | func doWorkspaceNudge(k *workspace, t1 interface{}) bool { 311 | t, ok := t1.(traversal) 312 | if !ok { 313 | return false 314 | } 315 | kn, kp := k.link[next], k.link[prev] 316 | kn.link[prev] = kp 317 | kp.link[next] = kn 318 | if t == next { 319 | k.link[next] = kn.link[next] 320 | k.link[prev] = kn 321 | } else { 322 | k.link[next] = kp 323 | k.link[prev] = kp.link[prev] 324 | } 325 | k.link[next].link[prev] = k 326 | k.link[prev].link[next] = k 327 | makeLists() 328 | return true 329 | } 330 | 331 | func doWindowSelect(k *workspace, b1 interface{}) bool { 332 | b, ok := b1.(bool) 333 | if !ok { 334 | return false 335 | } 336 | if b { 337 | allSelected := true 338 | for w := k.dummyWindow.link[next]; w != &k.dummyWindow; w = w.link[next] { 339 | if !w.selected { 340 | allSelected = false 341 | break 342 | } 343 | } 344 | for w := k.dummyWindow.link[next]; w != &k.dummyWindow; w = w.link[next] { 345 | w.selected = !allSelected 346 | } 347 | } else { 348 | if w := k.focusedFrame.window; w != nil { 349 | w.selected = !w.selected 350 | } 351 | } 352 | makeLists() 353 | return true 354 | } 355 | 356 | func doWorkspaceMigrate(k *workspace, _ interface{}) bool { 357 | previous := k.dummyWindow.link[prev] 358 | if k.focusedFrame.window != nil { 359 | previous = k.focusedFrame.window 360 | } 361 | var migrants []*window 362 | for k0 := dummyWorkspace.link[next]; k0 != &dummyWorkspace; k0 = k0.link[next] { 363 | migrants = migrants[:0] 364 | for w := k0.dummyWindow.link[next]; w != &k0.dummyWindow; w = w.link[next] { 365 | if !w.selected { 366 | continue 367 | } 368 | w.selected = false 369 | if k0 == k { 370 | continue 371 | } 372 | migrants = append(migrants, w) 373 | } 374 | for _, w := range migrants { 375 | if f := w.frame; f != nil { 376 | f.window, w.frame = nil, nil 377 | } 378 | wn, wp := w.link[next], w.link[prev] 379 | wn.link[prev] = wp 380 | wp.link[next] = wn 381 | wn, wp = previous.link[next], previous 382 | wn.link[prev] = w 383 | wp.link[next] = w 384 | w.link[next], w.link[prev] = wn, wp 385 | previous = w 386 | 387 | f := k.focusedFrame 388 | if f.window != nil { 389 | f = k.mainFrame.firstEmptyFrame() 390 | } 391 | if f != nil { 392 | f.window, w.frame = w, f 393 | } 394 | w.configure() 395 | } 396 | if k0.fullscreen && k0.focusedFrame.window == nil { 397 | if k0.screen != nil { 398 | doFullscreen(k0, nil) 399 | } else { 400 | k0.fullscreen = false 401 | } 402 | } 403 | } 404 | makeLists() 405 | return true 406 | } 407 | 408 | func doFullscreen(k *workspace, _ interface{}) bool { 409 | if !k.fullscreen && k.focusedFrame.window == nil { 410 | return true 411 | } 412 | k.fullscreen = !k.fullscreen 413 | if p, err := xp.QueryPointer(xConn, rootXWin).Reply(); err != nil { 414 | log.Println(err) 415 | } else if p != nil { 416 | k.focusFrame(k.frameContaining(p.RootX, p.RootY)) 417 | } 418 | k.configure() 419 | if k.screen != nil { 420 | k.screen.repaint() 421 | } 422 | return true 423 | } 424 | 425 | func doHide(k *workspace, _ interface{}) bool { 426 | f := k.focusedFrame 427 | w := f.window 428 | if w != nil { 429 | f.window, w.frame = nil, nil 430 | w.configure() 431 | } 432 | if k.fullscreen { 433 | doFullscreen(k, nil) 434 | } 435 | makeLists() 436 | return true 437 | } 438 | 439 | func doMerge(k *workspace, _ interface{}) bool { 440 | if k.fullscreen || k.listing == listWorkspaces { 441 | return false 442 | } 443 | f := k.focusedFrame 444 | if f.parent == nil { 445 | // Merge fails if the frame is the main frame. 446 | return true 447 | } 448 | w := f.window 449 | if w != nil { 450 | f.window, w.frame = nil, nil 451 | w.configure() 452 | } 453 | 454 | if f.prevSibling != nil { 455 | f.prevSibling.nextSibling = f.nextSibling 456 | k.focusedFrame = f.prevSibling.lastDescendent() 457 | } 458 | if f.nextSibling != nil { 459 | f.nextSibling.prevSibling = f.prevSibling 460 | k.focusedFrame = f.nextSibling.firstDescendent() 461 | } 462 | parent := f.parent 463 | if parent.firstChild == f { 464 | parent.firstChild = f.nextSibling 465 | } 466 | if parent.lastChild == f { 467 | parent.lastChild = f.prevSibling 468 | } 469 | 470 | if f.parent.numChildren() == 1 { 471 | // Hoist the sibling frame into the parent frame. 472 | sibling := parent.firstChild 473 | parent.firstChild = sibling.firstChild 474 | parent.lastChild = sibling.lastChild 475 | for c := parent.firstChild; c != nil; c = c.nextSibling { 476 | c.parent = parent 477 | } 478 | parent.orientation = sibling.orientation 479 | if w := sibling.window; w != nil { 480 | parent.window, w.frame = w, parent 481 | } 482 | if k.focusedFrame == sibling { 483 | k.focusedFrame = parent.firstDescendent() 484 | } 485 | *f, *sibling = frame{}, frame{} 486 | } 487 | 488 | parent.layout() 489 | finishMergeSplit(k) 490 | return true 491 | } 492 | 493 | func doSplit(k *workspace, o1 interface{}) bool { 494 | o, ok := o1.(orientation) 495 | if !ok { 496 | return false 497 | } 498 | if k.fullscreen || k.listing == listWorkspaces { 499 | return false 500 | } 501 | k.focusedFrame.split(o) 502 | finishMergeSplit(k) 503 | return true 504 | } 505 | 506 | func finishMergeSplit(k *workspace) { 507 | p, err := xp.QueryPointer(xConn, rootXWin).Reply() 508 | if err != nil { 509 | log.Println(err) 510 | } 511 | if p == nil { 512 | k.focusFrame(k.mainFrame.firstDescendent()) 513 | } else { 514 | k.focusFrame(k.frameContaining(p.RootX, p.RootY)) 515 | } 516 | k.screen.repaint() 517 | makeLists() 518 | } 519 | 520 | func doProgramAction(k *workspace, pa1 interface{}) bool { 521 | pa, ok := pa1.(programAction) 522 | if !ok { 523 | return false 524 | } 525 | w := k.focusedFrame.window 526 | if w == nil { 527 | return false 528 | } 529 | class := w.property(atomWMClass) 530 | if i := strings.Index(class, "\x00"); i >= 0 { 531 | class = class[:i] 532 | } 533 | a := programActions[class][pa] 534 | if a.keysym == 0 { 535 | return false 536 | } 537 | sendSynthetic(w, a.state, a.keysym) 538 | return true 539 | } 540 | 541 | func doSynthetic(k *workspace, buttonOrKeysym interface{}) bool { 542 | if w := k.focusedFrame.window; w != nil { 543 | sendSynthetic(w, keyState, buttonOrKeysym) 544 | } 545 | return false 546 | } 547 | 548 | func sendSynthetic(w *window, state uint16, buttonOrKeysym interface{}) { 549 | // {Button,Key}{Press,Release}Event types all have the same wire format, 550 | // except for the first byte (the X11 message type). 551 | e := xp.KeyPressEvent{ 552 | Time: eventTime, 553 | Root: rootXWin, 554 | Event: w.xWin, 555 | Child: w.xWin, 556 | RootX: keyRootX, 557 | RootY: keyRootY, 558 | EventX: keyRootX - w.rect.X, 559 | EventY: keyRootY - w.rect.Y, 560 | State: state, 561 | SameScreen: true, 562 | } 563 | var msg0, msg1 byte 564 | switch bk := buttonOrKeysym.(type) { 565 | case xp.Button: 566 | msg0 = xp.ButtonPress 567 | msg1 = xp.ButtonRelease 568 | e.Detail = xp.Keycode(bk) 569 | case xp.Keysym: 570 | msg0 = xp.KeyPress 571 | msg1 = xp.KeyRelease 572 | keycode, shift := findKeycode(bk) 573 | if keycode == 0 { 574 | return 575 | } 576 | e.Detail = keycode 577 | if shift { 578 | e.State |= xp.KeyButMaskShift 579 | } 580 | default: 581 | return 582 | } 583 | 584 | b := e.Bytes() 585 | b[0] = msg0 586 | check(xp.SendEventChecked(xConn, false, w.xWin, xp.EventMaskNoEvent, string(b))) 587 | b[0] = msg1 588 | check(xp.SendEventChecked(xConn, false, w.xWin, xp.EventMaskNoEvent, string(b))) 589 | } 590 | 591 | var ( 592 | quitTimes [2]time.Time 593 | quitIndex int 594 | quitting bool 595 | ) 596 | 597 | func doQuit(_ *workspace, _ interface{}) bool { 598 | if quitting { 599 | return false 600 | } 601 | now := time.Now() 602 | since := now.Sub(quitTimes[quitIndex]) 603 | quitTimes[quitIndex] = now 604 | quitIndex = (quitIndex + 1) % len(quitTimes) 605 | if since > 5*time.Second { 606 | return true 607 | } 608 | quitting = true 609 | 610 | waiting := false 611 | for k := dummyWorkspace.link[next]; k != &dummyWorkspace; k = k.link[next] { 612 | for w := k.dummyWindow.link[next]; w != &k.dummyWindow; w = w.link[next] { 613 | if w.wmDeleteWindow { 614 | waiting = true 615 | sendClientMessage(w.xWin, atomWMDeleteWindow) 616 | } 617 | } 618 | } 619 | if waiting { 620 | go func() { 621 | time.Sleep(quitDuration) 622 | os.Exit(0) 623 | }() 624 | } else { 625 | os.Exit(0) 626 | } 627 | return true 628 | } 629 | 630 | var previousFocusXWin xp.Window 631 | 632 | func focus(w *window) { 633 | xWin := desktopXWin 634 | if w != nil { 635 | xWin = w.xWin 636 | } 637 | if previousFocusXWin == xWin { 638 | return 639 | } 640 | previousFocusXWin = xWin 641 | 642 | active := []byte{ 643 | byte(xWin >> 0), 644 | byte(xWin >> 8), 645 | byte(xWin >> 16), 646 | byte(xWin >> 24), 647 | } 648 | if err := xp.ChangePropertyChecked(xConn, xp.PropModeReplace, rootXWin, 649 | atomNetActiveWindow, atomWindow, 32, 1, active).Check(); err != nil { 650 | log.Printf("could not set _NET_ACTIVE_WINDOW: %v", err) 651 | } 652 | 653 | if w != nil && w.wmTakeFocus { 654 | sendClientMessage(xWin, atomWMTakeFocus) 655 | } else { 656 | check(xp.SetInputFocusChecked(xConn, xp.InputFocusParent, xWin, eventTime)) 657 | } 658 | } 659 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | xp "github.com/BurntSushi/xgb/xproto" 7 | ) 8 | 9 | const ( 10 | // wmKeysym is the key to trigger taowm actions. For other possible 11 | // values, such as xkSuperL for the 'Windows' key that is typically 12 | // between the left Control and Alt keys, see keysym.go. 13 | wmKeysym = xkCapsLock 14 | 15 | // colorXxx are taowm's text and border colors. We assume 24-bit RGB. 16 | colorBaseUnfocused = 0x1f3f1f 17 | colorBaseFocused = 0x3f7f3f 18 | colorPulseUnfocused = 0x3f7f3f 19 | colorPulseFocused = 0x7fff7f 20 | colorQuitUnfocused = 0x7f1f1f 21 | colorQuitFocused = 0xff3f3f 22 | 23 | // dpi is the Dots Per Inch screen resolution. Hard-coding 96 DPI is the 24 | // same as what 2012-era gnome-settings-daemon does. For significantly 25 | // higher resolution screens, this value should be larger. Integer 26 | // multiples of 96 work best. 27 | dpi = 96 28 | 29 | // pulseXxx are the animation durations. 30 | pulseFrameDuration = 50 * time.Millisecond 31 | pulseTotalDuration = 1000 * time.Millisecond 32 | 33 | // quitDuration is the grace period, when quitting, for programs to exit 34 | // cleanly. 35 | quitDuration = 60 * time.Second 36 | 37 | showBatteryPercentage = false 38 | ) 39 | 40 | var ( 41 | // fontXxx are the font name and metrics, for the 6x13 font by default. 42 | // fontHeight1 is the vertical offset for the first line of text. 43 | fontName = "6x13" 44 | fontHeight = 16 45 | fontHeight1 = 9 46 | fontWidth = 6 47 | ) 48 | 49 | func init() { 50 | if dpi >= 1.5*96 { 51 | fontName = "12x24" 52 | fontHeight = 30 53 | fontHeight1 = 20 54 | fontWidth = 12 55 | } 56 | } 57 | 58 | // xSettings is the key/value pairs to announce via the XSETTINGS mechanism. 59 | // In particular, these include font and theme configuration parameters picked 60 | // up by GTK+ programs such as gnome-terminal. 61 | // 62 | // The dump_xsettings program from http://code.google.com/p/xsettingsd/ will 63 | // show the XSETTINGS key/value pairs set by other desktop environments such 64 | // as GNOME. 65 | // 66 | // If this array is empty, then taowm will not try to own the XSETTINGS list, 67 | // allowing another program such as gnome-settings-daemon to do so. 68 | var xSettings = [...]struct { 69 | name string 70 | value interface{} 71 | }{ 72 | {"Net/IconThemeName", "Tango"}, 73 | {"Net/ThemeName", "Clearlooks"}, 74 | {"Xft/Antialias", 1}, 75 | {"Xft/DPI", dpi * 1024}, 76 | {"Xft/Hinting", 1}, 77 | {"Xft/HintStyle", "hintslight"}, 78 | {"Xft/RGBA", "none"}, 79 | } 80 | 81 | const doAudioActions = true 82 | 83 | // actions lists the action to be performed for each key press. The do function 84 | // returns whether to pulsate the frames' borders to acknowledge the key press. 85 | // 86 | // The map keys are X11 keysyms as int32s. The unary +/^ means whether the 87 | // shift modifier needs to be absent/present. 88 | var actions = map[int32]struct { 89 | do func(*workspace, interface{}) bool 90 | arg interface{} 91 | }{ 92 | +' ': {doExec, []string{"google-chrome"}}, 93 | ^' ': {doExec, []string{"google-chrome", "--incognito"}}, 94 | ^'|': {doExec, []string{"gnome-screensaver-command", "-l"}}, 95 | +xkReturn: {doExec, []string{"gnome-terminal"}}, // "taote" is another option. 96 | ^xkReturn: {doExec, []string{"dmenu_run", "-nb", "#0f0f0f", "-nf", "#3f7f3f", 97 | "-sb", "#0f0f0f", "-sf", "#7fff7f", "-l", "10"}}, 98 | 99 | +xkAudioLowerVolume: {doAudio, []string{"pactl", "set-sink-volume", "@DEFAULT_SINK@", "-5%"}}, 100 | +xkAudioRaiseVolume: {doAudio, []string{"pactl", "set-sink-volume", "@DEFAULT_SINK@", "+5%"}}, 101 | +xkAudioMute: {doAudio, []string{"pactl", "set-sink-mute", "@DEFAULT_SINK@", "toggle"}}, 102 | 103 | +xkBackspace: {doWindowDelete, nil}, 104 | ^xkEscape: {doQuit, nil}, 105 | 106 | +'`': {doScreen, next}, 107 | ^'~': {doScreen, prev}, 108 | +xkTab: {doFrame, next}, 109 | ^xkISOLeftTab: {doFrame, prev}, 110 | 111 | +'q': {doList, listWorkspaces}, 112 | +'w': {doWorkspaceMigrate, nil}, 113 | +'e': {doWorkspace, prev}, 114 | ^'E': {doWorkspaceNudge, prev}, 115 | +'r': {doWorkspace, next}, 116 | ^'R': {doWorkspaceNudge, next}, 117 | +'t': {doWorkspaceNew, nil}, 118 | ^'T': {doWorkspaceDelete, nil}, 119 | 120 | +'a': {doList, listWindows}, 121 | +'s': {doWindowSelect, false}, 122 | ^'S': {doWindowSelect, true}, 123 | +'d': {doWindow, prev}, 124 | ^'D': {doWindowNudge, prev}, 125 | +'f': {doWindow, next}, 126 | ^'F': {doWindowNudge, next}, 127 | +'g': {doFullscreen, nil}, 128 | ^'G': {doHide, nil}, 129 | 130 | +'-': {doSplit, horizontal}, 131 | +'=': {doSplit, vertical}, 132 | ^'+': {doMerge, nil}, 133 | 134 | +'1': {doWindowN, 0}, 135 | +'2': {doWindowN, 1}, 136 | +'3': {doWindowN, 2}, 137 | +'4': {doWindowN, 3}, 138 | +'5': {doWindowN, 4}, 139 | +'6': {doWindowN, 5}, 140 | +'7': {doWindowN, 6}, 141 | +'8': {doWindowN, 7}, 142 | +'9': {doWindowN, 8}, 143 | +'0': {doWindowN, 9}, 144 | 145 | +xkF1: {doWorkspaceN, 0}, 146 | +xkF2: {doWorkspaceN, 1}, 147 | +xkF3: {doWorkspaceN, 2}, 148 | +xkF4: {doWorkspaceN, 3}, 149 | +xkF5: {doWorkspaceN, 4}, 150 | +xkF6: {doWorkspaceN, 5}, 151 | +xkF7: {doWorkspaceN, 6}, 152 | +xkF8: {doWorkspaceN, 7}, 153 | +xkF9: {doWorkspaceN, 8}, 154 | +xkF10: {doWorkspaceN, 9}, 155 | +xkF11: {doWorkspaceN, 10}, 156 | +xkF12: {doWorkspaceN, 11}, 157 | 158 | +'i': {doSynthetic, xp.Button(4)}, 159 | ^'I': {doSynthetic, xp.Button(4)}, 160 | +'m': {doSynthetic, xp.Button(5)}, 161 | ^'M': {doSynthetic, xp.Button(5)}, 162 | +'y': {doSynthetic, xp.Keysym(xkHome)}, 163 | ^'Y': {doSynthetic, xp.Keysym(xkHome)}, 164 | +'u': {doSynthetic, xp.Keysym(xkPageUp)}, 165 | ^'U': {doSynthetic, xp.Keysym(xkPageUp)}, 166 | +'h': {doSynthetic, xp.Keysym(xkLeft)}, 167 | ^'H': {doSynthetic, xp.Keysym(xkLeft)}, 168 | +'j': {doSynthetic, xp.Keysym(xkDown)}, 169 | ^'J': {doSynthetic, xp.Keysym(xkDown)}, 170 | +'k': {doSynthetic, xp.Keysym(xkUp)}, 171 | ^'K': {doSynthetic, xp.Keysym(xkUp)}, 172 | +'l': {doSynthetic, xp.Keysym(xkRight)}, 173 | ^'L': {doSynthetic, xp.Keysym(xkRight)}, 174 | +'b': {doSynthetic, xp.Keysym(xkEnd)}, 175 | ^'B': {doSynthetic, xp.Keysym(xkEnd)}, 176 | +'n': {doSynthetic, xp.Keysym(xkPageDown)}, 177 | ^'N': {doSynthetic, xp.Keysym(xkPageDown)}, 178 | +',': {doSynthetic, xp.Keysym(xkBackspace)}, 179 | ^'<': {doSynthetic, xp.Keysym(xkBackspace)}, 180 | +'.': {doSynthetic, xp.Keysym(xkDelete)}, 181 | ^'>': {doSynthetic, xp.Keysym(xkDelete)}, 182 | 183 | +'/': {doProgramAction, paTabNew}, 184 | ^'?': {doProgramAction, paTabClose}, 185 | +'c': {doProgramAction, paTabPrev}, 186 | ^'C': {doProgramAction, paTabNudgePrev}, 187 | +'v': {doProgramAction, paTabNext}, 188 | ^'V': {doProgramAction, paTabNudgeNext}, 189 | +'\'': {doProgramAction, paSearch}, 190 | +'o': {doProgramAction, paCopy}, 191 | ^'O': {doProgramAction, paCut}, 192 | +'p': {doProgramAction, paPaste}, 193 | ^'P': {doProgramAction, paPasteSpecial}, 194 | +'z': {doProgramAction, paZoomIn}, 195 | ^'Z': {doProgramAction, paZoomReset}, 196 | +'x': {doProgramAction, paZoomOut}, 197 | +';': {doProgramAction, paThemeNext}, 198 | ^':': {doProgramAction, paThemePrev}, 199 | } 200 | 201 | // programAction is an action for a particular program to invoke, as opposed 202 | // to a window management action or generic left/down/up/right synthetic key. 203 | type programAction int 204 | 205 | const ( 206 | paTabNew programAction = iota 207 | paTabClose 208 | paTabPrev 209 | paTabNext 210 | paTabNudgePrev 211 | paTabNudgeNext 212 | paSearch 213 | paCut 214 | paCopy 215 | paPaste 216 | paPasteSpecial 217 | paZoomIn 218 | paZoomOut 219 | paZoomReset 220 | paThemePrev 221 | paThemeNext 222 | nProgramActions 223 | ) 224 | 225 | // programActions defines the program-specific synthetic key combination to 226 | // send to perform a generic program action. For example, the 'copy' action is 227 | // Control-C for some programs and Control-Shift-C for others. 228 | // 229 | // The map keys are based on a window's WM_CLASS. To configure a program that 230 | // isn't listed here, run "xprop | grep WM_CLASS", click on a window from that 231 | // program, and use the first quoted value as the map key here. 232 | var programActions = map[string][nProgramActions]struct { 233 | state uint16 234 | keysym xp.Keysym 235 | }{ 236 | "google-chrome": { 237 | paTabNew: {xp.ModMaskControl, 't'}, 238 | paTabClose: {xp.ModMaskControl, 'w'}, 239 | paTabPrev: {xp.ModMaskControl, xkPageUp}, 240 | paTabNext: {xp.ModMaskControl, xkPageDown}, 241 | paSearch: {xp.ModMaskControl, 'f'}, 242 | paCut: {xp.ModMaskControl, 'x'}, 243 | paCopy: {xp.ModMaskControl, 'c'}, 244 | paPaste: {xp.ModMaskControl, 'v'}, 245 | paPasteSpecial: {xp.ModMaskControl | xp.ModMaskShift, 'V'}, 246 | paZoomIn: {xp.ModMaskControl | xp.ModMaskShift, '+'}, 247 | paZoomOut: {xp.ModMaskControl, '-'}, 248 | paZoomReset: {xp.ModMaskControl, '0'}, 249 | }, 250 | 251 | "gnome-terminal-server": { 252 | paTabNew: {xp.ModMaskControl | xp.ModMaskShift, 'T'}, 253 | paTabClose: {xp.ModMaskControl | xp.ModMaskShift, 'W'}, 254 | paTabPrev: {xp.ModMaskControl, xkPageUp}, 255 | paTabNext: {xp.ModMaskControl, xkPageDown}, 256 | paSearch: {xp.ModMaskControl | xp.ModMaskShift, 'F'}, 257 | paCut: {xp.ModMaskControl | xp.ModMaskShift, 'C'}, 258 | paCopy: {xp.ModMaskControl | xp.ModMaskShift, 'C'}, 259 | paPaste: {xp.ModMaskControl | xp.ModMaskShift, 'V'}, 260 | paPasteSpecial: {xp.ModMaskControl | xp.ModMaskShift, 'V'}, 261 | paZoomIn: {xp.ModMaskControl | xp.ModMaskShift, '+'}, 262 | paZoomOut: {xp.ModMaskControl, '-'}, 263 | paZoomReset: {xp.ModMaskControl, '0'}, 264 | }, 265 | 266 | // taote is https://github.com/nigeltao/taote 267 | "taote": { 268 | paTabNew: {xp.ModMaskControl | xp.ModMaskShift, 'T'}, 269 | paTabClose: {xp.ModMaskControl | xp.ModMaskShift, 'Y'}, 270 | paTabPrev: {xp.ModMaskControl | xp.ModMaskShift, xkPageUp}, 271 | paTabNext: {xp.ModMaskControl | xp.ModMaskShift, xkPageDown}, 272 | paTabNudgePrev: {xp.ModMaskControl | xp.ModMaskShift, xkHome}, 273 | paTabNudgeNext: {xp.ModMaskControl | xp.ModMaskShift, xkEnd}, 274 | paSearch: {xp.ModMaskControl | xp.ModMaskShift, 'F'}, 275 | paCut: {xp.ModMaskControl | xp.ModMaskShift, 'C'}, 276 | paCopy: {xp.ModMaskControl | xp.ModMaskShift, 'C'}, 277 | paPaste: {xp.ModMaskControl | xp.ModMaskShift, 'V'}, 278 | paPasteSpecial: {xp.ModMaskControl | xp.ModMaskShift, 'V'}, 279 | paZoomIn: {xp.ModMaskControl | xp.ModMaskShift, '+'}, 280 | paZoomOut: {xp.ModMaskControl | xp.ModMaskShift, '_'}, 281 | paZoomReset: {xp.ModMaskControl | xp.ModMaskShift, ')'}, 282 | paThemePrev: {xp.ModMaskControl | xp.ModMaskShift, '<'}, 283 | paThemeNext: {xp.ModMaskControl | xp.ModMaskShift, '>'}, 284 | }, 285 | } 286 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Taowm is The Acutely Opinionated Window Manager. It is a minimalist, keyboard 3 | driven, low distraction, tiling window manager for someone who uses a computer 4 | primarily to run just two GUI programs: a web browser and a terminal emulator. 5 | 6 | 7 | INSTALLATION 8 | 9 | To install taowm: 10 | 1. Install Go (as per http://golang.org/doc/install or get it from 11 | your distribution). 12 | 2. Run "go get github.com/nigeltao/taowm". 13 | 14 | This will install taowm in your $GOPATH, or under $GOROOT/bin if $GOPATH is 15 | empty. Run "go help gopath" to read more about $GOPATH. 16 | 17 | Taowm is designed to run from an Xsession session. Add this line to the end of 18 | your ~/.xsession file: 19 | /path/to/your/taowm 20 | where the path is wherever "go get" or "go install" wrote to. Again, run 21 | "go help gopath" for more information. 22 | 23 | Log out and log back in with the "Xsession" option. Some systems, such as 24 | Ubuntu 12.04 "Precise", do not offer an Xsession option by default. To enable 25 | it, create a new file /usr/share/xsessions/custom.desktop that contains: 26 | [Desktop Entry] 27 | Name=Xsession 28 | Exec=/etc/X11/Xsession 29 | 30 | 31 | USAGE 32 | 33 | Taowm starts with each screen divided into two side-by-side frames, outlined in 34 | green. Frames can frame windows, but they can also be empty: closing a frame's 35 | window will not collapse that frame. The frame that contains the mouse pointer 36 | is the focused frame, and its border is brighter than other frames. Its window 37 | (if it contains one) will have the keyboard focus. 38 | 39 | Taowm is primarily keyboard driven, and all keyboard shortcuts involve first 40 | holding down the Caps Lock key, similar to how holding down the Control key 41 | followed by the 'N' key, in your web browser, creates a new browser window. The 42 | default Caps Lock behavior, CHANGING ALL TYPED LETTERS TO UPPER CASE, is 43 | disabled. 44 | 45 | Caps Lock and the Space key will open a new web browser window. Caps Lock and 46 | the Enter key will open a new terminal emulator window. Caps Lock and the Shift 47 | key and the '|' pipe key will lock the screen. Caps Lock and the Backspace key 48 | will close the window in the focused frame. Caps Lock and the Tab key will 49 | cycle through the frames. 50 | 51 | To quit taowm and return to the log in screen, hold down Caps Lock and the 52 | Shift key and hit the Escape key three times in quick succession. Normally, 53 | this will quit immediately. Some programs may ask for something before closing, 54 | such as a file name to write unsaved data to. In this case, taowm will quit in 55 | 60 seconds or whenever all such programs have closed, instead of quitting 56 | immediately, and the frame borders will turn red. 57 | 58 | If there are more windows than frames, then Caps Lock and the 'D' or 'F' key 59 | will cycle through hidden windows. Caps Lock and a number key like '1', '2', 60 | etc. will move the 1st, 2nd, etc. window to the focused frame. Caps Lock and 61 | the 'A' key will show a list of windows: the one currently in the focused 62 | frame is marked with a '+', other windows in other frames are marked with a 63 | '-', hidden windows that have not been seen yet are marked with an '@', and 64 | hidden windows that have been seen before are unmarked. In particular, newly 65 | created windows will not automatically be shown. Taowm prevents new windows 66 | from popping up and 'stealing' keyboard focus, a problem if the password you 67 | are typing into your terminal emulator accidentally gets written to a chat 68 | window that popped up at the wrong time. Instead, if there isn't an empty frame 69 | to accept a new window, taowm keeps that window hidden (and marked with an '@' 70 | in the window list) until you are ready to deal with it. If there are any such 71 | windows that have not been seen yet, the green frame borders will pulsate to 72 | remind you. Selected windows are also marked with a '#'; selection is described 73 | below. 74 | 75 | Caps Lock and the 'G' key will toggle the focused frame in occupying the entire 76 | screen. Caps Lock and Shift and the 'G' key will hide the window in the focused 77 | frame. Caps Lock and the '-' key, the '=' key or Shift and the '+' key will 78 | split the current frame horizontally, vertically, or merge a frame to undo a 79 | frame split respectively. 80 | 81 | A screen contains workspaces like a frame contains windows. Caps Lock and the 82 | 'T' key will create a new workspace, hiding the current one. Caps Lock and the 83 | 'E' or 'R' key will cycle through hidden workspaces. Caps Lock and Shift and 84 | the 'T' key will delete the current workspace, provided that it holds no 85 | windows and there is another hidden workspace to switch to. Caps Lock and the 86 | 'Q' key will show a list of workspaces (and their windows). Caps Lock and the 87 | '`' key will cycle through the screens. Caps Lock and the F1 key, F2 key, etc. 88 | will move the 1st, 2nd, etc. workspace to the current screen. Caps Lock and the 89 | 'S' key will select a window, or unselect a selected window. More than one 90 | window may be selected at a time. Caps Lock and Shift and the 'S' key will 91 | select or unselect all windows in the current workspace. Caps Lock and the 'W' 92 | key will migrate all selected windows to the current workspace and unselect 93 | them. 94 | 95 | Taowm also provides alternative ways to navigate within a program's window. 96 | Caps Lock and the 'H', 'J', 'K' or 'L' keys are equivalent to pressing the 97 | Left, Down, Up or Right arrow keys. Similarly, Caps Lock and the 'Y', 'U', 98 | 'B' or 'N' keys are equivalent to Home, Page Up, End or Page Down. The 'I' or 99 | 'M' keys are equivalent to a mouse wheel scrolling up or down, and the ',' or 100 | '.' keys are equivalent to the Backspace or Delete keys. 101 | 102 | Taowm provides similar shortcuts for other common actions. Caps Lock and the 103 | 'O' or 'P' keys will copy or paste, '/' or Shift-and-'?' will open or close a 104 | tab in the current window, 'C' or 'V' will cycle through tabs, 'Z' or 'X' will 105 | zoom in or out. By default, these keys will only work with the google-chrome 106 | web browser and the gnome-terminal terminal emulator. Making these work with 107 | other programs will require some customization. 108 | 109 | 110 | CUSTOMIZATION 111 | 112 | Customizing the keyboard shortcuts, web browser, terminal emulator, colors, 113 | etc., is done by editing config.go and re-compiling (and re-installing): run 114 | "go install github.com/nigeltao/taowm". 115 | 116 | 117 | DEVELOPMENT 118 | 119 | When working on taowm, it can be run in a nested X server such as Xephyr. From 120 | the github.com/nigeltao/taowm directory under $GOPATH: 121 | Xephyr :9 2>/dev/null & 122 | DISPLAY=:9 go run *.go 123 | 124 | 125 | DISCUSSION 126 | 127 | The taowm mailing list is at http://groups.google.com/group/taowm 128 | 129 | 130 | LEGAL 131 | 132 | Taowm is copyright 2013 The Taowm Authors. All rights reserved. Use of this 133 | source code is governed by a BSD-style license that can be found in the 134 | LICENSE file. 135 | */ 136 | package main 137 | -------------------------------------------------------------------------------- /doc/screenshot0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nigeltao/taowm/4c67979f29667925d8bcb11959236d5436f1d925/doc/screenshot0.png -------------------------------------------------------------------------------- /doc/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nigeltao/taowm/4c67979f29667925d8bcb11959236d5436f1d925/doc/screenshot1.png -------------------------------------------------------------------------------- /draw.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | "sync" 8 | "time" 9 | 10 | xp "github.com/BurntSushi/xgb/xproto" 11 | ) 12 | 13 | var desktopColor uint32 14 | 15 | func setForeground(c uint32) { 16 | if desktopColor == c { 17 | return 18 | } 19 | desktopColor = c 20 | check(xp.ChangeGCChecked(xConn, desktopXGC, xp.GcForeground, []uint32{c})) 21 | } 22 | 23 | func drawText(x, y int16, text string) { 24 | check(xp.ImageText8Checked(xConn, uint8(len(text)), 25 | xp.Drawable(desktopXWin), desktopXGC, x, y, text)) 26 | } 27 | 28 | func clip(k *workspace) (int16, int16) { 29 | r := k.focusedFrame.rect 30 | if k.fullscreen || k.listing == listWorkspaces { 31 | r = k.mainFrame.rect 32 | } 33 | r.X, r.Y, r.Width, r.Height = r.X+2, r.Y+2, r.Width-3, r.Height-3 34 | check(xp.SetClipRectanglesChecked( 35 | xConn, xp.ClipOrderingUnsorted, desktopXGC, 0, 0, []xp.Rectangle{r})) 36 | return r.X, r.Y 37 | } 38 | 39 | func unclip() { 40 | r := xp.Rectangle{X: 0, Y: 0, Width: desktopWidth, Height: desktopHeight} 41 | check(xp.SetClipRectanglesChecked( 42 | xConn, xp.ClipOrderingUnsorted, desktopXGC, 0, 0, []xp.Rectangle{r})) 43 | } 44 | 45 | func handleExpose(e xp.ExposeEvent) { 46 | if e.Count != 0 { 47 | return 48 | } 49 | for _, s := range screens { 50 | k := s.workspace 51 | k.drawFrameBorders() 52 | if k.listing == listNone { 53 | continue 54 | } 55 | x, y := clip(k) 56 | y += int16(fontHeight1) 57 | setForeground(colorPulseUnfocused) 58 | info := time.Now().Format("2006-01-02 15:04 Monday") 59 | if showBatteryPercentage { 60 | info = fmt.Sprintf("Bat: %4s %s", batteryPercentage(), info) 61 | } 62 | drawText(x, y, info) 63 | y += int16(fontHeight) 64 | 65 | if k.listing == listWindows { 66 | setForeground(colorPulseFocused) 67 | } 68 | wNum := 0 69 | for i, item := range k.list { 70 | if iw, ok := item.(*window); ok { 71 | c0, c1 := ' ', ' ' 72 | if k.listing == listWindows { 73 | if iw.frame == k.focusedFrame { 74 | c0 = '+' 75 | } else if iw.frame != nil { 76 | c0 = '-' 77 | } else if !iw.seen { 78 | c0 = '@' 79 | } 80 | } 81 | if iw.selected { 82 | c1 = '#' 83 | } 84 | drawText(x+int16(3*fontWidth), y+int16(i*fontHeight), 85 | fmt.Sprintf("%c%c %c %s", c0, c1, windowNames[wNum], iw.name)) 86 | if wNum < len(windowNames)-1 { 87 | wNum++ 88 | } 89 | } else { 90 | wNum = 0 91 | } 92 | } 93 | if k.listing == listWorkspaces { 94 | setForeground(colorPulseFocused) 95 | kNum := 0 96 | for i, item := range k.list { 97 | if ik, ok := item.(*workspace); ok { 98 | c := ' ' 99 | if ik.screen == s { 100 | c = '+' 101 | } else if ik.screen != nil { 102 | c = '-' 103 | } 104 | drawText(x+int16(3*fontWidth), y+int16(i*fontHeight), 105 | fmt.Sprintf("%c %s", c, workspaceNames[kNum])) 106 | if kNum < len(workspaceNames)-1 { 107 | kNum++ 108 | } 109 | } 110 | } 111 | } 112 | if k.index >= 0 { 113 | drawText(x+int16(fontWidth), y+int16(k.index*fontHeight), ">") 114 | } 115 | unclip() 116 | } 117 | } 118 | 119 | var percentage = []byte("percentage:") 120 | 121 | func batteryPercentage() string { 122 | b, err := exec.Command("/usr/bin/upower", "--show-info", "/org/freedesktop/UPower/devices/battery_BAT0").Output() 123 | if err != nil { 124 | return "???" 125 | } 126 | if i := bytes.Index(b, percentage); i >= 0 { 127 | b = b[i+len(percentage):] 128 | } else { 129 | return "???" 130 | } 131 | if i := bytes.IndexByte(b, '%'); i >= 0 { 132 | b = b[:i+1] 133 | } else { 134 | return "???" 135 | } 136 | return string(bytes.TrimSpace(b)) 137 | } 138 | 139 | var ( 140 | pulseTimeLock sync.Mutex 141 | pulseTime time.Time 142 | ) 143 | 144 | var ( 145 | pulseChan = make(chan time.Time) 146 | pulseDoneChan = make(chan struct{}) 147 | 148 | colorUnfocused uint32 = colorBaseUnfocused 149 | colorFocused uint32 = colorBaseFocused 150 | ) 151 | 152 | func init() { 153 | go runPulses() 154 | } 155 | 156 | func runPulses() { 157 | tChan := (<-chan time.Time)(nil) 158 | fChan := (chan func())(nil) 159 | for { 160 | select { 161 | case when := <-pulseChan: 162 | pulseTimeLock.Lock() 163 | pulseTime = when 164 | pulseTimeLock.Unlock() 165 | if tChan == nil { 166 | fChan = proactiveChan 167 | } 168 | case <-pulseDoneChan: 169 | if tChan == nil { 170 | tChan = time.After(pulseFrameDuration) 171 | } 172 | case <-tChan: 173 | tChan = nil 174 | fChan = proactiveChan 175 | case fChan <- pulse: 176 | fChan = nil 177 | } 178 | } 179 | } 180 | 181 | func pulse() { 182 | pulseTimeLock.Lock() 183 | t := pulseTime 184 | pulseTimeLock.Unlock() 185 | i := int(time.Since(t) * time.Duration(len(cos)) / pulseTotalDuration) 186 | if i < 0 { 187 | i = 0 188 | } 189 | anyUnseenWindows := findWindow(func(w *window) bool { return !w.seen }) != nil 190 | if !anyUnseenWindows && i > len(cos)/2 { 191 | i = len(cos) / 2 192 | } 193 | if quitting { 194 | colorFocused = colorQuitFocused 195 | colorUnfocused = colorQuitUnfocused 196 | } else { 197 | colorFocused = blend(colorPulseFocused, colorBaseFocused, uint32(i)) 198 | colorUnfocused = blend(colorPulseUnfocused, colorBaseUnfocused, uint32(i)) 199 | } 200 | for _, s := range screens { 201 | s.workspace.drawFrameBorders() 202 | } 203 | if i < len(cos)/2 || anyUnseenWindows { 204 | pulseDoneChan <- struct{}{} 205 | } 206 | } 207 | 208 | func blend(c0, c1, i uint32) uint32 { 209 | x := uint32(cos[i%uint32(len(cos))]) 210 | y := 256 - x 211 | r0 := (c0 >> 16) & 0xff 212 | g0 := (c0 >> 8) & 0xff 213 | b0 := (c0 >> 0) & 0xff 214 | r1 := (c1 >> 16) & 0xff 215 | g1 := (c1 >> 8) & 0xff 216 | b1 := (c1 >> 0) & 0xff 217 | r2 := ((r0 * x) + (r1 * y)) / 256 218 | g2 := ((g0 * x) + (g1 * y)) / 256 219 | b2 := ((b0 * x) + (b1 * y)) / 256 220 | return r2<<16 | g2<<8 | b2 221 | } 222 | 223 | // cos was generated by: 224 | // 225 | // const N = 32 226 | // for i := 0; i < N; i++ { 227 | // c := math.Cos(float64(i) * 2 * math.Pi / N) 228 | // fmt.Printf("%v,\n", int(0.5+(c+1)*256/2)) 229 | // } 230 | var cos = [32]uint16{ 231 | 256, 232 | 254, 233 | 246, 234 | 234, 235 | 219, 236 | 199, 237 | 177, 238 | 153, 239 | 128, 240 | 103, 241 | 79, 242 | 57, 243 | 37, 244 | 22, 245 | 10, 246 | 2, 247 | 0, 248 | 2, 249 | 10, 250 | 22, 251 | 37, 252 | 57, 253 | 79, 254 | 103, 255 | 128, 256 | 153, 257 | 177, 258 | 199, 259 | 219, 260 | 234, 261 | 246, 262 | 254, 263 | } 264 | 265 | var windowNames = [...]byte{ 266 | '1', 267 | '2', 268 | '3', 269 | '4', 270 | '5', 271 | '6', 272 | '7', 273 | '8', 274 | '9', 275 | '0', 276 | ':', 277 | } 278 | 279 | var workspaceNames = [...][3]byte{ 280 | {'F', '1', ' '}, 281 | {'F', '2', ' '}, 282 | {'F', '3', ' '}, 283 | {'F', '4', ' '}, 284 | {'F', '5', ' '}, 285 | {'F', '6', ' '}, 286 | {'F', '7', ' '}, 287 | {'F', '8', ' '}, 288 | {'F', '9', ' '}, 289 | {'F', '1', '0'}, 290 | {'F', '1', '1'}, 291 | {'F', '1', '2'}, 292 | {':', ':', ':'}, 293 | } 294 | -------------------------------------------------------------------------------- /geom.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | xp "github.com/BurntSushi/xgb/xproto" 7 | ) 8 | 9 | type orientation int 10 | 11 | const ( 12 | noOrientation orientation = iota 13 | horizontal 14 | vertical 15 | ) 16 | 17 | type traversal int 18 | 19 | const ( 20 | next traversal = iota 21 | prev 22 | ) 23 | 24 | type listing int 25 | 26 | const ( 27 | listNone listing = iota 28 | listWindows 29 | listWorkspaces 30 | ) 31 | 32 | // offscreenXY is a very negative X/Y co-ordinate. The most negative value is 33 | // -1<<15, but using that value is dangerously close to the int16 underflow 34 | // discontinuity. For example, using -1<<15 can cause the X server to close the 35 | // connection, exiting the window manager and thus the session, when using the 36 | // GIMP's "Perspective" tool that creates an unusual window to show the 37 | // perspective grid. It's not entirely obvious why mapping and configuring that 38 | // window leads to a connection close, but empirically, moving offscreenXY away 39 | // from that int16 underflow discontinuity prevents that from happening. 40 | const offscreenXY = -1<<15 + 1<<13 // = -32768 + 8192 = -24576 = int16(uint16(0xa000)) 41 | 42 | func contains(r xp.Rectangle, x, y int16) bool { 43 | return r.X <= x && x <= r.X+int16(r.Width) && 44 | r.Y <= y && y <= r.Y+int16(r.Height) 45 | } 46 | 47 | func screenContaining(x, y int16) *screen { 48 | for _, s := range screens { 49 | if contains(s.rect, x, y) { 50 | return s 51 | } 52 | } 53 | return screens[0] 54 | } 55 | 56 | var ( 57 | screens []*screen 58 | dummyWorkspace workspace // The anchor of a doubly-linked list of workspaces. 59 | ) 60 | 61 | func init() { 62 | dummyWorkspace.link[next] = &dummyWorkspace 63 | dummyWorkspace.link[prev] = &dummyWorkspace 64 | } 65 | 66 | func findWindow(predicate func(*window) bool) *window { 67 | for k := dummyWorkspace.link[next]; k != &dummyWorkspace; k = k.link[next] { 68 | for w := k.dummyWindow.link[next]; w != &k.dummyWindow; w = w.link[next] { 69 | if predicate(w) { 70 | return w 71 | } 72 | } 73 | } 74 | return nil 75 | } 76 | 77 | type screen struct { 78 | workspace *workspace 79 | rect xp.Rectangle 80 | } 81 | 82 | type workspace struct { 83 | link [2]*workspace 84 | screen *screen 85 | focusedFrame *frame 86 | mainFrame frame 87 | dummyWindow window // The anchor of a doubly-linked list of windows. 88 | fullscreen bool 89 | listing listing 90 | list []interface{} 91 | index int 92 | } 93 | 94 | type frame struct { 95 | parent *frame 96 | prevSibling *frame 97 | nextSibling *frame 98 | firstChild *frame 99 | lastChild *frame 100 | orientation orientation 101 | workspace *workspace 102 | window *window 103 | rect xp.Rectangle 104 | } 105 | 106 | type window struct { 107 | frame *frame 108 | link [2]*window 109 | transientFor *window 110 | xWin xp.Window 111 | rect xp.Rectangle 112 | name string 113 | offscreenSeqNum uint32 114 | hasTransientFor bool 115 | seen bool 116 | selected bool 117 | wmDeleteWindow bool 118 | wmTakeFocus bool 119 | } 120 | 121 | func (s *screen) repaint() { 122 | check(xp.ClearAreaChecked(xConn, true, desktopXWin, 123 | s.rect.X, s.rect.Y, s.rect.Width+1, s.rect.Height+1)) 124 | } 125 | 126 | func newWorkspace(rect xp.Rectangle, previous *workspace) *workspace { 127 | k := &workspace{ 128 | mainFrame: frame{ 129 | rect: rect, 130 | }, 131 | index: -1, 132 | } 133 | k.mainFrame.workspace = k 134 | k.dummyWindow.link[next] = &k.dummyWindow 135 | k.dummyWindow.link[prev] = &k.dummyWindow 136 | k.focusedFrame = &k.mainFrame 137 | 138 | k.link[next] = previous.link[next] 139 | k.link[prev] = previous 140 | k.link[next].link[prev] = k 141 | k.link[prev].link[next] = k 142 | 143 | k.mainFrame.split(horizontal) 144 | return k 145 | } 146 | 147 | func makeLists() { 148 | for _, s := range screens { 149 | if s.workspace.listing != listNone { 150 | s.workspace.makeList() 151 | } 152 | } 153 | } 154 | 155 | func (k *workspace) makeList() { 156 | switch k.listing { 157 | case listWindows: 158 | k.list = k.makeWindowList() 159 | case listWorkspaces: 160 | k.list = k.makeWorkspaceList() 161 | default: 162 | k.list = nil 163 | } 164 | k.index = -1 165 | if len(k.list) != 0 { 166 | if p, err := xp.QueryPointer(xConn, rootXWin).Reply(); err != nil { 167 | log.Println(err) 168 | } else if p != nil { 169 | k.index = k.indexForPoint(p.RootX, p.RootY) 170 | } 171 | } 172 | k.configure() 173 | k.screen.repaint() 174 | } 175 | 176 | func (k *workspace) makeWindowList() (list []interface{}) { 177 | for w := k.dummyWindow.link[next]; w != &k.dummyWindow; w = w.link[next] { 178 | // TODO: listen instead of poll for name changes. 179 | w.name = w.property(atomNetWMName) 180 | if w.name == "" { 181 | w.name = "?" 182 | } 183 | list = append(list, w) 184 | } 185 | return list 186 | } 187 | 188 | func (k *workspace) makeWorkspaceList() (list []interface{}) { 189 | for k := dummyWorkspace.link[next]; k != &dummyWorkspace; k = k.link[next] { 190 | list = append(list, k) 191 | list = append(list, k.makeWindowList()...) 192 | } 193 | return list 194 | } 195 | 196 | func (k *workspace) indexForPoint(rootX, rootY int16) int { 197 | r := k.focusedFrame.rect 198 | if k.fullscreen || k.listing == listWorkspaces { 199 | r = k.mainFrame.rect 200 | } 201 | x := int(rootX - r.X) 202 | y := int(rootY - r.Y) 203 | if x <= 0 || int(r.Width) <= x || y <= 0 || int(r.Height) <= y { 204 | return -1 205 | } 206 | i := int(y/fontHeight) - 1 207 | if i < 0 || len(k.list) <= i { 208 | return -1 209 | } 210 | if k.listing == listWorkspaces { 211 | for ; i >= 0; i-- { 212 | if _, ok := k.list[i].(*workspace); ok { 213 | return i 214 | } 215 | } 216 | } 217 | return i 218 | } 219 | 220 | func (k *workspace) configure() { 221 | for w := k.dummyWindow.link[next]; w != &k.dummyWindow; w = w.link[next] { 222 | w.configure() 223 | } 224 | } 225 | 226 | func (k *workspace) drawFrameBorders() { 227 | if k.fullscreen || k.listing == listWorkspaces { 228 | return 229 | } 230 | setForeground(colorUnfocused) 231 | rects := k.mainFrame.appendRectangles(nil) 232 | check(xp.PolyRectangleChecked(xConn, xp.Drawable(desktopXWin), desktopXGC, rects)) 233 | setForeground(colorFocused) 234 | k.focusedFrame.drawBorder() 235 | } 236 | 237 | func (k *workspace) focusFrame(f *frame) { 238 | if f == nil { 239 | return 240 | } 241 | if k.focusedFrame != f { 242 | if !k.fullscreen && k.listing != listWorkspaces { 243 | setForeground(colorUnfocused) 244 | k.focusedFrame.drawBorder() 245 | setForeground(colorFocused) 246 | f.drawBorder() 247 | } 248 | k.focusedFrame = f 249 | } 250 | w := f.window 251 | if k.listing != listNone { 252 | w = nil 253 | } 254 | focus(w) 255 | } 256 | 257 | func (k *workspace) frameContaining(x, y int16) *frame { 258 | if k.fullscreen || k.listing == listWorkspaces || contains(k.focusedFrame.rect, x, y) { 259 | return k.focusedFrame 260 | } 261 | return k.mainFrame.frameContaining(x, y) 262 | } 263 | 264 | func (k *workspace) layout() { 265 | if k.screen != nil { 266 | k.mainFrame.rect = k.screen.rect 267 | } else { 268 | k.mainFrame.rect = xp.Rectangle{X: offscreenXY, Y: offscreenXY, Width: 256, Height: 256} 269 | } 270 | k.mainFrame.layout() 271 | } 272 | 273 | func (f *frame) frameContaining(x, y int16) *frame { 274 | if contains(f.rect, x, y) { 275 | if f.firstChild == nil { 276 | return f 277 | } 278 | for c := f.firstChild; c != nil; c = c.nextSibling { 279 | if g := c.frameContaining(x, y); g != nil { 280 | return g 281 | } 282 | } 283 | } 284 | return nil 285 | } 286 | 287 | func (f *frame) firstDescendent() *frame { 288 | for f.firstChild != nil { 289 | f = f.firstChild 290 | } 291 | return f 292 | } 293 | 294 | func (f *frame) lastDescendent() *frame { 295 | for f.lastChild != nil { 296 | f = f.lastChild 297 | } 298 | return f 299 | } 300 | 301 | func (f *frame) firstEmptyFrame() *frame { 302 | if f.firstChild != nil { 303 | for c := f.firstChild; c != nil; c = c.nextSibling { 304 | if ret := c.firstEmptyFrame(); ret != nil { 305 | return ret 306 | } 307 | } 308 | } else if f.window == nil { 309 | return f 310 | } 311 | return nil 312 | } 313 | 314 | func (f *frame) numChildren() (n int) { 315 | for c := f.firstChild; c != nil; c = c.nextSibling { 316 | n++ 317 | } 318 | return n 319 | } 320 | 321 | func (f *frame) appendRectangles(r []xp.Rectangle) []xp.Rectangle { 322 | if f.firstChild != nil { 323 | for c := f.firstChild; c != nil; c = c.nextSibling { 324 | r = c.appendRectangles(r) 325 | } 326 | return r 327 | } 328 | return append(r, f.rect) 329 | } 330 | 331 | func (f *frame) split(o orientation) { 332 | if f.parent != nil && f.parent.orientation == o { 333 | g := &frame{ 334 | parent: f.parent, 335 | workspace: f.workspace, 336 | prevSibling: f, 337 | nextSibling: f.nextSibling, 338 | } 339 | if f.nextSibling != nil { 340 | f.nextSibling.prevSibling = g 341 | } else { 342 | f.parent.lastChild = g 343 | } 344 | f.nextSibling = g 345 | if f.window != nil { 346 | f.workspace.focusedFrame = g 347 | } 348 | f.parent.layout() 349 | return 350 | } 351 | 352 | f.orientation = o 353 | f.firstChild = &frame{ 354 | parent: f, 355 | workspace: f.workspace, 356 | } 357 | f.lastChild = &frame{ 358 | parent: f, 359 | workspace: f.workspace, 360 | } 361 | f.firstChild.nextSibling = f.lastChild 362 | f.lastChild.prevSibling = f.firstChild 363 | if f.workspace.focusedFrame == f { 364 | f.workspace.focusedFrame = f.firstChild 365 | } 366 | if w := f.window; w != nil { 367 | f.window = nil 368 | f.firstChild.window = w 369 | w.frame = f.firstChild 370 | } 371 | f.layout() 372 | } 373 | 374 | func (f *frame) layout() { 375 | if f.orientation == noOrientation { 376 | if f.window != nil { 377 | f.window.configure() 378 | } 379 | return 380 | } 381 | i, n := 0, f.numChildren() 382 | for c := f.firstChild; c != nil; i, c = i+1, c.nextSibling { 383 | c.rect = f.rect 384 | switch f.orientation { 385 | case horizontal: 386 | i0 := (i + 0) * int(f.rect.Width) / n 387 | i1 := (i + 1) * int(f.rect.Width) / n 388 | c.rect.X += int16(i0) 389 | c.rect.Width = uint16(i1 - i0) 390 | case vertical: 391 | i0 := (i + 0) * int(f.rect.Height) / n 392 | i1 := (i + 1) * int(f.rect.Height) / n 393 | c.rect.Y += int16(i0) 394 | c.rect.Height = uint16(i1 - i0) 395 | } 396 | c.layout() 397 | } 398 | } 399 | 400 | func (f *frame) traverse(t traversal) *frame { 401 | if f.parent == nil { 402 | return f 403 | } 404 | if t == next && f.nextSibling != nil { 405 | return f.nextSibling.firstDescendent() 406 | } 407 | if t == prev && f.prevSibling != nil { 408 | return f.prevSibling.lastDescendent() 409 | } 410 | f, from := f.parent, f 411 | for { 412 | switch from { 413 | case f.parent: 414 | if f.firstChild == nil { 415 | return f 416 | } 417 | if t == next { 418 | f, from = f.firstChild, f 419 | } else { 420 | f, from = f.lastChild, f 421 | } 422 | case f.firstChild: 423 | if f.prevSibling != nil { 424 | f, from = f.prevSibling, f 425 | } else if f.parent != nil { 426 | f, from = f.parent, f 427 | } else { 428 | f, from = f.lastChild, f 429 | } 430 | case f.lastChild: 431 | if f.nextSibling != nil { 432 | f, from = f.nextSibling, f 433 | } else if f.parent != nil { 434 | f, from = f.parent, f 435 | } else { 436 | f, from = f.firstChild, f 437 | } 438 | case f.prevSibling: 439 | return f.firstDescendent() 440 | case f.nextSibling: 441 | return f.lastDescendent() 442 | } 443 | } 444 | } 445 | 446 | func (f *frame) drawBorder() { 447 | check(xp.PolyRectangleChecked(xConn, xp.Drawable(desktopXWin), desktopXGC, 448 | []xp.Rectangle{f.rect})) 449 | } 450 | 451 | var nextOffscreenSeqNum uint32 = 1 452 | 453 | func (w *window) property(a xp.Atom) string { 454 | p, err := xp.GetProperty(xConn, false, w.xWin, a, xp.GetPropertyTypeAny, 0, 1<<32-1).Reply() 455 | if err != nil { 456 | log.Println(err) 457 | } 458 | if p == nil { 459 | return "" 460 | } 461 | return string(p.Value) 462 | } 463 | 464 | func (w *window) configure() { 465 | mask, values := uint16(0), []uint32(nil) 466 | r := xp.Rectangle{X: offscreenXY, Y: offscreenXY, Width: w.rect.Width, Height: w.rect.Height} 467 | if w.frame != nil && w.frame.workspace.screen != nil { 468 | k := w.frame.workspace 469 | if k.listing == listWorkspaces || 470 | (k.listing == listWindows && k.focusedFrame == w.frame) { 471 | // No-op; r is offscreen. 472 | } else if k.fullscreen { 473 | if k.focusedFrame == w.frame { 474 | r.X = k.mainFrame.rect.X 475 | r.Y = k.mainFrame.rect.Y 476 | r.Width = k.mainFrame.rect.Width + 1 477 | r.Height = k.mainFrame.rect.Height + 1 478 | } 479 | } else { 480 | r.X = w.frame.rect.X + 2 481 | r.Y = w.frame.rect.Y + 2 482 | r.Width = w.frame.rect.Width - 3 483 | r.Height = w.frame.rect.Height - 3 484 | } 485 | } 486 | if w.seen && w.rect == r { 487 | return 488 | } 489 | w.rect = r 490 | if r.X != offscreenXY { 491 | w.seen = true 492 | mask = xp.ConfigWindowX | 493 | xp.ConfigWindowY | 494 | xp.ConfigWindowWidth | 495 | xp.ConfigWindowHeight | 496 | xp.ConfigWindowBorderWidth 497 | values = []uint32{ 498 | uint32(uint16(r.X)), 499 | uint32(uint16(r.Y)), 500 | uint32(r.Width), 501 | uint32(r.Height), 502 | 0, 503 | } 504 | } else { 505 | w.offscreenSeqNum = nextOffscreenSeqNum 506 | nextOffscreenSeqNum++ 507 | mask = xp.ConfigWindowX | xp.ConfigWindowY 508 | values = []uint32{ 509 | uint32(uint16(r.X)), 510 | uint32(uint16(r.Y)), 511 | } 512 | } 513 | check(xp.ConfigureWindowChecked(xConn, w.xWin, mask, values)) 514 | } 515 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nigeltao/taowm 2 | 3 | go 1.15 4 | 5 | require github.com/BurntSushi/xgb v0.0.0-20201008132610-5f9e7b3c49cd // indirect 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/xgb v0.0.0-20201008132610-5f9e7b3c49cd h1:u7K2oMFMd8APDV3fM1j2rO3U/XJf1g1qC3DDTKou8iM= 2 | github.com/BurntSushi/xgb v0.0.0-20201008132610-5f9e7b3c49cd/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 3 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | xp "github.com/BurntSushi/xgb/xproto" 7 | ) 8 | 9 | func handleButtonPress(e xp.ButtonPressEvent) { 10 | s := screenContaining(e.RootX, e.RootY) 11 | button := e.Detail 12 | if e.State&xp.ModMaskControl != 0 { 13 | // Control-click is treated as a Middle Mouse Button. 14 | button = 2 15 | } else if e.State&xp.ModMask1 != 0 { 16 | // Alt-click is treated as a Right Mouse Button. 17 | button = 3 18 | } 19 | if button == 2 { 20 | // Middle Mouse Button is treated as Mouse-Wheel Up/Down. 21 | if e.State&xp.ModMaskShift != 0 { 22 | button = 4 23 | } else { 24 | button = 5 25 | } 26 | } 27 | k := s.workspace 28 | if k.listing != listNone && button > 3 { 29 | return 30 | } 31 | if k.listing == listWindows { 32 | button = 1 33 | } else if k.listing == listWorkspaces { 34 | button = 3 35 | } 36 | 37 | switch button { 38 | case 1: 39 | if k.index >= 0 { 40 | if iw, ok := k.list[k.index].(*window); ok { 41 | k.listing, k.list, k.index = listNone, nil, -1 42 | changeWindow(k.focusedFrame, k.focusedFrame.window, iw) 43 | } 44 | s.repaint() 45 | } else { 46 | doList(k, listWindows) 47 | } 48 | case 3: 49 | if k.index >= 0 { 50 | if ik, ok := k.list[k.index].(*workspace); ok { 51 | k.listing, k.list, k.index = listNone, nil, -1 52 | changeWorkspace(s, k, ik) 53 | } 54 | s.repaint() 55 | } else { 56 | doList(k, listWorkspaces) 57 | } 58 | case 4: 59 | doWindow(k, prev) 60 | case 5: 61 | doWindow(k, next) 62 | } 63 | 64 | if button <= 3 { 65 | k = s.workspace 66 | w := k.focusedFrame.window 67 | if k.listing != listNone { 68 | w = nil 69 | } 70 | focus(w) 71 | } 72 | } 73 | 74 | func handleEnterNotify(e xp.EnterNotifyEvent) { 75 | w := findWindow(func(w *window) bool { return w.xWin == e.Event }) 76 | if w == nil || w.frame == nil { 77 | return 78 | } 79 | k := w.frame.workspace 80 | f0 := k.focusedFrame 81 | k.focusFrame(w.frame) 82 | if k.listing == listWindows && k.focusedFrame != f0 { 83 | k.makeList() 84 | } 85 | } 86 | 87 | func handleKeyPress(e xp.KeyPressEvent) { 88 | shift := 0 89 | if e.State&xp.ModMaskShift != 0 { 90 | shift = 1 91 | } 92 | keysym := int32(keysyms[e.Detail][shift]) 93 | if shift != 0 { 94 | if keysym == 0 { 95 | keysym = int32(keysyms[e.Detail][0]) 96 | } 97 | keysym = ^keysym 98 | } 99 | if a := actions[keysym]; a.do != nil { 100 | if a.do(screenContaining(e.RootX, e.RootY).workspace, a.arg) { 101 | pulseChan <- time.Now() 102 | } 103 | } 104 | } 105 | 106 | func handleMotionNotify(e xp.MotionNotifyEvent) { 107 | s := screenContaining(e.RootX, e.RootY) 108 | k := s.workspace 109 | f0 := k.focusedFrame 110 | if !k.fullscreen && k.listing != listWorkspaces { 111 | k.focusFrame(k.frameContaining(e.RootX, e.RootY)) 112 | } 113 | if k.listing == listNone { 114 | return 115 | } 116 | i1, i0 := k.indexForPoint(e.RootX, e.RootY), k.index 117 | k.index = i1 118 | if k.listing == listWindows && k.focusedFrame != f0 { 119 | k.makeList() 120 | return 121 | } 122 | if i1 == i0 { 123 | return 124 | } 125 | 126 | x, y := clip(k) 127 | setForeground(colorPulseFocused) 128 | y += int16(fontHeight + fontHeight1) 129 | if i0 != -1 { 130 | drawText(x+int16(fontWidth), y+int16(i0*fontHeight), " ") 131 | } 132 | if i1 != -1 { 133 | drawText(x+int16(fontWidth), y+int16(i1*fontHeight), ">") 134 | } 135 | unclip() 136 | } 137 | -------------------------------------------------------------------------------- /keysym.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // These constants come from /usr/include/X11/keysymdef.h. 4 | 5 | import ( 6 | xp "github.com/BurntSushi/xgb/xproto" 7 | ) 8 | 9 | const ( 10 | xkISOLeftTab = 0xfe20 11 | xkBackspace = 0xff08 12 | xkTab = 0xff09 13 | xkReturn = 0xff0d 14 | xkEscape = 0xff1b 15 | xkHome = 0xff50 16 | xkLeft = 0xff51 17 | xkUp = 0xff52 18 | xkRight = 0xff53 19 | xkDown = 0xff54 20 | xkPageUp = 0xff55 21 | xkPageDown = 0xff56 22 | xkEnd = 0xff57 23 | xkMenu = 0xff67 24 | xkF1 = 0xffbe 25 | xkF2 = 0xffbf 26 | xkF3 = 0xffc0 27 | xkF4 = 0xffc1 28 | xkF5 = 0xffc2 29 | xkF6 = 0xffc3 30 | xkF7 = 0xffc4 31 | xkF8 = 0xffc5 32 | xkF9 = 0xffc6 33 | xkF10 = 0xffc7 34 | xkF11 = 0xffc8 35 | xkF12 = 0xffc9 36 | xkShiftL = 0xffe1 37 | xkShiftR = 0xffe2 38 | xkControlL = 0xffe3 39 | xkControlR = 0xffe4 40 | xkCapsLock = 0xffe5 41 | xkShiftLock = 0xffe6 42 | xkMetaL = 0xffe7 43 | xkMetaR = 0xffe8 44 | xkAltL = 0xffe9 45 | xkAltR = 0xffea 46 | xkSuperL = 0xffeb 47 | xkSuperR = 0xffec 48 | xkHyperL = 0xffed 49 | xkHyperR = 0xffee 50 | xkDelete = 0xffff 51 | 52 | xkAudioLowerVolume = 0x1008ff11 53 | xkAudioMute = 0x1008ff12 54 | xkAudioRaiseVolume = 0x1008ff13 55 | ) 56 | 57 | func keysymString(keysym xp.Keysym) string { 58 | switch keysym { 59 | case xkMenu: 60 | return "Menu" 61 | case xkShiftL: 62 | return "ShiftL" 63 | case xkShiftR: 64 | return "ShiftR" 65 | case xkControlL: 66 | return "ControlL" 67 | case xkControlR: 68 | return "ControlR" 69 | case xkCapsLock: 70 | return "CapsLock" 71 | case xkShiftLock: 72 | return "ShiftLock" 73 | case xkMetaL: 74 | return "MetaL" 75 | case xkMetaR: 76 | return "MetaR" 77 | case xkAltL: 78 | return "AltL" 79 | case xkAltR: 80 | return "AltR" 81 | case xkSuperL: 82 | return "SuperL" 83 | case xkSuperR: 84 | return "SuperR" 85 | case xkHyperL: 86 | return "HyperL" 87 | case xkHyperR: 88 | return "HyperR" 89 | } 90 | return "UnknownKeysym" 91 | } 92 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "time" 7 | 8 | "github.com/BurntSushi/xgb" 9 | "github.com/BurntSushi/xgb/xinerama" 10 | xp "github.com/BurntSushi/xgb/xproto" 11 | ) 12 | 13 | var ( 14 | xConn *xgb.Conn 15 | rootXWin xp.Window 16 | 17 | eventTime xp.Timestamp 18 | keyRootX int16 19 | keyRootY int16 20 | keyState uint16 21 | 22 | // proactiveChan carries X operations that happen of the program's 23 | // own accord, such as animations. These are sent to the main goroutine 24 | // from other goroutines. In comparison, examples of reactive operations 25 | // are responding to window creation and key presses. 26 | proactiveChan = make(chan func()) 27 | ) 28 | 29 | type checker interface { 30 | Check() error 31 | } 32 | 33 | var checkers []checker 34 | 35 | func check(c checker) { 36 | checkers = append(checkers, c) 37 | } 38 | 39 | func sendClientMessage(xWin xp.Window, atom xp.Atom) { 40 | check(xp.SendEventChecked(xConn, false, xWin, xp.EventMaskNoEvent, 41 | string(xp.ClientMessageEvent{ 42 | Format: 32, 43 | Window: xWin, 44 | Type: atomWMProtocols, 45 | Data: xp.ClientMessageDataUnionData32New([]uint32{ 46 | uint32(atom), 47 | uint32(eventTime), 48 | 0, 49 | 0, 50 | 0, 51 | }), 52 | }.Bytes()), 53 | )) 54 | } 55 | 56 | func handleConfigureNotify(e xp.ConfigureNotifyEvent) { 57 | if rootXWin != e.Window || (desktopWidth == e.Width && desktopHeight == e.Height) { 58 | return 59 | } 60 | desktopWidth, desktopHeight = e.Width, e.Height 61 | 62 | check(xp.ConfigureWindowChecked( 63 | xConn, 64 | desktopXWin, 65 | xp.ConfigWindowWidth|xp.ConfigWindowHeight, 66 | []uint32{ 67 | uint32(desktopWidth), 68 | uint32(desktopHeight), 69 | }, 70 | )) 71 | check(xp.SetClipRectanglesChecked( 72 | xConn, xp.ClipOrderingUnsorted, desktopXGC, 0, 0, []xp.Rectangle{{ 73 | X: 0, Y: 0, Width: desktopWidth, Height: desktopHeight, 74 | }})) 75 | check(xp.ClearAreaChecked(xConn, true, desktopXWin, 0, 0, desktopWidth, desktopHeight)) 76 | 77 | initScreens() 78 | for k := dummyWorkspace.link[next]; k != &dummyWorkspace; k = k.link[next] { 79 | k.layout() 80 | } 81 | makeLists() 82 | } 83 | 84 | func handleConfigureRequest(e xp.ConfigureRequestEvent) { 85 | if w := findWindow(func(w *window) bool { return w.xWin == e.Window }); w != nil { 86 | cne := xp.ConfigureNotifyEvent{ 87 | Event: w.xWin, 88 | Window: w.xWin, 89 | X: w.rect.X, 90 | Y: w.rect.Y, 91 | Width: w.rect.Width, 92 | Height: w.rect.Height, 93 | } 94 | check(xp.SendEventChecked(xConn, false, w.xWin, 95 | xp.EventMaskStructureNotify, string(cne.Bytes()))) 96 | return 97 | } 98 | 99 | mask, values := uint16(0), []uint32(nil) 100 | if e.ValueMask&xp.ConfigWindowX != 0 { 101 | mask |= xp.ConfigWindowX 102 | values = append(values, uint32(e.X)) 103 | } 104 | if e.ValueMask&xp.ConfigWindowY != 0 { 105 | mask |= xp.ConfigWindowY 106 | values = append(values, uint32(e.Y)) 107 | } 108 | if e.ValueMask&xp.ConfigWindowWidth != 0 { 109 | mask |= xp.ConfigWindowWidth 110 | values = append(values, uint32(e.Width)) 111 | } 112 | if e.ValueMask&xp.ConfigWindowHeight != 0 { 113 | mask |= xp.ConfigWindowHeight 114 | values = append(values, uint32(e.Height)) 115 | } 116 | if e.ValueMask&xp.ConfigWindowBorderWidth != 0 { 117 | mask |= xp.ConfigWindowBorderWidth 118 | values = append(values, uint32(e.BorderWidth)) 119 | } 120 | if e.ValueMask&xp.ConfigWindowSibling != 0 { 121 | mask |= xp.ConfigWindowSibling 122 | values = append(values, uint32(e.Sibling)) 123 | } 124 | if e.ValueMask&xp.ConfigWindowStackMode != 0 { 125 | mask |= xp.ConfigWindowStackMode 126 | values = append(values, uint32(e.StackMode)) 127 | } 128 | check(xp.ConfigureWindowChecked(xConn, e.Window, mask, values)) 129 | } 130 | 131 | func manage(xWin xp.Window, mapRequest bool) { 132 | callFocus := false 133 | w := findWindow(func(w *window) bool { return w.xWin == xWin }) 134 | if w == nil { 135 | wmDeleteWindow, wmTakeFocus := false, false 136 | if prop, err := xp.GetProperty(xConn, false, xWin, atomWMProtocols, 137 | xp.GetPropertyTypeAny, 0, 64).Reply(); err != nil { 138 | log.Println(err) 139 | } else if prop != nil { 140 | for v := prop.Value; len(v) >= 4; v = v[4:] { 141 | switch xp.Atom(u32(v)) { 142 | case atomWMDeleteWindow: 143 | wmDeleteWindow = true 144 | case atomWMTakeFocus: 145 | wmTakeFocus = true 146 | } 147 | } 148 | } 149 | 150 | transientFor := (*window)(nil) 151 | if prop, err := xp.GetProperty(xConn, false, xWin, atomWMTransientFor, 152 | xp.GetPropertyTypeAny, 0, 64).Reply(); err != nil { 153 | log.Println(err) 154 | } else if prop != nil { 155 | if v := prop.Value; len(v) == 4 { 156 | transientForXWin := xp.Window(u32(v)) 157 | transientFor = findWindow(func(w *window) bool { 158 | return w.xWin == transientForXWin 159 | }) 160 | } 161 | } 162 | 163 | k := screens[0].workspace 164 | if p, err := xp.QueryPointer(xConn, rootXWin).Reply(); err != nil { 165 | log.Println(err) 166 | } else if p != nil { 167 | k = screenContaining(p.RootX, p.RootY).workspace 168 | } 169 | w = &window{ 170 | transientFor: transientFor, 171 | xWin: xWin, 172 | rect: xp.Rectangle{ 173 | X: offscreenXY, 174 | Y: offscreenXY, 175 | Width: 1, 176 | Height: 1, 177 | }, 178 | wmDeleteWindow: wmDeleteWindow, 179 | wmTakeFocus: wmTakeFocus, 180 | } 181 | f := k.focusedFrame 182 | previous := k.dummyWindow.link[prev] 183 | if transientFor != nil { 184 | previous = transientFor 185 | } else if f.window != nil { 186 | previous = f.window 187 | } 188 | w.link[next] = previous.link[next] 189 | w.link[prev] = previous 190 | w.link[next].link[prev] = w 191 | w.link[prev].link[next] = w 192 | 193 | if transientFor != nil && transientFor.frame != nil { 194 | f = transientFor.frame 195 | f.window, transientFor.frame = nil, nil 196 | } else if f.window != nil { 197 | f = k.mainFrame.firstEmptyFrame() 198 | } 199 | if f != nil { 200 | f.window, w.frame = w, f 201 | callFocus = f == k.focusedFrame 202 | } else { 203 | pulseChan <- time.Now() 204 | } 205 | 206 | check(xp.ChangeWindowAttributesChecked(xConn, xWin, xp.CwEventMask, 207 | []uint32{xp.EventMaskEnterWindow | xp.EventMaskStructureNotify}, 208 | )) 209 | w.configure() 210 | if transientFor != nil { 211 | transientFor.hasTransientFor = true 212 | transientFor.configure() 213 | } 214 | } 215 | if mapRequest { 216 | check(xp.MapWindowChecked(xConn, xWin)) 217 | } 218 | if callFocus { 219 | focus(w) 220 | } 221 | makeLists() 222 | pulseChan <- time.Now() 223 | } 224 | 225 | func unmanage(xWin xp.Window) { 226 | w := findWindow(func(w *window) bool { return w.xWin == xWin }) 227 | if w == nil { 228 | return 229 | } 230 | if quitting && findWindow(func(w *window) bool { return true }) == nil { 231 | os.Exit(0) 232 | } 233 | if w.hasTransientFor { 234 | for { 235 | w1 := findWindow(func(w2 *window) bool { return w2.transientFor == w }) 236 | if w1 == nil { 237 | break 238 | } 239 | w1.transientFor = nil 240 | } 241 | } 242 | if f := w.frame; f != nil { 243 | k := f.workspace 244 | replacement := (*window)(nil) 245 | if w.transientFor != nil && w.transientFor.frame == nil { 246 | replacement = w.transientFor 247 | } else { 248 | bestOffscreenSeqNum := uint32(0) 249 | for w1 := w.link[next]; w1 != w; w1 = w1.link[next] { 250 | if w1.offscreenSeqNum <= bestOffscreenSeqNum { 251 | continue 252 | } 253 | if k.fullscreen { 254 | if w1.frame == k.focusedFrame { 255 | continue 256 | } 257 | } else if w1.frame != nil { 258 | continue 259 | } 260 | replacement, bestOffscreenSeqNum = w1, w1.offscreenSeqNum 261 | } 262 | } 263 | if replacement != nil { 264 | if f0 := replacement.frame; f0 != nil { 265 | f0.window, replacement.frame = nil, nil 266 | } 267 | f.window, replacement.frame = replacement, f 268 | replacement.configure() 269 | if p, err := xp.QueryPointer(xConn, rootXWin).Reply(); err != nil { 270 | log.Println(err) 271 | } else if p != nil && contains(f.rect, p.RootX, p.RootY) { 272 | focus(replacement) 273 | } 274 | } else { 275 | f.window = nil 276 | if k.fullscreen && f == k.focusedFrame { 277 | doFullscreen(k, nil) 278 | } 279 | } 280 | } 281 | w.link[next].link[prev] = w.link[prev] 282 | w.link[prev].link[next] = w.link[next] 283 | *w = window{} 284 | makeLists() 285 | pulseChan <- time.Now() 286 | } 287 | 288 | type xEventOrError struct { 289 | event xgb.Event 290 | error xgb.Error 291 | } 292 | 293 | func main() { 294 | var err error 295 | xConn, err = xgb.NewConn() 296 | if err != nil { 297 | log.Fatal(err) 298 | } 299 | if err = xinerama.Init(xConn); err != nil { 300 | log.Fatal(err) 301 | } 302 | xSetup := xp.Setup(xConn) 303 | if len(xSetup.Roots) != 1 { 304 | log.Fatalf("X setup has unsupported number of roots: %d", len(xSetup.Roots)) 305 | } 306 | rootXWin = xSetup.Roots[0].Root 307 | 308 | becomeTheWM() 309 | initAtoms() 310 | initDesktop(&xSetup.Roots[0]) 311 | initKeyboardMapping() 312 | initScreens() 313 | 314 | // Manage any existing windows. 315 | if tree, err := xp.QueryTree(xConn, rootXWin).Reply(); err != nil { 316 | log.Fatal(err) 317 | } else if tree != nil { 318 | for _, c := range tree.Children { 319 | if c == desktopXWin { 320 | continue 321 | } 322 | attrs, err := xp.GetWindowAttributes(xConn, c).Reply() 323 | if attrs == nil || err != nil { 324 | continue 325 | } 326 | if attrs.OverrideRedirect || attrs.MapState == xp.MapStateUnmapped { 327 | continue 328 | } 329 | manage(c, false) 330 | } 331 | } 332 | 333 | // Focus the last frame of the first screen. The first screen because 334 | // there's typically only one screen. The last frame because, for 335 | // left-to-right languages, the last frame's text is typically closer to 336 | // the screen center than the first frame's text. 337 | if len(screens) > 0 { 338 | warpPointerTo(screens[0].workspace.mainFrame.lastDescendent()) 339 | } 340 | 341 | // Process X events. 342 | eeChan := make(chan xEventOrError) 343 | go func() { 344 | for { 345 | e, err := xConn.WaitForEvent() 346 | eeChan <- xEventOrError{e, err} 347 | } 348 | }() 349 | for { 350 | for i, c := range checkers { 351 | if err := c.Check(); err != nil { 352 | log.Println(err) 353 | } 354 | checkers[i] = nil 355 | } 356 | checkers = checkers[:0] 357 | 358 | select { 359 | case f := <-proactiveChan: 360 | f() 361 | case ee := <-eeChan: 362 | if ee.error != nil { 363 | log.Println(ee.error) 364 | continue 365 | } 366 | switch e := ee.event.(type) { 367 | case xp.ButtonPressEvent: 368 | eventTime = e.Time 369 | handleButtonPress(e) 370 | case xp.ButtonReleaseEvent: 371 | eventTime = e.Time 372 | case xp.ClientMessageEvent: 373 | // No-op. 374 | case xp.ConfigureNotifyEvent: 375 | handleConfigureNotify(e) 376 | case xp.ConfigureRequestEvent: 377 | handleConfigureRequest(e) 378 | case xp.DestroyNotifyEvent: 379 | // No-op. 380 | case xp.EnterNotifyEvent: 381 | eventTime = e.Time 382 | handleEnterNotify(e) 383 | case xp.ExposeEvent: 384 | handleExpose(e) 385 | case xp.KeyPressEvent: 386 | eventTime, keyRootX, keyRootY, keyState = e.Time, e.RootX, e.RootY, e.State 387 | handleKeyPress(e) 388 | case xp.KeyReleaseEvent: 389 | eventTime, keyRootX, keyRootY, keyState = e.Time, 0, 0, 0 390 | case xp.MapNotifyEvent: 391 | // No-op. 392 | case xp.MappingNotifyEvent: 393 | // No-op. 394 | case xp.MapRequestEvent: 395 | manage(e.Window, true) 396 | case xp.MotionNotifyEvent: 397 | eventTime = e.Time 398 | handleMotionNotify(e) 399 | case xp.UnmapNotifyEvent: 400 | unmanage(e.Window) 401 | default: 402 | log.Printf("unhandled event: %v", ee.event) 403 | } 404 | } 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /xinit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "os/exec" 7 | 8 | "github.com/BurntSushi/xgb/xinerama" 9 | xp "github.com/BurntSushi/xgb/xproto" 10 | ) 11 | 12 | var ( 13 | atomNetActiveWindow xp.Atom 14 | atomNetWMName xp.Atom 15 | atomWindow xp.Atom 16 | atomWMClass xp.Atom 17 | atomWMDeleteWindow xp.Atom 18 | atomWMProtocols xp.Atom 19 | atomWMTakeFocus xp.Atom 20 | atomWMTransientFor xp.Atom 21 | 22 | desktopXWin xp.Window 23 | desktopXGC xp.Gcontext 24 | desktopWidth uint16 25 | desktopHeight uint16 26 | 27 | keysyms [256][2]xp.Keysym 28 | ) 29 | 30 | func becomeTheWM() { 31 | if err := xp.ChangeWindowAttributesChecked(xConn, rootXWin, xp.CwEventMask, []uint32{ 32 | xp.EventMaskButtonPress | 33 | xp.EventMaskButtonRelease | 34 | xp.EventMaskPointerMotion | 35 | xp.EventMaskStructureNotify | 36 | xp.EventMaskSubstructureRedirect, 37 | }).Check(); err != nil { 38 | if _, ok := err.(xp.AccessError); ok { 39 | log.Fatal("could not become the window manager. Is another window manager running?") 40 | } 41 | log.Fatal(err) 42 | } 43 | } 44 | 45 | func initAtoms() { 46 | atomNetActiveWindow = internAtom("_NET_ACTIVE_WINDOW") 47 | atomNetWMName = internAtom("_NET_WM_NAME") 48 | atomWindow = internAtom("WINDOW") 49 | atomWMClass = internAtom("WM_CLASS") 50 | atomWMDeleteWindow = internAtom("WM_DELETE_WINDOW") 51 | atomWMProtocols = internAtom("WM_PROTOCOLS") 52 | atomWMTakeFocus = internAtom("WM_TAKE_FOCUS") 53 | atomWMTransientFor = internAtom("WM_TRANSIENT_FOR") 54 | } 55 | 56 | func internAtom(name string) xp.Atom { 57 | r, err := xp.InternAtom(xConn, false, uint16(len(name)), name).Reply() 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | if r == nil { 62 | return 0 63 | } 64 | return r.Atom 65 | } 66 | 67 | func initDesktop(xScreen *xp.ScreenInfo) { 68 | xCursorFont, err := xp.NewFontId(xConn) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | xCursor, err := xp.NewCursorId(xConn) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | err = xp.OpenFontChecked(xConn, xCursorFont, uint16(len("cursor")), "cursor").Check() 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | const xcLeftPtr = 68 // XC_left_ptr from cursorfont.h. 81 | err = xp.CreateGlyphCursorChecked( 82 | xConn, xCursor, xCursorFont, xCursorFont, xcLeftPtr, xcLeftPtr+1, 83 | 0xffff, 0xffff, 0xffff, 0, 0, 0).Check() 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | err = xp.CloseFontChecked(xConn, xCursorFont).Check() 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | xTextFont, err := xp.NewFontId(xConn) 93 | if err != nil { 94 | log.Fatal(err) 95 | } 96 | err = xp.OpenFontChecked(xConn, xTextFont, uint16(len(fontName)), fontName).Check() 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | defer xp.CloseFont(xConn, xTextFont) 101 | 102 | desktopXWin, err = xp.NewWindowId(xConn) 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | desktopXGC, err = xp.NewGcontextId(xConn) 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | desktopWidth = xScreen.WidthInPixels 111 | desktopHeight = xScreen.HeightInPixels 112 | 113 | if err := xp.CreateWindowChecked( 114 | xConn, xScreen.RootDepth, desktopXWin, xScreen.Root, 115 | 0, 0, desktopWidth, desktopHeight, 0, 116 | xp.WindowClassInputOutput, 117 | xScreen.RootVisual, 118 | xp.CwOverrideRedirect|xp.CwEventMask, 119 | []uint32{ 120 | 1, 121 | xp.EventMaskExposure, 122 | }, 123 | ).Check(); err != nil { 124 | log.Fatal(err) 125 | } 126 | 127 | if len(xSettings) != 0 { 128 | initXSettings() 129 | } 130 | 131 | if err := xp.ConfigureWindowChecked( 132 | xConn, 133 | desktopXWin, 134 | xp.ConfigWindowStackMode, 135 | []uint32{ 136 | xp.StackModeBelow, 137 | }, 138 | ).Check(); err != nil { 139 | log.Fatal(err) 140 | } 141 | 142 | if err := xp.ChangeWindowAttributesChecked( 143 | xConn, 144 | desktopXWin, 145 | xp.CwBackPixel|xp.CwCursor, 146 | []uint32{ 147 | xScreen.BlackPixel, 148 | uint32(xCursor), 149 | }, 150 | ).Check(); err != nil { 151 | log.Fatal(err) 152 | } 153 | 154 | if err := xp.CreateGCChecked( 155 | xConn, 156 | desktopXGC, 157 | xp.Drawable(xScreen.Root), 158 | xp.GcFont, 159 | []uint32{ 160 | uint32(xTextFont), 161 | }, 162 | ).Check(); err != nil { 163 | log.Fatal(err) 164 | } 165 | 166 | if err := xp.MapWindowChecked(xConn, desktopXWin).Check(); err != nil { 167 | log.Fatal(err) 168 | } 169 | } 170 | 171 | func initKeyboardMapping() { 172 | const ( 173 | keyLo = 8 174 | keyHi = 255 175 | ) 176 | km, err := xp.GetKeyboardMapping(xConn, keyLo, keyHi-keyLo+1).Reply() 177 | if err != nil { 178 | log.Fatal(err) 179 | } 180 | if km == nil { 181 | log.Fatal("couldn't get keyboard mapping") 182 | } 183 | n := int(km.KeysymsPerKeycode) 184 | if n < 2 { 185 | log.Fatalf("too few keysyms per keycode: %d", n) 186 | } 187 | for i := keyLo; i <= keyHi; i++ { 188 | keysyms[i][0] = km.Keysyms[(i-keyLo)*n+0] 189 | keysyms[i][1] = km.Keysyms[(i-keyLo)*n+1] 190 | } 191 | 192 | toGrabs := []xp.Keysym{wmKeysym} 193 | if doAudioActions { 194 | toGrabs = append(toGrabs, xkAudioLowerVolume, xkAudioMute, xkAudioRaiseVolume) 195 | } 196 | for _, toGrab := range toGrabs { 197 | keycode := xp.Keycode(0) 198 | for i := keyLo; i <= keyHi; i++ { 199 | if keysyms[i][0] == toGrab || keysyms[i][1] == toGrab { 200 | keycode = xp.Keycode(i) 201 | break 202 | } 203 | } 204 | if keycode == 0 { 205 | if toGrab != wmKeysym { 206 | continue 207 | } 208 | log.Fatalf("could not find the window manager key %s", keysymString(toGrab)) 209 | } 210 | if err := xp.GrabKeyChecked(xConn, false, rootXWin, xp.ModMaskAny, keycode, 211 | xp.GrabModeAsync, xp.GrabModeAsync).Check(); err != nil { 212 | log.Fatal(err) 213 | } 214 | } 215 | 216 | // Disable Caps Lock if it is the wmKeysym. 217 | if wmKeysym == xkCapsLock { 218 | // On Ubuntu 12.04, disabling Caps Lock involved the equivalent of 219 | // `xmodmap -e "clear lock"`. On Ubuntu 14.04, XKB has replaced xmodmap, 220 | // possibly because this facilitates per-window keyboard layouts, so the 221 | // equivalent of `xmodmap -e "clear lock"` doesn't work. As of October 222 | // 2014, github.com/BurntSushi/xgb doesn't support XKB, so we exec the 223 | // setxkbmap program instead of speaking the X11 protocol directly to 224 | // disable Caps Lock. 225 | if err := exec.Command("setxkbmap", "-option", "caps:none").Run(); err != nil { 226 | log.Fatalf("setxkbmap failed: %v", err) 227 | } 228 | } 229 | } 230 | 231 | func findKeycode(keysym xp.Keysym) (keycode xp.Keycode, shift bool) { 232 | for i, k := range keysyms { 233 | if k[0] == keysym { 234 | return xp.Keycode(i), false 235 | } 236 | if k[1] == keysym { 237 | return xp.Keycode(i), true 238 | } 239 | } 240 | return 0, false 241 | } 242 | 243 | func initScreens() { 244 | oldScreens := screens 245 | 246 | xine, err := xinerama.QueryScreens(xConn).Reply() 247 | if err != nil { 248 | log.Fatal(err) 249 | } 250 | if len(xine.ScreenInfo) > 0 { 251 | screens = make([]*screen, len(xine.ScreenInfo)) 252 | for i, si := range xine.ScreenInfo { 253 | screens[i] = &screen{ 254 | rect: xp.Rectangle{ 255 | X: si.XOrg, 256 | Y: si.YOrg, 257 | Width: si.Width - 1, 258 | Height: si.Height - 1, 259 | }, 260 | } 261 | } 262 | } else { 263 | screens = make([]*screen, 1) 264 | screens[0] = &screen{ 265 | rect: xp.Rectangle{ 266 | X: 0, 267 | Y: 0, 268 | Width: desktopWidth - 1, 269 | Height: desktopHeight - 1, 270 | }, 271 | } 272 | } 273 | 274 | for i, s := range screens { 275 | k := (*workspace)(nil) 276 | if i < len(oldScreens) { 277 | k = oldScreens[i].workspace 278 | oldScreens[i].workspace = nil 279 | } else { 280 | k = newWorkspace(s.rect, dummyWorkspace.link[prev]) 281 | } 282 | s.workspace, k.screen = k, s 283 | } 284 | 285 | if len(oldScreens) > len(screens) { 286 | for _, s := range oldScreens[len(screens):] { 287 | s.workspace.screen = nil 288 | s.workspace = nil 289 | } 290 | } 291 | } 292 | 293 | func initXSettings() { 294 | a0 := internAtom("_XSETTINGS_S0") 295 | if err := xp.SetSelectionOwnerChecked(xConn, desktopXWin, a0, 296 | xp.TimeCurrentTime).Check(); err != nil { 297 | log.Printf("could not set xsettings: %v", err) 298 | return 299 | } 300 | a1 := internAtom("_XSETTINGS_SETTINGS") 301 | encoded := makeEncodedXSettings() 302 | if err := xp.ChangePropertyChecked(xConn, xp.PropModeReplace, desktopXWin, a1, a1, 303 | 8, uint32(len(encoded)), encoded).Check(); err != nil { 304 | log.Printf("could not set xsettings: %v", err) 305 | return 306 | } 307 | } 308 | 309 | func makeEncodedXSettings() []byte { 310 | b := new(bytes.Buffer) 311 | b.WriteString("\x00\x00\x00\x00") // Zero means little-endian. 312 | b.WriteString("\x00\x00\x00\x00") // Serial number. 313 | writeUint32(b, uint32(len(xSettings))) 314 | for _, s := range xSettings { 315 | switch s.value.(type) { 316 | case int: 317 | b.WriteString("\x00\x00") 318 | case float64: 319 | b.WriteString("\x00\x00") 320 | case string: 321 | b.WriteString("\x01\x00") 322 | default: 323 | log.Fatalf("unsupported XSettings type %T", s.value) 324 | } 325 | writeUint16(b, uint16(len(s.name))) 326 | b.WriteString(s.name) 327 | if x := len(s.name) % 4; x != 0 { 328 | b.WriteString("\x00\x00\x00\x00"[:4-x]) // Padding. 329 | } 330 | b.WriteString("\x00\x00\x00\x00") // Serial number. 331 | switch v := s.value.(type) { 332 | case int: 333 | writeUint32(b, uint32(v)) 334 | case float64: 335 | writeUint32(b, uint32(v)) 336 | case string: 337 | writeUint32(b, uint32(len(v))) 338 | b.WriteString(v) 339 | if x := len(v) % 4; x != 0 { 340 | b.WriteString("\x00\x00\x00\x00"[:4-x]) // Padding. 341 | } 342 | } 343 | } 344 | return b.Bytes() 345 | } 346 | 347 | func writeUint16(b *bytes.Buffer, u uint16) { 348 | b.WriteByte(byte(u >> 0)) 349 | b.WriteByte(byte(u >> 8)) 350 | } 351 | 352 | func writeUint32(b *bytes.Buffer, u uint32) { 353 | b.WriteByte(byte(u >> 0)) 354 | b.WriteByte(byte(u >> 8)) 355 | b.WriteByte(byte(u >> 16)) 356 | b.WriteByte(byte(u >> 24)) 357 | } 358 | 359 | func u32(b []byte) uint32 { 360 | return uint32(b[0])<<0 | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 361 | } 362 | --------------------------------------------------------------------------------