├── screenshots ├── 0.png └── 1.png ├── Gopkg.toml ├── .gitignore ├── Gopkg.lock ├── LICENCE ├── main.go ├── server ├── server.go └── handlers.go ├── wmutils ├── opt.go └── core.go ├── config.example.toml ├── config └── config.go ├── grid ├── utils.go ├── operations.go └── grid.go ├── view └── view.go ├── focus └── focus.go ├── client └── client.go └── README.md /screenshots/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callum-oakley/fgwm/HEAD/screenshots/0.png -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callum-oakley/fgwm/HEAD/screenshots/1.png -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | [[constraint]] 2 | name = "github.com/pelletier/go-toml" 3 | branch = "master" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 18 | .glide/ 19 | 20 | # End of https://www.gitignore.io/api/go 21 | 22 | vendor 23 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/pelletier/go-toml" 7 | packages = ["."] 8 | revision = "0131db6d737cfbbfb678f8b7d92e55e27ce46224" 9 | 10 | [solve-meta] 11 | analyzer-name = "dep" 12 | analyzer-version = 1 13 | inputs-digest = "24db4362d23306bb9f47b189e41eef52cee004fb8e8808c1cc1cc08843f20344" 14 | solver-name = "gps-cdcl" 15 | solver-version = 1 16 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Callum Oakley 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/callum-oakley/fgwm/client" 9 | "github.com/callum-oakley/fgwm/config" 10 | "github.com/callum-oakley/fgwm/server" 11 | ) 12 | 13 | func main() { 14 | if len(os.Args) >= 2 && os.Args[1] != "-c" && os.Args[1] != "--config" { 15 | client.Run(os.Args) 16 | return 17 | } 18 | configPath := fmt.Sprintf( 19 | "%v/.config/fgwm/config.toml", 20 | os.Getenv("HOME"), 21 | ) 22 | if len(os.Args) >= 3 && (os.Args[1] == "-c" || os.Args[1] == "--config") { 23 | configPath = os.Args[2] 24 | } 25 | options, err := config.Load(configPath) 26 | if err != nil { 27 | log.Fatalf("%v: %v\n", os.Args[0], err) 28 | } 29 | if err := server.Run(os.Args[0], options); err != nil { 30 | log.Fatalf("%v: %v\n", os.Args[0], err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "net/http" 9 | "net/rpc" 10 | 11 | "github.com/callum-oakley/fgwm/grid" 12 | ) 13 | 14 | type Server struct { 15 | name string 16 | grid *grid.Grid 17 | } 18 | 19 | func Run(name string, options *grid.Options) error { 20 | g, err := grid.New(options) 21 | if err != nil { 22 | return err 23 | } 24 | go func() { 25 | log.Fatal(g.WatchWindowEvents()) 26 | }() 27 | s := &Server{name, g} 28 | rpc.Register(s) 29 | rpc.HandleHTTP() 30 | listener, err := net.Listen("tcp", ":0") 31 | if err != nil { 32 | return err 33 | } 34 | port := listener.Addr().(*net.TCPAddr).Port 35 | err = ioutil.WriteFile("/tmp/fgwm-port", []byte(fmt.Sprint(port)), 0666) 36 | if err != nil { 37 | return err 38 | } 39 | log.Printf("%v: listening on localhost:%v\n", s.name, port) 40 | http.Serve(listener, nil) 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /wmutils/opt.go: -------------------------------------------------------------------------------- 1 | package wmutils 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | ) 7 | 8 | type EventType uint 9 | 10 | type Event struct { 11 | Type EventType 12 | WID WindowID 13 | } 14 | 15 | const ( 16 | CreateNotifyEvent EventType = 16 + iota 17 | DestroyNotifyEvent 18 | UnmapNotifyEvent 19 | MapNotifyEvent 20 | ) 21 | 22 | // this doesn't do any cleanup yet... 23 | func WatchEvents() <-chan Event { 24 | evChan := make(chan Event) 25 | // might want to use CommandContext to kill this and clean up when done 26 | cmd := exec.Command("wew") 27 | stdout, err := cmd.StdoutPipe() 28 | if err != nil { 29 | close(evChan) 30 | return evChan 31 | } 32 | if err := cmd.Start(); err != nil { 33 | close(evChan) 34 | return evChan 35 | } 36 | go func() { 37 | for { 38 | var ev Event 39 | _, err := fmt.Fscanf(stdout, "%v:%v", &ev.Type, &ev.WID) 40 | if err != nil { 41 | close(evChan) 42 | break 43 | } 44 | isIgnored, err := IsIgnored(ev.WID) 45 | if err != nil { 46 | close(evChan) 47 | break 48 | } 49 | if !isIgnored { 50 | evChan <- ev 51 | } 52 | } 53 | }() 54 | return evChan 55 | } 56 | -------------------------------------------------------------------------------- /config.example.toml: -------------------------------------------------------------------------------- 1 | # Colours for the borders of focussed and unfocussed windows -- in rgb hex. 2 | focussed_colour = 0xd8dee9 # off white 3 | unfocussed_colour = 0x65737e # greyish 4 | 5 | # The width of the border around each window -- in pixels. 6 | border = 5 7 | 8 | # The empty padding around each cell of the grid (width and height can be 9 | # defined separately if you want different horizonal space than vertical) -- in 10 | # pixels. 11 | pad = { width = 10, height = 10 } 12 | 13 | # The minimum margin to leave at edge of each side of the screen (in addition 14 | # to window padding) -- in pixels. You might want a larger margin at one side 15 | # than the others in order to accomodate a bar or panel. 16 | margins = { top = 10, bottom = 10, left = 10, right = 10 } 17 | 18 | # The granularity with which to divide the screen. 24x24 works well for thirds 19 | # and halves, with nice centering and resizing. 20 | grid_size = { width = 24, height = 24 } 21 | 22 | # The view in which to begin. Depending on your keybindings you'll probably 23 | # want this to be 0 or 1, but any integer is valid. 24 | initial_view = 1 25 | 26 | # The time for which a window must be focussed before it is considered "most 27 | # recently used" -- in milliseconds. 28 | focus_timeout_ms = 500 29 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/pelletier/go-toml" 5 | "os" 6 | "time" 7 | 8 | "github.com/callum-oakley/fgwm/grid" 9 | "github.com/callum-oakley/fgwm/wmutils" 10 | ) 11 | 12 | func Load(path string) (*grid.Options, error) { 13 | c, err := toml.LoadFile(path) 14 | if os.IsNotExist(err) { 15 | c = &toml.Tree{} // Will just revert to defaults for everything. 16 | } else if err != nil { 17 | return nil, err 18 | } 19 | return &grid.Options{ 20 | Border: wmutils.Pixels(c.GetDefault("border", int64(5)).(int64)), 21 | Margins: grid.Margins{ 22 | Top: wmutils.Pixels( 23 | c.GetDefault("margins.top", int64(10)).(int64), 24 | ), 25 | Bottom: wmutils.Pixels( 26 | c.GetDefault("margins.bottom", int64(10)).(int64), 27 | ), 28 | Left: wmutils.Pixels( 29 | c.GetDefault("margins.left", int64(10)).(int64), 30 | ), 31 | Right: wmutils.Pixels( 32 | c.GetDefault("margins.right", int64(10)).(int64), 33 | ), 34 | }, 35 | Pad: wmutils.Size{ 36 | wmutils.Pixels(c.GetDefault("pad.width", int64(10)).(int64)), 37 | wmutils.Pixels(c.GetDefault("pad.height", int64(10)).(int64)), 38 | }, 39 | Size: grid.Size{ 40 | int(c.GetDefault("grid_size.width", int64(24)).(int64)), 41 | int(c.GetDefault("grid_size.height", int64(24)).(int64)), 42 | }, 43 | InitialView: int(c.GetDefault("initial_view", int64(1)).(int64)), 44 | FocusTimeout: time.Duration( 45 | c.GetDefault("focus_timeout_ms", int64(500)).(int64), 46 | ) * time.Millisecond, 47 | FocussedColour: wmutils.Colour( 48 | c.GetDefault("focussed_colour", int64(0xd8dee9)).(int64), 49 | ), 50 | UnfocussedColour: wmutils.Colour( 51 | c.GetDefault("unfocussed_colour", int64(0x65737e)).(int64), 52 | ), 53 | }, nil 54 | } 55 | -------------------------------------------------------------------------------- /server/handlers.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/callum-oakley/fgwm/grid" 5 | "log" 6 | ) 7 | 8 | func (s *Server) Snap(struct{}, *struct{}) error { 9 | err := s.grid.Snap() 10 | if err != nil { 11 | log.Printf("%v: %v\n", s.name, err) 12 | } 13 | return err 14 | } 15 | 16 | func (s *Server) Center(struct{}, *struct{}) error { 17 | err := s.grid.Center() 18 | if err != nil { 19 | log.Printf("%v: %v\n", s.name, err) 20 | } 21 | return err 22 | } 23 | 24 | func (s *Server) Fullscreen(struct{}, *struct{}) error { 25 | err := s.grid.Fullscreen() 26 | if err != nil { 27 | log.Printf("%v: %v\n", s.name, err) 28 | } 29 | return err 30 | } 31 | 32 | func (s *Server) Kill(struct{}, *struct{}) error { 33 | err := s.grid.Kill() 34 | if err != nil { 35 | log.Printf("%v: %v\n", s.name, err) 36 | } 37 | return err 38 | } 39 | 40 | func (s *Server) Move(diff grid.Size, _ *struct{}) error { 41 | err := s.grid.Move(diff) 42 | if err != nil { 43 | log.Printf("%v: %v\n", s.name, err) 44 | } 45 | return err 46 | } 47 | 48 | func (s *Server) Grow(diff grid.Size, _ *struct{}) error { 49 | err := s.grid.Grow(diff) 50 | if err != nil { 51 | log.Printf("%v: %v\n", s.name, err) 52 | } 53 | return err 54 | } 55 | 56 | func (s *Server) Throw(direction grid.Direction, _ *struct{}) error { 57 | err := s.grid.Throw(direction) 58 | if err != nil { 59 | log.Printf("%v: %v\n", s.name, err) 60 | } 61 | return err 62 | } 63 | 64 | func (s *Server) Spread(direction grid.Direction, _ *struct{}) error { 65 | err := s.grid.Spread(direction) 66 | if err != nil { 67 | log.Printf("%v: %v\n", s.name, err) 68 | } 69 | return err 70 | } 71 | 72 | func (s *Server) Focus(strategy grid.FocusStrategy, _ *struct{}) error { 73 | err := s.grid.Focus(strategy) 74 | if err != nil { 75 | log.Printf("%v: %v\n", s.name, err) 76 | } 77 | return err 78 | } 79 | 80 | func (s *Server) Teleport(rectangle grid.Rectangle, _ *struct{}) error { 81 | err := s.grid.Teleport(rectangle) 82 | if err != nil { 83 | log.Printf("%v: %v\n", s.name, err) 84 | } 85 | return err 86 | } 87 | 88 | func (s *Server) ViewInclude(n int, _ *struct{}) error { 89 | err := s.grid.ViewInclude(n) 90 | if err != nil { 91 | log.Printf("%v: %v\n", s.name, err) 92 | } 93 | return err 94 | } 95 | 96 | func (s *Server) ViewSet(n int, _ *struct{}) error { 97 | err := s.grid.ViewSet(n) 98 | if err != nil { 99 | log.Printf("%v: %v\n", s.name, err) 100 | } 101 | return err 102 | } 103 | -------------------------------------------------------------------------------- /grid/utils.go: -------------------------------------------------------------------------------- 1 | package grid 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/callum-oakley/fgwm/wmutils" 7 | ) 8 | 9 | func (g *Grid) getRectangle(wid wmutils.WindowID) (Rectangle, error) { 10 | pPos, pSize, err := wmutils.GetAttributes(wid) 11 | if err != nil { 12 | return Rectangle{}, fmt.Errorf( 13 | "error getting rectangle for wid %v: %v", 14 | wid, 15 | err, 16 | ) 17 | } 18 | return Rectangle{ 19 | g.closestPoint(pPos.Offset(g.pad.Scale(-1))), 20 | g.closestPoint(pPos.Offset(pSize.Add( 21 | g.pad.Add(wmutils.Size{g.border, g.border}), 22 | ))), 23 | }, nil 24 | } 25 | 26 | func (g *Grid) closestPoint(p wmutils.Position) Position { 27 | return Position{ 28 | X: round((float64(p.X - g.margins.Left)) / float64(g.cell.W)), 29 | Y: round((float64(p.Y - g.margins.Top)) / float64(g.cell.H)), 30 | } 31 | } 32 | 33 | func round(x float64) int { 34 | return int(x + 0.5) 35 | } 36 | 37 | func (g *Grid) pInGrid(p Position) bool { 38 | return 0 <= p.X && p.X <= g.size.W && 0 <= p.Y && p.Y <= g.size.H 39 | } 40 | 41 | func (g *Grid) inGrid(r Rectangle) bool { 42 | return g.pInGrid(r.TopLeft) && g.pInGrid(r.BottomRight) 43 | } 44 | 45 | func (g *Grid) pixelSize(size Size) wmutils.Size { 46 | return wmutils.Size{ 47 | W: wmutils.Pixels(size.W) * g.cell.W, 48 | H: wmutils.Pixels(size.H) * g.cell.H, 49 | } 50 | } 51 | 52 | func (g *Grid) pixelPosition(pos Position) wmutils.Position { 53 | return wmutils.Position{ 54 | X: g.margins.Left + wmutils.Pixels(pos.X)*g.cell.W, 55 | Y: g.margins.Top + wmutils.Pixels(pos.Y)*g.cell.H, 56 | } 57 | } 58 | 59 | func index(wids []wmutils.WindowID, wid wmutils.WindowID) (int, error) { 60 | for i := 0; i < len(wids); i++ { 61 | if wids[i] == wid { 62 | return i, nil 63 | } 64 | } 65 | return 0, fmt.Errorf("can't find %v in %v", wid, wids) 66 | } 67 | 68 | func (g *Grid) centerWID(wid wmutils.WindowID) error { 69 | center := Position{g.size.W / 2, g.size.H / 2} 70 | r, err := g.getRectangle(wid) 71 | if err != nil { 72 | return nil 73 | } 74 | size := r.Size() 75 | offset := Size{size.W / 2, size.H / 2} 76 | return g.teleportWID(wid, Rectangle{ 77 | center.Offset(offset.Scale(-1)), 78 | center.Offset(offset), 79 | }) 80 | } 81 | 82 | func (g *Grid) teleportWID(wid wmutils.WindowID, r Rectangle) error { 83 | g.view.Unfullscreen(wid) 84 | if !g.inGrid(r) || !r.Valid() { 85 | return nil 86 | } 87 | return wmutils.Teleport( 88 | wid, 89 | g.pixelPosition(r.TopLeft).Offset(g.pad), 90 | g.pixelSize(r.Size()).Add( 91 | g.pad.Add(wmutils.Size{g.border, g.border}).Scale(-2), 92 | ), 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /view/view.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import "github.com/callum-oakley/fgwm/wmutils" 4 | 5 | type View interface { 6 | // Register wid with the current view 7 | Register(wid wmutils.WindowID) 8 | // Unregister wid from the current view 9 | Unregister(wid wmutils.WindowID) error 10 | // Unregister wid from all views 11 | UnregisterAll(wid wmutils.WindowID) 12 | // true if wid is registered in any view 13 | IsRegistered(wid wmutils.WindowID) bool 14 | // Include wid in view n 15 | Include(wid wmutils.WindowID, n int) 16 | // Set the view to n 17 | Set(n int) error 18 | // Toggle fullsceen for wid 19 | Fullscreen(wid wmutils.WindowID) error 20 | // Mark wid as not fullscreen 21 | Unfullscreen(wid wmutils.WindowID) error 22 | } 23 | 24 | type windowState struct { 25 | position wmutils.Position 26 | size wmutils.Size 27 | fullscreen bool 28 | } 29 | 30 | type view struct { 31 | screen wmutils.Size 32 | border wmutils.Pixels 33 | current int 34 | views map[int]map[wmutils.WindowID]*windowState 35 | } 36 | 37 | func New(screen wmutils.Size, border wmutils.Pixels, start int) (View, error) { 38 | v := view{ 39 | screen: screen, 40 | border: border, 41 | current: start, 42 | views: map[int]map[wmutils.WindowID]*windowState{ 43 | start: map[wmutils.WindowID]*windowState{}, 44 | }, 45 | } 46 | wids, err := wmutils.List() 47 | if err != nil { 48 | return nil, err 49 | } 50 | for wid := range wids { 51 | v.Register(wid) 52 | } 53 | return &v, nil 54 | } 55 | 56 | func (v *view) Register(wid wmutils.WindowID) { 57 | v.Include(wid, v.current) 58 | } 59 | 60 | func (v *view) Unregister(wid wmutils.WindowID) error { 61 | delete(v.views[v.current], wid) 62 | return wmutils.Unmap(wid) 63 | } 64 | 65 | func (v *view) UnregisterAll(wid wmutils.WindowID) { 66 | for _, wids := range v.views { 67 | delete(wids, wid) 68 | } 69 | } 70 | 71 | func (v *view) IsRegistered(wid wmutils.WindowID) bool { 72 | for _, wids := range v.views { 73 | if _, ok := wids[wid]; ok { 74 | return true 75 | } 76 | } 77 | return false 78 | } 79 | 80 | func (v *view) Include(wid wmutils.WindowID, n int) { 81 | if _, ok := v.views[n]; !ok { 82 | v.views[n] = map[wmutils.WindowID]*windowState{} 83 | } 84 | if _, ok := v.views[n][wid]; !ok { 85 | v.views[n][wid] = nil 86 | } 87 | } 88 | 89 | func (v *view) Set(n int) error { 90 | for wid := range v.views[v.current] { 91 | if err := wmutils.Unmap(wid); err != nil { 92 | return err 93 | } 94 | if err := v.save(wid); err != nil { 95 | return err 96 | } 97 | } 98 | v.current = n 99 | for wid := range v.views[v.current] { 100 | if err := v.restore(wid); err != nil { 101 | return err 102 | } 103 | if err := wmutils.Map(wid); err != nil { 104 | return err 105 | } 106 | } 107 | return nil 108 | } 109 | 110 | func (v *view) Fullscreen(wid wmutils.WindowID) error { 111 | if ws := v.views[v.current][wid]; ws != nil && ws.fullscreen { 112 | ws.fullscreen = false 113 | return v.restore(wid) 114 | } 115 | if err := v.save(wid); err != nil { 116 | return err 117 | } 118 | v.views[v.current][wid].fullscreen = true 119 | if err := wmutils.SetBorderWidth(wid, 0); err != nil { 120 | return err 121 | } 122 | return wmutils.Teleport(wid, wmutils.Position{}, v.screen) 123 | } 124 | 125 | func (v *view) Unfullscreen(wid wmutils.WindowID) error { 126 | if ws := v.views[v.current][wid]; ws != nil && ws.fullscreen { 127 | ws.fullscreen = false 128 | if err := wmutils.SetBorderWidth(wid, v.border); err != nil { 129 | return err 130 | } 131 | } 132 | return nil 133 | } 134 | 135 | func (v *view) save(wid wmutils.WindowID) error { 136 | if ws := v.views[v.current][wid]; ws != nil && ws.fullscreen { 137 | return nil 138 | } 139 | position, size, err := wmutils.GetAttributes(wid) 140 | if err != nil { 141 | return err 142 | } 143 | v.views[v.current][wid] = &windowState{position, size, false} 144 | return nil 145 | } 146 | 147 | func (v *view) restore(wid wmutils.WindowID) error { 148 | ws := v.views[v.current][wid] 149 | if ws == nil { 150 | return nil 151 | } 152 | if ws.fullscreen { 153 | if err := wmutils.SetBorderWidth(wid, 0); err != nil { 154 | return err 155 | } 156 | return wmutils.Teleport(wid, wmutils.Position{}, v.screen) 157 | } 158 | if err := wmutils.SetBorderWidth(wid, v.border); err != nil { 159 | return err 160 | } 161 | return wmutils.Teleport(wid, ws.position, ws.size) 162 | } 163 | -------------------------------------------------------------------------------- /focus/focus.go: -------------------------------------------------------------------------------- 1 | package focus 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/callum-oakley/fgwm/wmutils" 9 | ) 10 | 11 | type Focus interface { 12 | Register(wid wmutils.WindowID) error 13 | Unregister(wid wmutils.WindowID) error 14 | Get() (wmutils.WindowID, error) 15 | Set(wid wmutils.WindowID) error 16 | Unset(wid wmutils.WindowID) error 17 | Next() error 18 | Prev() error 19 | Top() error 20 | } 21 | 22 | type focus struct { 23 | mux sync.Mutex 24 | wids []wmutils.WindowID 25 | i int 26 | timer *time.Timer 27 | timeout time.Duration 28 | focussedColour, unfocussedColour wmutils.Colour 29 | } 30 | 31 | func New( 32 | timeout time.Duration, 33 | focussedColour, unfocussedColour wmutils.Colour, 34 | ) (Focus, error) { 35 | f := focus{ 36 | timeout: timeout, 37 | focussedColour: focussedColour, 38 | unfocussedColour: unfocussedColour, 39 | } 40 | f.timer = time.AfterFunc(f.timeout, f.update) 41 | wids, err := wmutils.List() 42 | if err != nil { 43 | return nil, err 44 | } 45 | for wid := range wids { 46 | if err := f.Register(wid); err != nil { 47 | return nil, err 48 | } 49 | } 50 | return &f, nil 51 | } 52 | 53 | func (f *focus) update() { 54 | wid, err := f.Get() 55 | if err != nil { 56 | return 57 | } 58 | f.mux.Lock() 59 | defer f.mux.Unlock() 60 | for j := f.i; j > 0; j-- { 61 | f.wids[j] = f.wids[j-1] 62 | } 63 | f.i = 0 64 | f.wids[0] = wid 65 | } 66 | 67 | func (f *focus) Get() (wmutils.WindowID, error) { 68 | f.mux.Lock() 69 | defer f.mux.Unlock() 70 | if f.i >= len(f.wids) { 71 | return 0, fmt.Errorf( 72 | "index is %v but we only have %v wids!", 73 | f.i, 74 | len(f.wids), 75 | ) 76 | } 77 | return f.wids[f.i], nil 78 | } 79 | 80 | func (f *focus) Register(wid wmutils.WindowID) error { 81 | f.mux.Lock() 82 | if index(wid, f.wids) < 0 { 83 | f.wids = append([]wmutils.WindowID{wid}, f.wids...) 84 | f.i = 0 85 | f.mux.Unlock() 86 | return f.Set(wid) 87 | } 88 | f.mux.Unlock() 89 | return nil 90 | } 91 | 92 | func (f *focus) Unregister(wid wmutils.WindowID) error { 93 | f.mux.Lock() 94 | if i := index(wid, f.wids); i >= 0 { 95 | f.wids = append(f.wids[:i], f.wids[i+1:]...) 96 | f.mux.Unlock() 97 | return f.Top() 98 | } 99 | f.mux.Unlock() 100 | return nil 101 | } 102 | 103 | func (f *focus) Set(wid wmutils.WindowID) error { 104 | f.mux.Lock() 105 | defer f.mux.Unlock() 106 | f.timer.Stop() 107 | for j := 0; j < len(f.wids); j++ { 108 | if j != f.i { 109 | err := wmutils.SetBorderColour(f.wids[j], f.unfocussedColour) 110 | if err != nil { 111 | return err 112 | } 113 | } 114 | } 115 | if err := wmutils.Focus(wid); err != nil { 116 | return err 117 | } 118 | if err := wmutils.Raise(wid); err != nil { 119 | return err 120 | } 121 | if err := wmutils.SetBorderColour(wid, f.focussedColour); err != nil { 122 | return err 123 | } 124 | f.timer.Reset(f.timeout) 125 | return nil 126 | } 127 | 128 | func (f *focus) Unset(wid wmutils.WindowID) error { 129 | w, err := f.Get() 130 | if err != nil || w == wid { 131 | return f.Top() 132 | } 133 | return nil 134 | } 135 | 136 | func (f *focus) Top() error { 137 | if len(f.wids) == 0 { 138 | return nil 139 | } 140 | visible, err := wmutils.List() 141 | if err != nil { 142 | return err 143 | } 144 | f.mux.Lock() 145 | for f.i = 0; f.i < len(f.wids); f.i++ { 146 | if visible[f.wids[f.i]] { 147 | f.mux.Unlock() 148 | return f.Set(f.wids[f.i]) 149 | } 150 | } 151 | f.mux.Unlock() 152 | return nil 153 | } 154 | 155 | func (f *focus) focusFunc(g func(int) int) error { 156 | if len(f.wids) == 0 { 157 | return nil 158 | } 159 | visible, err := wmutils.List() 160 | if err != nil { 161 | return err 162 | } 163 | f.mux.Lock() 164 | for j := 0; j == 0 || !visible[f.wids[f.i]]; j++ { 165 | f.i = g(f.i) 166 | if j == len(f.wids) { 167 | f.mux.Unlock() 168 | return nil 169 | } 170 | } 171 | f.mux.Unlock() 172 | return f.Set(f.wids[f.i]) 173 | } 174 | 175 | func (f *focus) Next() error { 176 | return f.focusFunc(func(i int) int { 177 | return (i + 1) % len(f.wids) 178 | }) 179 | } 180 | 181 | func (f *focus) Prev() error { 182 | return f.focusFunc(func(i int) int { 183 | return (i + len(f.wids) - 1) % len(f.wids) 184 | }) 185 | } 186 | 187 | func index(wid wmutils.WindowID, wids []wmutils.WindowID) int { 188 | for i, w := range wids { 189 | if w == wid { 190 | return i 191 | } 192 | } 193 | return -1 194 | } 195 | -------------------------------------------------------------------------------- /grid/operations.go: -------------------------------------------------------------------------------- 1 | package grid 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/callum-oakley/fgwm/wmutils" 7 | ) 8 | 9 | func (g *Grid) Focus(strategy FocusStrategy) error { 10 | g.mux.Lock() 11 | defer g.mux.Unlock() 12 | switch strategy { 13 | case Next: 14 | return g.focus.Next() 15 | case Prev: 16 | return g.focus.Prev() 17 | default: 18 | return fmt.Errorf("unsupported focus strategy '%v'", strategy) 19 | } 20 | } 21 | 22 | func (g *Grid) ViewInclude(n int) error { 23 | g.mux.Lock() 24 | defer g.mux.Unlock() 25 | wid, err := g.focus.Get() 26 | if err != nil { 27 | return err 28 | } 29 | g.view.Include(wid, n) 30 | return nil 31 | } 32 | 33 | func (g *Grid) ViewSet(n int) error { 34 | g.mux.Lock() 35 | defer g.mux.Unlock() 36 | if err := g.view.Set(n); err != nil { 37 | return err 38 | } 39 | return g.focus.Top() 40 | } 41 | 42 | func (g *Grid) Snap() error { 43 | return g.Move(Size{0, 0}) 44 | } 45 | 46 | func (g *Grid) Center() error { 47 | g.mux.Lock() 48 | defer g.mux.Unlock() 49 | wid, err := g.focus.Get() 50 | if err != nil { 51 | return err 52 | } 53 | return g.centerWID(wid) 54 | } 55 | 56 | func (g *Grid) Fullscreen() error { 57 | g.mux.Lock() 58 | defer g.mux.Unlock() 59 | wid, err := g.focus.Get() 60 | if err != nil { 61 | return err 62 | } 63 | return g.view.Fullscreen(wid) 64 | } 65 | 66 | func (g *Grid) Kill() error { 67 | g.mux.Lock() 68 | defer g.mux.Unlock() 69 | wid, err := g.focus.Get() 70 | if err != nil { 71 | return err 72 | } 73 | if err := g.view.Unregister(wid); err != nil { 74 | return err 75 | } 76 | if g.view.IsRegistered(wid) { 77 | return nil 78 | } 79 | return wmutils.Kill(wid) 80 | } 81 | 82 | func (g *Grid) Move(diff Size) error { 83 | g.mux.Lock() 84 | defer g.mux.Unlock() 85 | wid, err := g.focus.Get() 86 | if err != nil { 87 | return err 88 | } 89 | r, err := g.getRectangle(wid) 90 | if err != nil { 91 | return err 92 | } 93 | return g.teleportWID(wid, r.Offset(diff)) 94 | } 95 | 96 | func (g *Grid) Grow(diff Size) error { 97 | g.mux.Lock() 98 | defer g.mux.Unlock() 99 | wid, err := g.focus.Get() 100 | if err != nil { 101 | return err 102 | } 103 | r, err := g.getRectangle(wid) 104 | if err != nil { 105 | return err 106 | } 107 | if rg := r.Grow(diff); g.inGrid(rg) { 108 | return g.teleportWID(wid, rg) 109 | } 110 | if rg := r.Grow(diff).Offset(diff); g.inGrid(rg) { 111 | return g.teleportWID(wid, rg) 112 | } 113 | if rg := r.Grow(diff).Offset(diff.Scale(-1)); g.inGrid(rg) { 114 | return g.teleportWID(wid, rg) 115 | } 116 | return nil 117 | } 118 | 119 | func (g *Grid) Throw(direction Direction) error { 120 | g.mux.Lock() 121 | defer g.mux.Unlock() 122 | wid, err := g.focus.Get() 123 | if err != nil { 124 | return err 125 | } 126 | r, err := g.getRectangle(wid) 127 | if err != nil { 128 | return err 129 | } 130 | size := r.Size() 131 | switch direction { 132 | case Left: 133 | return g.teleportWID(wid, Rectangle{ 134 | Position{0, r.TopLeft.Y}, 135 | Position{size.W, r.BottomRight.Y}, 136 | }) 137 | case Right: 138 | return g.teleportWID(wid, Rectangle{ 139 | Position{g.size.W - size.W, r.TopLeft.Y}, 140 | Position{g.size.W, r.BottomRight.Y}, 141 | }) 142 | case Up: 143 | return g.teleportWID(wid, Rectangle{ 144 | Position{r.TopLeft.X, 0}, 145 | Position{r.BottomRight.X, size.H}, 146 | }) 147 | case Down: 148 | return g.teleportWID(wid, Rectangle{ 149 | Position{r.TopLeft.X, g.size.H - size.H}, 150 | Position{r.BottomRight.X, g.size.H}, 151 | }) 152 | default: 153 | return fmt.Errorf("unsupported direction '%v'", direction) 154 | } 155 | } 156 | 157 | func (g *Grid) Spread(direction Direction) error { 158 | g.mux.Lock() 159 | defer g.mux.Unlock() 160 | wid, err := g.focus.Get() 161 | if err != nil { 162 | return err 163 | } 164 | r, err := g.getRectangle(wid) 165 | if err != nil { 166 | return err 167 | } 168 | switch direction { 169 | case Left: 170 | return g.teleportWID(wid, Rectangle{ 171 | Position{0, r.TopLeft.Y}, 172 | r.BottomRight, 173 | }) 174 | case Right: 175 | return g.teleportWID(wid, Rectangle{ 176 | r.TopLeft, 177 | Position{g.size.W, r.BottomRight.Y}, 178 | }) 179 | case Up: 180 | return g.teleportWID(wid, Rectangle{ 181 | Position{r.TopLeft.X, 0}, 182 | r.BottomRight}, 183 | ) 184 | case Down: 185 | return g.teleportWID(wid, Rectangle{ 186 | r.TopLeft, 187 | Position{r.BottomRight.X, g.size.H}, 188 | }) 189 | default: 190 | return fmt.Errorf("Unsupported direction '%v'", direction) 191 | } 192 | } 193 | 194 | func (g *Grid) Teleport(r Rectangle) error { 195 | g.mux.Lock() 196 | defer g.mux.Unlock() 197 | wid, err := g.focus.Get() 198 | if err != nil { 199 | return err 200 | } 201 | return g.teleportWID(wid, r) 202 | } 203 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/rpc" 8 | "os" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/callum-oakley/fgwm/grid" 13 | ) 14 | 15 | type client struct { 16 | name string 17 | } 18 | 19 | func Run(args []string) { 20 | c := client{args[0]} 21 | port, err := ioutil.ReadFile("/tmp/fgwm-port") 22 | if err != nil { 23 | log.Fatalf("%v: %v", c.name, err) 24 | } 25 | conn, err := rpc.DialHTTP("tcp", fmt.Sprintf("localhost:%s", port)) 26 | if err != nil { 27 | log.Fatalf("%v: %v", c.name, err) 28 | } 29 | switch args[1] { 30 | case "snap": 31 | err = conn.Call("Server.Snap", c.noArgs(args[2:]), nil) 32 | case "center": 33 | err = conn.Call("Server.Center", c.noArgs(args[2:]), nil) 34 | case "fullscreen": 35 | err = conn.Call("Server.Fullscreen", c.noArgs(args[2:]), nil) 36 | case "kill": 37 | err = conn.Call("Server.Kill", c.noArgs(args[2:]), nil) 38 | case "move": 39 | err = conn.Call("Server.Move", c.sizeArg(args[2:]), nil) 40 | case "grow": 41 | err = conn.Call("Server.Grow", c.sizeArg(args[2:]), nil) 42 | case "throw": 43 | err = conn.Call("Server.Throw", c.directionArg(args[2:]), nil) 44 | case "spread": 45 | err = conn.Call("Server.Spread", c.directionArg(args[2:]), nil) 46 | case "focus": 47 | err = conn.Call("Server.Focus", c.focusStrategyArg(args[2:]), nil) 48 | case "view-include": 49 | err = conn.Call("Server.ViewInclude", c.intArg(args[2:]), nil) 50 | case "view-set": 51 | err = conn.Call("Server.ViewSet", c.intArg(args[2:]), nil) 52 | case "teleport": 53 | err = conn.Call("Server.Teleport", c.rectangleArg(args[2:]), nil) 54 | case "help": 55 | c.printHelpAndExit(args[2:]) 56 | default: 57 | c.printHelpAndExit(nil) 58 | } 59 | if err != nil { 60 | log.Fatalf("%v: %v", c.name, err) 61 | } 62 | } 63 | 64 | func (c client) noArgs(args []string) struct{} { 65 | if len(args) != 0 { 66 | c.printHelpAndExit(nil) 67 | } 68 | return struct{}{} 69 | } 70 | 71 | func (c client) intArg(args []string) int { 72 | if len(args) != 1 { 73 | c.printHelpAndExit(nil) 74 | } 75 | n, err := strconv.Atoi(args[0]) 76 | if err != nil { 77 | c.printHelpAndExit(nil) 78 | } 79 | return n 80 | } 81 | 82 | func (c client) sizeArg(args []string) grid.Size { 83 | if len(args) != 2 { 84 | c.printHelpAndExit(nil) 85 | } 86 | var size grid.Size 87 | var err error 88 | if size.W, err = strconv.Atoi(args[0]); err != nil { 89 | c.printHelpAndExit(nil) 90 | } 91 | if size.H, err = strconv.Atoi(args[1]); err != nil { 92 | c.printHelpAndExit(nil) 93 | } 94 | return size 95 | } 96 | 97 | func (c client) directionArg(args []string) grid.Direction { 98 | if len(args) != 1 { 99 | c.printHelpAndExit(nil) 100 | } 101 | var direction grid.Direction 102 | switch strings.ToLower(args[0]) { 103 | case "left", "l": 104 | direction = grid.Left 105 | case "right", "r": 106 | direction = grid.Right 107 | case "up", "u": 108 | direction = grid.Up 109 | case "down", "d": 110 | direction = grid.Down 111 | default: 112 | c.printHelpAndExit(nil) 113 | } 114 | return direction 115 | } 116 | 117 | func (c client) focusStrategyArg(args []string) grid.FocusStrategy { 118 | if len(args) != 1 { 119 | c.printHelpAndExit(nil) 120 | } 121 | var strategy grid.FocusStrategy 122 | switch strings.ToLower(args[0]) { 123 | case "next", "n": 124 | strategy = grid.Next 125 | case "prev", "p": 126 | strategy = grid.Prev 127 | default: 128 | c.printHelpAndExit(nil) 129 | } 130 | return strategy 131 | } 132 | 133 | func (c client) rectangleArg(args []string) grid.Rectangle { 134 | if len(args) != 4 { 135 | c.printHelpAndExit(nil) 136 | } 137 | var r grid.Rectangle 138 | var err error 139 | if r.TopLeft.X, err = strconv.Atoi(args[0]); err != nil { 140 | c.printHelpAndExit(nil) 141 | } 142 | if r.TopLeft.Y, err = strconv.Atoi(args[1]); err != nil { 143 | c.printHelpAndExit(nil) 144 | } 145 | if r.BottomRight.X, err = strconv.Atoi(args[2]); err != nil { 146 | c.printHelpAndExit(nil) 147 | } 148 | if r.BottomRight.Y, err = strconv.Atoi(args[3]); err != nil { 149 | c.printHelpAndExit(nil) 150 | } 151 | return r 152 | } 153 | 154 | func (c client) printHelpAndExit(args []string) { 155 | // TODO improve this (including command specific help) 156 | fmt.Printf("Usage:\n\n\t%v command [arguments]\n\n", c.name) 157 | fmt.Println("Where command is one of:\n") 158 | fmt.Println("\tcenter") 159 | fmt.Println("\tfocus") 160 | fmt.Println("\tfullscreen") 161 | fmt.Println("\tgrow") 162 | fmt.Println("\tkill") 163 | fmt.Println("\tmove") 164 | fmt.Println("\tsnap") 165 | fmt.Println("\tspread") 166 | fmt.Println("\tteleport") 167 | fmt.Println("\tthrow") 168 | fmt.Println("\tview-include") 169 | fmt.Println("\tview-set") 170 | fmt.Println("\thelp") 171 | fmt.Println("\nRunning with no commands starts the daemon:\n") 172 | fmt.Printf("\t %v [--config|-c FILE]\n", c.name) 173 | os.Exit(0) 174 | } 175 | -------------------------------------------------------------------------------- /grid/grid.go: -------------------------------------------------------------------------------- 1 | package grid 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | 8 | "github.com/callum-oakley/fgwm/focus" 9 | "github.com/callum-oakley/fgwm/view" 10 | "github.com/callum-oakley/fgwm/wmutils" 11 | ) 12 | 13 | type Direction int 14 | 15 | const ( 16 | Left Direction = iota 17 | Right 18 | Up 19 | Down 20 | ) 21 | 22 | type FocusStrategy int 23 | 24 | const ( 25 | Next FocusStrategy = iota 26 | Prev 27 | ) 28 | 29 | type Position struct { 30 | X, Y int 31 | } 32 | 33 | type Size struct { 34 | W, H int 35 | } 36 | 37 | type Rectangle struct { 38 | TopLeft, BottomRight Position 39 | } 40 | 41 | func (p Position) Offset(s Size) Position { 42 | return Position{p.X + s.W, p.Y + s.H} 43 | } 44 | 45 | func (a Position) Diff(b Position) Size { 46 | return Size{a.X - b.X, a.Y - b.Y} 47 | } 48 | 49 | func (a Size) Add(b Size) Size { 50 | return Size{a.W + b.W, a.H + b.H} 51 | } 52 | 53 | func (a Size) Scale(k int) Size { 54 | return Size{k * a.W, k * a.H} 55 | } 56 | 57 | func (r Rectangle) Size() Size { 58 | return r.BottomRight.Diff(r.TopLeft) 59 | } 60 | 61 | func (r Rectangle) Offset(s Size) Rectangle { 62 | return Rectangle{r.TopLeft.Offset(s), r.BottomRight.Offset(s)} 63 | } 64 | 65 | func (r Rectangle) Grow(s Size) Rectangle { 66 | return Rectangle{r.TopLeft.Offset(s.Scale(-1)), r.BottomRight.Offset(s)} 67 | } 68 | 69 | func (r Rectangle) Valid() bool { 70 | return r.TopLeft.X < r.BottomRight.X && r.TopLeft.Y < r.BottomRight.Y 71 | } 72 | 73 | type Margins struct { 74 | Top, Bottom, Left, Right wmutils.Pixels 75 | } 76 | 77 | type Grid struct { 78 | // size of the screen 79 | screen wmutils.Size 80 | // margin at each edge of the screen 81 | margins Margins 82 | // padding around cells 83 | pad wmutils.Size 84 | // border around cells 85 | border wmutils.Pixels 86 | // size of each cell, including pad and border but excluding margin 87 | cell wmutils.Size 88 | // the pixel locations of the cell boundaries 89 | points map[Position]wmutils.Position 90 | // the size of the grid in cells 91 | size Size 92 | focus focus.Focus 93 | view view.View 94 | mux sync.Mutex 95 | } 96 | 97 | // The sizes that define the grid layout are made up as follows (bd is border). 98 | // The Y direction is similar. 99 | // 100 | // | <-------------------------------- screen -------------------------------> | 101 | // | | | | | | | | | | | | | 102 | // | margin | pad | bd | | bd | pad | pad | bd | | bd | pad | margin | 103 | // | | | | | | | | | | | | | 104 | // | | <--------- cell ---------> | <--------- cell ---------> | | 105 | // 106 | 107 | type Options struct { 108 | Border wmutils.Pixels 109 | Margins Margins 110 | Pad wmutils.Size 111 | Size Size 112 | InitialView int 113 | FocusTimeout time.Duration 114 | FocussedColour, UnfocussedColour wmutils.Colour 115 | } 116 | 117 | func New(opts *Options) (*Grid, error) { 118 | wid, err := wmutils.Root() 119 | if err != nil { 120 | return nil, err 121 | } 122 | _, screen, err := wmutils.GetAttributes(wid) 123 | if err != nil { 124 | return nil, err 125 | } 126 | cell := wmutils.Size{ 127 | W: (screen.W - opts.Margins.Left - opts.Margins.Right) / 128 | wmutils.Pixels(opts.Size.W), 129 | H: (screen.H - opts.Margins.Top - opts.Margins.Bottom) / 130 | wmutils.Pixels(opts.Size.H), 131 | } 132 | // Absorb any excess pixels resulting from rounding in to the margins 133 | extraMarginW := screen.W - wmutils.Pixels(opts.Size.W)*cell.W - 134 | opts.Margins.Left - opts.Margins.Right 135 | extraMarginH := screen.H - wmutils.Pixels(opts.Size.H)*cell.H - 136 | opts.Margins.Top - opts.Margins.Bottom 137 | margins := Margins{ 138 | Top: opts.Margins.Top + extraMarginH/2, 139 | Bottom: opts.Margins.Bottom + extraMarginH/2, 140 | Left: opts.Margins.Left + extraMarginW/2, 141 | Right: opts.Margins.Right + extraMarginW/2, 142 | } 143 | focus, err := focus.New( 144 | opts.FocusTimeout, 145 | opts.FocussedColour, 146 | opts.UnfocussedColour, 147 | ) 148 | if err != nil { 149 | return nil, err 150 | } 151 | view, err := view.New(screen, opts.Border, opts.InitialView) 152 | if err != nil { 153 | return nil, err 154 | } 155 | return &Grid{ 156 | screen: screen, 157 | margins: margins, 158 | border: opts.Border, 159 | pad: opts.Pad, 160 | cell: cell, 161 | size: opts.Size, 162 | focus: focus, 163 | view: view, 164 | }, nil 165 | } 166 | 167 | func (g *Grid) WatchWindowEvents() error { 168 | for ev := range wmutils.WatchEvents() { 169 | switch ev.Type { 170 | case wmutils.CreateNotifyEvent: 171 | // Wait for a tick so that the window's self imposed size has a 172 | // chance to settle 173 | time.Sleep(100 * time.Millisecond) 174 | g.mux.Lock() 175 | if err := g.centerWID(ev.WID); err != nil { 176 | return err 177 | } 178 | if err := wmutils.SetBorderWidth(ev.WID, g.border); err != nil { 179 | return err 180 | } 181 | g.mux.Unlock() 182 | case wmutils.DestroyNotifyEvent: 183 | g.mux.Lock() 184 | if err := g.focus.Unregister(ev.WID); err != nil { 185 | return err 186 | } 187 | g.view.UnregisterAll(ev.WID) 188 | g.mux.Unlock() 189 | case wmutils.UnmapNotifyEvent: 190 | g.mux.Lock() 191 | if err := g.focus.Unset(ev.WID); err != nil { 192 | return err 193 | } 194 | g.mux.Unlock() 195 | case wmutils.MapNotifyEvent: 196 | g.mux.Lock() 197 | if err := g.focus.Register(ev.WID); err != nil { 198 | return err 199 | } 200 | g.view.Register(ev.WID) 201 | g.mux.Unlock() 202 | } 203 | } 204 | return errors.New("Window event channel closed!") 205 | } 206 | -------------------------------------------------------------------------------- /wmutils/core.go: -------------------------------------------------------------------------------- 1 | // Package wmutils provides wrappers around https://github.com/wmutils 2 | package wmutils 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "os/exec" 8 | ) 9 | 10 | type WindowID uint 11 | 12 | func (wid WindowID) String() string { 13 | return fmt.Sprintf("0x%08x", uint(wid)) 14 | } 15 | 16 | type Colour uint 17 | 18 | func (colour Colour) String() string { 19 | return fmt.Sprintf("0x%06x", uint(colour)) 20 | } 21 | 22 | type Pixels int 23 | 24 | type Position struct { 25 | X, Y Pixels 26 | } 27 | 28 | type Size struct { 29 | W, H Pixels 30 | } 31 | 32 | func (p Position) Offset(s Size) Position { 33 | return Position{p.X + s.W, p.Y + s.H} 34 | } 35 | 36 | func (a Size) Add(b Size) Size { 37 | return Size{a.W + b.W, a.H + b.H} 38 | } 39 | 40 | func (a Size) Scale(k Pixels) Size { 41 | return Size{k * a.W, k * a.H} 42 | } 43 | 44 | // Focussed returns the WindowID of the currently focussed window. Wraps pfw. 45 | func Focussed() (WindowID, error) { 46 | return fetchWID(exec.Command("pfw")) 47 | } 48 | 49 | // List returns the IDs of the child windows of the root (excluding invisible 50 | // or ignored windows). Wraps lsw. 51 | func List() (map[WindowID]bool, error) { 52 | wids := map[WindowID]bool{} 53 | cmd := exec.Command("lsw") 54 | stdout, err := cmd.StdoutPipe() 55 | if err != nil { 56 | return nil, err 57 | } 58 | if err := cmd.Start(); err != nil { 59 | return nil, err 60 | } 61 | for { 62 | var wid WindowID 63 | _, err := fmt.Fscanf(stdout, "%v", &wid) 64 | if err == io.EOF { 65 | break 66 | } else if err != nil { 67 | return nil, err 68 | } 69 | wids[wid] = true 70 | } 71 | if err := cmd.Wait(); err != nil { 72 | return nil, err 73 | } 74 | return wids, nil 75 | } 76 | 77 | // Root gets the window ID of the root window. Wraps lsw -r. 78 | func Root() (WindowID, error) { 79 | return fetchWID(exec.Command("lsw", "-r")) 80 | } 81 | 82 | // Focus sets the keyboard input focus to the window with the given ID if it 83 | // exists and is viewable. Wraps wtf. 84 | func Focus(wid WindowID) error { 85 | return exec.Command("wtf", wid.String()).Run() 86 | } 87 | 88 | // Kills the window with the given ID. Wraps killw -p. 89 | func Kill(wid WindowID) error { 90 | return exec.Command("killw", "-p", wid.String()).Run() 91 | } 92 | 93 | // Teleports the window with given ID to the given position, and resizes it to 94 | // the given size. Wraps wtp. 95 | func Teleport(wid WindowID, pos Position, size Size) error { 96 | return exec.Command( 97 | "wtp", 98 | fmt.Sprint(pos.X), 99 | fmt.Sprint(pos.Y), 100 | fmt.Sprint(size.W), 101 | fmt.Sprint(size.H), 102 | wid.String(), 103 | ).Run() 104 | } 105 | 106 | // Raises the window with the given ID to the top of the stacking order. Wraps 107 | // chwso. 108 | func Raise(wid WindowID) error { 109 | return exec.Command("chwso", "-r", wid.String()).Run() 110 | } 111 | 112 | // SetBorderWidth sets the width of the border for the window with the given 113 | // ID. Wraps chwb -s. 114 | func SetBorderWidth(wid WindowID, width Pixels) error { 115 | return exec.Command( 116 | "chwb", 117 | "-s", 118 | fmt.Sprintf("%v", width), 119 | wid.String(), 120 | ).Run() 121 | } 122 | 123 | // SetBorderColour sets the colour of the border for the window with the given 124 | // ID. Wraps chwb -c. 125 | func SetBorderColour(wid WindowID, colour Colour) error { 126 | return exec.Command("chwb", "-c", colour.String(), wid.String()).Run() 127 | } 128 | 129 | // Map (show) the window with the given ID. Wraps mapw -m. 130 | func Map(wid WindowID) error { 131 | return exec.Command("mapw", "-m", wid.String()).Run() 132 | } 133 | 134 | // Unmap (hide) the window with the given ID. Wraps mapw -u. 135 | func Unmap(wid WindowID) error { 136 | return exec.Command("mapw", "-u", wid.String()).Run() 137 | } 138 | 139 | // Toggle the visibility of the window with the given ID. Wraps mapw -t. 140 | func Toggle(wid WindowID) error { 141 | return exec.Command("mapw", "-t", wid.String()).Run() 142 | } 143 | 144 | // IsIgnored returns true if and only if the window with the given ID has the 145 | // override_redirect attribute set. Wraps wattr o. 146 | func IsIgnored(wid WindowID) (bool, error) { 147 | return exitStatusOk(exec.Command("wattr", "o", wid.String())) 148 | } 149 | 150 | // Exists returns true if there is a window with the given ID, false otherwise. 151 | // Wraps wattr. 152 | func Exists(wid WindowID) (bool, error) { 153 | return exitStatusOk(exec.Command("wattr", wid.String())) 154 | } 155 | 156 | // GetAttributes returns the size and position of the window with the given ID. 157 | // Wraps wattr xywh. 158 | func GetAttributes(wid WindowID) (Position, Size, error) { 159 | var pos Position 160 | var size Size 161 | cmd := exec.Command("wattr", "xywh", wid.String()) 162 | stdout, err := cmd.StdoutPipe() 163 | if err != nil { 164 | return pos, size, err 165 | } 166 | if err := cmd.Start(); err != nil { 167 | return pos, size, err 168 | } 169 | if _, err := fmt.Fscanf( 170 | stdout, 171 | "%v %v %v %v", 172 | &pos.X, 173 | &pos.Y, 174 | &size.W, 175 | &size.H, 176 | ); err != nil { 177 | return pos, size, err 178 | } 179 | if err := cmd.Wait(); err != nil { 180 | return pos, size, err 181 | } 182 | return pos, size, nil 183 | } 184 | 185 | func exitStatusOk(cmd *exec.Cmd) (bool, error) { 186 | err := cmd.Run() 187 | if err != nil { 188 | switch err.(type) { 189 | case *exec.ExitError: 190 | return false, nil 191 | default: 192 | return false, err 193 | } 194 | } 195 | return true, nil 196 | } 197 | 198 | func fetchWID(cmd *exec.Cmd) (WindowID, error) { 199 | var wid WindowID 200 | stdout, err := cmd.StdoutPipe() 201 | if err != nil { 202 | return 0, err 203 | } 204 | if err := cmd.Start(); err != nil { 205 | return 0, err 206 | } 207 | if _, err := fmt.Fscanf(stdout, "%v", &wid); err != nil { 208 | return 0, err 209 | } 210 | if err := cmd.Wait(); err != nil { 211 | return 0, err 212 | } 213 | return wid, nil 214 | } 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fgwm (floating grid window manager) 2 | 3 | *fgwm* (pronounced "fugwum") sits somewhere between a floating and a tiling 4 | window manager. Movement and resizing is done manually, as in a floating WM, 5 | but every window always snaps perfectly to a (coarse) grid. Movements and 6 | resizes are always done in multiples of the width and height of a cell in this 7 | grid. This allows for the pixel perfect and efficient layouts of a tiling WM, 8 | with the flexibility and aesthetics of a floating WM. 9 | 10 | ## Install 11 | 12 | *fgwm* relies on [wmutils][0] ([core][1] and [opt][2]) being installed and 13 | available in your path. 14 | 15 | ### From source 16 | 17 | You'll need a [go environment][3] and [dep][4]. 18 | 19 | $ go get -u github.com/callum-oakley/fgwm 20 | $ cd $GOPATH/src/github.com/callum-oakley/fgwm 21 | $ dep ensure 22 | $ go install 23 | 24 | ## Run 25 | 26 | The *fgwm* binary starts as a daemon when run with no commands, or acts as a 27 | client when provided with a command. Start the daemon from `.xinitrc` or 28 | similar: 29 | 30 | exec fgwm 31 | 32 | and then then issue commands like 33 | 34 | $ fgwm throw left 35 | $ fgwm move 2 0 36 | 37 | You'll want to bind these commands with a hotkey daemon such as [sxhkd][5]. 38 | See [here][6] for an example sxhkdrc. See [here][7] for a description of 39 | available commands. 40 | 41 | ## Configure 42 | 43 | By default *fgwm* looks in `$HOME/.config/fgwm/config.toml` for a configuration 44 | file. See [here][8] for a documented example config. Pass an alternative path 45 | with the `--config` or `-c` flags. 46 | 47 | exec fgwm -c CONFIG_PATH 48 | 49 | ## Commands 50 | 51 | The following commands act on the currently focussed window unless otherwise 52 | stated. 53 | 54 | fgwm center 55 | 56 | Center the window on the screen. 57 | 58 | fgwm focus next 59 | 60 | Focus the next window in the stack. See [Focus][9]. 61 | 62 | fgwm focus prev 63 | 64 | Focus the previous window in the stack. See [Focus][9]. 65 | 66 | fgwm fullscreen 67 | 68 | Toggle the current window in or out of full screen (restores the pre-fullscreen 69 | position and size). 70 | 71 | fgwm grow x y 72 | 73 | Resize the window so that the top and bottom edges move away from the center by 74 | `x` cells each, and the left and right by `y` cells each. `x` or `y` negative 75 | causes the window to shrink. e.g. `fgwm grow 2 0` makes the window two cells 76 | wider in each direction, four cells wider over all. 77 | 78 | fgwm kill 79 | 80 | Close the window in the current view (see [Views][10]). 81 | 82 | fgwm move x y 83 | 84 | Move the window by `x` cells to the left, and `y` cells down. Negative 85 | arguments reverse the direction. e.g. `fgwm move -2 0` moves the window two 86 | cells to the left. 87 | 88 | fgwm snap 89 | 90 | Force the window to the grid if something has put it out of alignment. 91 | 92 | fgwm spread direction 93 | 94 | Where `direction` is one of `up`, `down`, `left`, or `right`. Resise the window 95 | so that the side indicated by the direction moves all the way to the edge of 96 | the screen. 97 | 98 | fgwm teleport a b c d 99 | 100 | Move and resize the window so that it occupies the rectangle with top left 101 | corner at `(a, b)`, and bottom left at, `(c, d)`. The top left corner is `(0, 102 | 0)`. e.g. `fgwm teleport 6 0 18 24` resizes the window to take up a half of the 103 | screen, and places it centrally, in a `24x24` grid. 104 | 105 | fgwm throw direction 106 | 107 | Where `direction` is one of `up`, `down`, `left`, or `right`. Moves the window 108 | in the given direction all the way to the edge of the screen. 109 | 110 | fgwm view-include n 111 | 112 | Includes the window in the view `n`. See [Views][10]. 113 | 114 | fgwm view-set n 115 | 116 | Sets the current view to `n`. See [Views][10]. 117 | 118 | fgwm help 119 | 120 | List available commands. 121 | 122 | ## Focus 123 | 124 | *fgwm* maintains a stack of the *most recently used* windows, and `fgwm focus 125 | next` focusses the next window in the stack (i.e. *the* most recently used 126 | window). A window isn't considered *used* until it has been focussed for a set 127 | amount of time (500ms by default, configurable with the `focus_timeout_ms` 128 | option), so calling `fgwm focus next` again before that timeout moves to the 129 | next window in the stack, and so on. When a window has been focussed for long 130 | enough to be considered *used* it is moved to the top of the stack, and your 131 | position in the stack is reset. 132 | 133 | This allows a single call of `fgwm focus next` to swap between your two most 134 | recently used windows, while multiple calls can be used to traverse the window 135 | stack as far back as you like. `fgwm focus prev` moves back up the stack, and 136 | is for situations where you accidentally focus past the window you wanted, so 137 | that you don't have to loop all the way back around. 138 | 139 | Focus changing only takes place within the currently active view (see below), 140 | windows in other views are ignored. 141 | 142 | ## Views 143 | 144 | *Views* are similar to *desktops*, *workspaces*, or *groups* found in most 145 | window managers. They have the following properties: 146 | 147 | - A view is denoted by a single integer. 148 | - Exactly one view is active at a time, the active view can be set to `n` with 149 | `fgwm view-set n`. 150 | - Windows can belong to any number of views, and have an independent position 151 | and size in each. To include a window in view `n`, use `fgwm view-include n`. 152 | 153 | When a window is created, it gets included in the currently active view. When 154 | including a window in another view, it initially has the same position and 155 | location in both, but henceforth can be moved and resized indepently. Killing a 156 | window which belongs to multiple views with `fgwm kill` only removes it from 157 | the current view. 158 | 159 | The initial view can be changed with the `initial_view` option (`1` by default). 160 | 161 | ## Screenshots 162 | 163 | A single centered window 164 | 165 | ![](screenshots/0.png) 166 | 167 | That same window as part of a different view 168 | 169 | ![](screenshots/1.png) 170 | 171 | [0]: https://github.com/wmutils 172 | [1]: https://github.com/wmutils/core 173 | [2]: https://github.com/wmutils/opt 174 | [3]: https://golang.org/doc/install 175 | [4]: https://github.com/golang/dep#setup 176 | [5]: https://github.com/baskerville/sxhkd 177 | [6]: https://github.com/callum-oakley/dots/blob/master/.config/sxhkd/sxhkdrc 178 | [7]: #commands 179 | [8]: https://github.com/callum-oakley/fgwm/blob/master/config.example.toml 180 | [9]: #focus 181 | [10]: #views 182 | --------------------------------------------------------------------------------