diff --git a/.gitignore b/.gitignore index e2fd324..5aaae21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .assets -dist \ No newline at end of file +cortile +dist diff --git a/README.md b/README.md index e7ea45b..dab6daf 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ Once enabled, the tiling manager will handle _resizing_ and _positioning_ of _ex - [x] Drag & drop window swap. - [x] Auto detection of panels. - [x] Multi monitor support. -- [x] Selective tiling areas. Support for **keyboard and mouse navigation** sets cortile apart from other tiling solutions. The _go_ implementation ensures a fast and responsive system, where _multiple layouts_, _keyboard shortcuts_, _drag & drop_ and _hot corner_ events simplify and speed up your daily work. @@ -184,7 +183,7 @@ cd cortile If necessary you can make local changes, then execute: ```bash -go build && go install +go install -ldflags="-X 'main.date=$(date --iso-8601=seconds)'" ``` @@ -198,8 +197,6 @@ $GOPATH/bin/cortile -v Special settings: - Use the `edge_margin` property to account for additional spaces. - e.g. for deskbar panels or conky infographics. -- Use the `edge_margin` property to enable tiling only for parts of the monitor. - - e.g. use a margin that is half the resolution of a large screen to tile only windows that are moved within a specified area. - Use the `window_slaves_max` property to limit the number of windows. - e.g. with one active master and `window_slaves_max = 2`, all windows following the third window are stacked behind the two slaves. @@ -219,9 +216,6 @@ In Xfce environments, they can be found under "Window Manager" > "Advanced" > "W If you encounter problems start the process with `cortile -vv`, which provides additional verbose outputs. A log file is created by default under `/tmp/cortile.log`. -Known limitations: -- Only the biggest monitor is used for tiling. - ## Credits [![credits](https://img.shields.io/github/contributors/leukipp/cortile)](#credits-) Based on [zentile](https://github.com/blrsn/zentile) from [Berin Larson](https://github.com/blrsn). diff --git a/common/corner.go b/common/corner.go index b9f64a8..43f3873 100644 --- a/common/corner.go +++ b/common/corner.go @@ -6,37 +6,45 @@ import ( ) type Corner struct { - Name string // Corner name used in config - Active bool // Mouse pointer is in this corner - Area xrect.Rect // Rectangle area of the corner section + Name string // Corner name used in config + Active bool // Mouse pointer is in this corner + ScreenNum uint // Screen number the corner is located + Area xrect.Rect // Rectangle area of the corner section } -func CreateCorner(name string, x int, y int, w int, h int) *Corner { +func CreateCorner(name string, screenNum uint, x int, y int, w int, h int) *Corner { return &Corner{ - Name: name, - Active: false, - Area: xrect.New(x, y, w, h), + Name: name, + ScreenNum: screenNum, + Area: xrect.New(x, y, w, h), + Active: false, } } func CreateCorners() []*Corner { - xw, yw, ww, hw := ScreenDimensions() - - // Corner dimensions - wcs, hcs := Config.EdgeCornerSize, Config.EdgeCornerSize - wcl, hcl := Config.EdgeCenterSize, Config.EdgeCenterSize - - // Define corners and positions - tl := CreateCorner("top_left", xw, yw, wcs, hcs) - tc := CreateCorner("top_center", (xw+ww)/2-wcl/2, yw, wcl, hcs) - tr := CreateCorner("top_right", xw+ww-wcs, yw, wcs, hcs) - cr := CreateCorner("center_right", xw+ww-wcs, (yw+hw)/2-hcl/2, wcs, hcl) - br := CreateCorner("bottom_right", xw+ww-wcs, yw+hw-hcs, wcs, hcs) - bc := CreateCorner("bottom_center", (xw+ww)/2-wcl/2, yw+hw-hcs, wcl, hcl) - bl := CreateCorner("bottom_left", xw, yw+hw-hcs, wcs, hcs) - cl := CreateCorner("center_left", xw, (yw+hw)/2-hcl/2, wcs, hcl) - - return []*Corner{tl, tc, tr, cr, br, bc, bl, cl} + corners := []*Corner{} + + for i, s := range ViewPorts.Screens { + xw, yw, ww, hw := s.Pieces() + + // Corner dimensions + wcs, hcs := Config.EdgeCornerSize, Config.EdgeCornerSize + wcl, hcl := Config.EdgeCenterSize, Config.EdgeCenterSize + + // Define corners and positions + tl := CreateCorner("top_left", uint(i), xw, yw, wcs, hcs) + tc := CreateCorner("top_center", uint(i), (xw+ww)/2-wcl/2, yw, wcl, hcs) + tr := CreateCorner("top_right", uint(i), xw+ww-wcs, yw, wcs, hcs) + cr := CreateCorner("center_right", uint(i), xw+ww-wcs, (yw+hw)/2-hcl/2, wcs, hcl) + br := CreateCorner("bottom_right", uint(i), xw+ww-wcs, yw+hw-hcs, wcs, hcs) + bc := CreateCorner("bottom_center", uint(i), (xw+ww)/2-wcl/2, yw+hw-hcs, wcl, hcl) + bl := CreateCorner("bottom_left", uint(i), xw, yw+hw-hcs, wcs, hcs) + cl := CreateCorner("center_left", uint(i), xw, (yw+hw)/2-hcl/2, wcs, hcl) + + corners = append(corners, []*Corner{tl, tc, tr, cr, br, bc, bl, cl}...) + } + + return corners } func (c *Corner) IsActive(p *xproto.QueryPointerReply) bool { @@ -46,13 +54,3 @@ func (c *Corner) IsActive(p *xproto.QueryPointerReply) bool { return c.Active } - -func IsInsideRect(p *xproto.QueryPointerReply, r xrect.Rect) bool { - x, y, w, h := r.Pieces() - - // Check if x and y are inside rectangle - xInRect := int(p.RootX) >= x && int(p.RootX) <= (x+w) - yInRect := int(p.RootY) >= y && int(p.RootY) <= (y+h) - - return xInRect && yInRect -} diff --git a/common/state.go b/common/root.go similarity index 58% rename from common/state.go rename to common/root.go index 90c5ddb..08a0292 100644 --- a/common/state.go +++ b/common/root.go @@ -16,14 +16,17 @@ import ( ) var ( - X *xgbutil.XUtil // X connection object - DeskCount uint // Number of desktop workspaces - CurrentDesk uint // Current desktop - ViewPorts Head // Physical monitors - Windows []xproto.Window // List of client windows - ActiveWindow xproto.Window // Current active window - Corners []*Corner // Corners for pointer events - Pointer *xproto.QueryPointerReply // Pointer position and state + X *xgbutil.XUtil // X connection object + DeskCount uint // Number of desktops + ScreenCount uint // Number of screens + CurrentDesk uint // Current desktop number + CurrentScreen uint // Current screen number + ViewPorts Head // Physical monitors + Windows []xproto.Window // List of client windows + ActiveWindow xproto.Window // Current active window + Corners []*Corner // Corners for pointer events + Pointer *xproto.QueryPointerReply // Pointer position and state + callbacks []func(string) // State event callback functions ) type Head struct { @@ -31,7 +34,7 @@ type Head struct { Desktops xinerama.Heads // Desktop size (workarea without panels) } -func InitState() { +func InitRoot() { var err error X := Connect() @@ -53,7 +56,6 @@ func InitState() { checkFatal(err) Corners = CreateCorners() - Pointer, _ = xproto.QueryPointer(X.Conn(), X.RootWin()).Reply() root.Listen(xproto.EventMaskPropertyChange) xevent.PropertyNotifyFun(stateUpdate).Connect(X, X.RootWin()) @@ -62,20 +64,20 @@ func InitState() { func Connect() *xgbutil.XUtil { var err error - // connect to X server + // Connect to X server X, err = xgbutil.NewConn() checkFatal(err) - // check ewmh compliance + // Check ewmh compliance _, err = ewmh.GetEwmhWM(X) if err != nil { - log.Fatal("Window manager is not ewmh complaint ", err) + log.Fatal("Window manager is not EWMH compliant ", err) } - // wait for client list availability + // Wait for client list availability i, j := 0, 100 for i < j { - _, err = ewmh.ClientListGet(X) + _, err = ewmh.ClientListStackingGet(X) if err == nil { break } @@ -83,6 +85,8 @@ func Connect() *xgbutil.XUtil { time.Sleep(100 * time.Millisecond) } + log.Info("Connected to X server") + return X } @@ -108,14 +112,14 @@ func ViewPortsGet(X *xgbutil.XUtil) (Head, error) { desktops := PhysicalHeadsGet(rGeom) // Adjust desktops geometry - clients, err := ewmh.ClientListGet(X) + clients, err := ewmh.ClientListStackingGet(X) for _, id := range clients { strut, err := ewmh.WmStrutPartialGet(X, id) if err != nil { continue } - // Apply in place struts to our desktops + // Apply in place struts to desktops xrect.ApplyStrut(desktops, uint(rGeom.Width()), uint(rGeom.Height()), strut.Left, strut.Right, strut.Top, strut.Bottom, strut.LeftStartY, strut.LeftEndY, @@ -124,22 +128,30 @@ func ViewPortsGet(X *xgbutil.XUtil) (Head, error) { strut.BottomStartX, strut.BottomEndX) } + // Update screen count + ScreenCount = uint(len(screens)) + log.Info("Screens ", screens) log.Info("Desktops ", desktops) return Head{Screens: screens, Desktops: desktops}, err } -func DesktopDimensions() (x, y, w, h int) { - for _, d := range ViewPorts.Desktops { - hx, hy, hw, hh := d.Pieces() +func ScreenNumGet(p *xproto.QueryPointerReply) uint { - // Use biggest head (monitor) as desktop area - if hw*hh > w*h { - x, y, w, h = hx, hy, hw, hh + // Check if point is inside screen rectangle + for screenNum, rect := range ViewPorts.Screens { + if IsInsideRect(p, rect) { + return uint(screenNum) } } + return 0 +} + +func DesktopDimensions(screenNum uint) (x, y, w, h int) { + x, y, w, h = ViewPorts.Desktops[screenNum].Pieces() + // Add desktop margin x += Config.EdgeMargin[3] y += Config.EdgeMargin[0] @@ -149,43 +161,33 @@ func DesktopDimensions() (x, y, w, h int) { return } -func ScreenDimensions() (x, y, w, h int) { - for _, s := range ViewPorts.Screens { - hx, hy, hw, hh := s.Pieces() - - // Use biggest head (monitor) as screen area - if hw*hh > w*h { - x, y, w, h = hx, hy, hw, hh - } - } - - return +func OnStateUpdate(fun func(string)) { + callbacks = append(callbacks, fun) } func stateUpdate(X *xgbutil.XUtil, e xevent.PropertyNotifyEvent) { var err error aname, _ := xprop.AtomName(X, e.Atom) - log.Trace("State event ", aname) + log.Info("State event ", aname) // Update common state variables - if aname == "_NET_NUMBER_OF_DESKTOPS" { + if IsInList(aname, []string{"_NET_NUMBER_OF_DESKTOPS"}) { DeskCount, err = ewmh.NumberOfDesktopsGet(X) - } else if aname == "_NET_CURRENT_DESKTOP" { + stateCallbacks(aname) + } else if IsInList(aname, []string{"_NET_CURRENT_DESKTOP"}) { CurrentDesk, err = ewmh.CurrentDesktopGet(X) - } else if aname == "_NET_DESKTOP_LAYOUT" { - ViewPorts, err = ViewPortsGet(X) - Corners = CreateCorners() - } else if aname == "_NET_DESKTOP_VIEWPORT" { - ViewPorts, err = ViewPortsGet(X) - Corners = CreateCorners() - } else if aname == "_NET_WORKAREA" { + stateCallbacks(aname) + } else if IsInList(aname, []string{"_NET_DESKTOP_LAYOUT", "_NET_DESKTOP_GEOMETRY", "_NET_DESKTOP_VIEWPORT", "_NET_WORKAREA"}) { ViewPorts, err = ViewPortsGet(X) Corners = CreateCorners() - } else if aname == "_NET_CLIENT_LIST" { - Windows, err = ewmh.ClientListGet(X) - } else if aname == "_NET_ACTIVE_WINDOW" { + stateCallbacks(aname) + } else if IsInList(aname, []string{"_NET_CLIENT_LIST_STACKING"}) { + Windows, err = ewmh.ClientListStackingGet(X) + stateCallbacks(aname) + } else if IsInList(aname, []string{"_NET_ACTIVE_WINDOW"}) { ActiveWindow, err = ewmh.ActiveWindowGet(X) + stateCallbacks(aname) } if err != nil { @@ -193,6 +195,12 @@ func stateUpdate(X *xgbutil.XUtil, e xevent.PropertyNotifyEvent) { } } +func stateCallbacks(aname string) { + for _, fun := range callbacks { + fun(aname) + } +} + func checkFatal(err error) { if err != nil { log.Fatal("Error on initialization ", err) diff --git a/common/utils.go b/common/utils.go new file mode 100644 index 0000000..7bfd9aa --- /dev/null +++ b/common/utils.go @@ -0,0 +1,44 @@ +package common + +import ( + "github.com/BurntSushi/xgb/xproto" + "github.com/BurntSushi/xgbutil/xgraphics" + "github.com/BurntSushi/xgbutil/xrect" + log "github.com/sirupsen/logrus" +) + +func IsInList(item string, items []string) bool { + for i := 0; i < len(items); i++ { + if items[i] == item { + return true + } + } + return false +} + +func IsInsideRect(p *xproto.QueryPointerReply, r xrect.Rect) bool { + x, y, w, h := r.Pieces() + + // Check if x and y are inside rectangle + xInRect := int(p.RootX) >= x && int(p.RootX) <= (x+w) + yInRect := int(p.RootY) >= y && int(p.RootY) <= (y+h) + + return xInRect && yInRect +} + +func GetColor(name string) xgraphics.BGRA { + rgba := Config.Colors[name] + + // Validate length + if len(rgba) != 4 { + log.Warn("Error obtaining color for ", name) + return xgraphics.BGRA{} + } + + return xgraphics.BGRA{ + R: uint8(rgba[0]), + G: uint8(rgba[1]), + B: uint8(rgba[2]), + A: uint8(rgba[3]), + } +} diff --git a/desktop/gui.go b/desktop/gui.go index 5778b60..db96d16 100644 --- a/desktop/gui.go +++ b/desktop/gui.go @@ -37,28 +37,26 @@ func ShowLayout(ws *Workspace) { return } - // Obtain layout infos - al := ws.ActiveLayout() - mg := al.GetManager() - name := al.GetName() - - // Calculate scaled desktop dimensions - dx, dy, dw, dh := common.DesktopDimensions() - _, _, width, height := scale(dx, dy, dw, dh) - - // Create an empty canvas image - bg := bgra("gui_background") - cv := xgraphics.New(common.X, image.Rect(0, 0, width+rectMargin, height+fontSize+2*fontMargin+2*rectMargin)) - cv.For(func(x int, y int) xgraphics.BGRA { return bg }) - // Wait for tiling events time.AfterFunc(100*time.Millisecond, func() { + al := ws.ActiveLayout() + mg := al.GetManager() + name := al.GetName() + + // Calculate scaled desktop dimensions + dx, dy, dw, dh := common.DesktopDimensions(common.CurrentScreen) + _, _, width, height := scale(dx, dy, dw, dh) + + // Create an empty canvas image + bg := common.GetColor("gui_background") + cv := xgraphics.New(common.X, image.Rect(0, 0, width+rectMargin, height+fontSize+2*fontMargin+2*rectMargin)) + cv.For(func(x int, y int) xgraphics.BGRA { return bg }) // Draw client rectangles drawClients(cv, mg, name) // Draw layout name - drawText(cv, name, bgra("gui_text"), cv.Rect.Dx()/2, cv.Rect.Dy()-fontSize-2*fontMargin-rectMargin) + drawText(cv, name, common.GetColor("gui_text"), cv.Rect.Dx()/2, cv.Rect.Dy()-fontSize-2*fontMargin-rectMargin) // Show the canvas graphics showGraphics(cv, time.Duration(common.Config.TilingGui)) @@ -80,11 +78,11 @@ func drawClients(cv *xgraphics.Image, mg *store.Manager, layout string) { if len(clients) == 0 { // Calculate scaled desktop dimensions - _, _, dw, dh := common.DesktopDimensions() + _, _, dw, dh := common.DesktopDimensions(common.CurrentScreen) x, y, width, height := scale(0, 0, dw, dh) // Draw client rectangle onto canvas - color := bgra("gui_client_slave") + color := common.GetColor("gui_client_slave") rect := &image.Uniform{color} drawImage(cv, rect, color, x+rectMargin, y+rectMargin, x+width, y+height) @@ -96,7 +94,7 @@ func drawClients(cv *xgraphics.Image, mg *store.Manager, layout string) { // Calculate scaled client dimensions cx, cy, cw, ch := c.OuterGeometry() - dx, dy, _, _ := common.DesktopDimensions() + dx, dy, _, _ := common.DesktopDimensions(common.CurrentScreen) x, y, width, height := scale(cx-dx, cy-dy, cw, ch) // Calculate icon size @@ -110,9 +108,9 @@ func drawClients(cv *xgraphics.Image, mg *store.Manager, layout string) { iconSize /= 2 // Obtain rectangle color - color := bgra("gui_client_slave") + color := common.GetColor("gui_client_slave") if mg.IsMaster(c) || layout == "fullscreen" { - color = bgra("gui_client_master") + color = common.GetColor("gui_client_master") } // Draw client rectangle onto canvas @@ -152,7 +150,7 @@ func showGraphics(img *xgraphics.Image, duration time.Duration) *xwindow.Window } // Calculate window dimensions - dx, dy, dw, dh := common.DesktopDimensions() + dx, dy, dw, dh := common.DesktopDimensions(common.CurrentScreen) w, h := img.Rect.Dx(), img.Rect.Dy() x, y := dx+dw/2-w/2, dy+dh/2-h/2 @@ -213,23 +211,6 @@ func showGraphics(img *xgraphics.Image, duration time.Duration) *xwindow.Window return win } -func bgra(name string) xgraphics.BGRA { - rgba := common.Config.Colors[name] - - // Validate length - if len(rgba) != 4 { - log.Warn("Error obtaining color for ", name) - return xgraphics.BGRA{} - } - - return xgraphics.BGRA{ - R: uint8(rgba[0]), - G: uint8(rgba[1]), - B: uint8(rgba[2]), - A: uint8(rgba[3]), - } -} - func scale(x, y, w, h int) (sx, sy, sw, sh int) { s := 10 sx, sy, sw, sh = x/s, y/s, w/s, h/s diff --git a/desktop/tracker.go b/desktop/tracker.go index 7293cc6..a52ef86 100644 --- a/desktop/tracker.go +++ b/desktop/tracker.go @@ -23,7 +23,12 @@ var ( type Tracker struct { Clients map[xproto.Window]*store.Client // List of clients that are being tracked - Workspaces map[uint]*Workspace // List of workspaces used + Workspaces map[Location]*Workspace // List of workspaces per location +} + +type Location struct { + DeskNum uint // Workspace desktop number + ScreenNum uint // Workspace screen number } type Swap struct { @@ -31,51 +36,49 @@ type Swap struct { Client2 *store.Client // Stores hovered client } -func CreateTracker(ws map[uint]*Workspace) *Tracker { +func CreateTracker(ws map[Location]*Workspace) *Tracker { tr := Tracker{ Clients: make(map[xproto.Window]*store.Client), Workspaces: ws, } + // Attach to state update events + common.OnStateUpdate(tr.onStateUpdate) + // Populate clients - xevent.PropertyNotifyFun(tr.handleWorkspaceUpdates).Connect(common.X, common.X.RootWin()) tr.Update() // Startup tiling if common.Config.TilingEnabled { - for _, ws := range ws { - ws.Tile() - } - ShowLayout(tr.Workspaces[common.CurrentDesk]) + ShowLayout(tr.ActiveWorkspace()) } return &tr } func (tr *Tracker) Update() { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return } - // Add trackable windows + // Map trackable windows + trackable := make(map[xproto.Window]bool) for _, w := range common.Windows { - if tr.isTrackable(w) { - tr.trackWindow(w) - } + trackable[w] = tr.isTrackable(w) } - // If window is tracked, but not in client list - for w1 := range tr.Clients { - trackable := false - for _, w2 := range common.Windows { - if w1 == w2 { - trackable = tr.isTrackable(w1) - break - } + // Remove untrackable windows + for w := range tr.Clients { + if !trackable[w] { + tr.untrackWindow(w) } - if !trackable { - tr.untrackWindow(w1) + } + + // Add trackable windows + for _, w := range common.Windows { + if trackable[w] { + tr.trackWindow(w) } } @@ -83,6 +86,25 @@ func (tr *Tracker) Update() { ws.Tile() } +func (tr *Tracker) Reset() { + + // Reset client list + for w := range tr.Clients { + tr.untrackWindow(w) + } + + // Reset workspaces + tr.Workspaces = CreateWorkspaces() +} + +func (tr *Tracker) ActiveWorkspace() *Workspace { + return tr.Workspaces[Location{DeskNum: common.CurrentDesk, ScreenNum: common.CurrentScreen}] +} + +func (tr *Tracker) ClientWorkspace(c *store.Client) *Workspace { + return tr.Workspaces[Location{DeskNum: c.Latest.DeskNum, ScreenNum: c.Latest.ScreenNum}] +} + func (tr *Tracker) trackWindow(w xproto.Window) { if tr.isTracked(w) { return @@ -91,7 +113,7 @@ func (tr *Tracker) trackWindow(w xproto.Window) { // Add new client c := store.CreateClient(w) tr.Clients[c.Win.Id] = c - ws := tr.Workspaces[c.Latest.DeskNum] + ws := tr.ClientWorkspace(c) ws.AddClient(c) // Attach handlers and tile @@ -100,32 +122,35 @@ func (tr *Tracker) trackWindow(w xproto.Window) { } func (tr *Tracker) untrackWindow(w xproto.Window) { - if tr.isTracked(w) { - c := tr.Clients[w] - ws := tr.Workspaces[c.Latest.DeskNum] + if !tr.isTracked(w) { + return + } - // Detach events - xevent.Detach(common.X, w) + // Client and workspace + c := tr.Clients[w] + ws := tr.ClientWorkspace(c) - // Restore client - c.Restore() + // Detach events + xevent.Detach(common.X, w) - // Remove client - ws.RemoveClient(c) - delete(tr.Clients, w) - } + // Restore client + c.Restore() + + // Remove client + ws.RemoveClient(c) + delete(tr.Clients, w) } func (tr *Tracker) tileWorkspace(c *store.Client) { - ws := tr.Workspaces[c.Latest.DeskNum] + ws := tr.ClientWorkspace(c) // Tile workspace ws.Tile() } func (tr *Tracker) handleResizeClient(c *store.Client) { - ws := tr.Workspaces[c.Latest.DeskNum] - if !ws.IsEnabled() || store.IsMaximized(c.Win.Id) { + ws := tr.ClientWorkspace(c) + if !tr.isTracked(c.Win.Id) || !ws.IsEnabled() || store.IsMaximized(c.Win.Id) { return } @@ -141,13 +166,14 @@ func (tr *Tracker) handleResizeClient(c *store.Client) { cx, cy, cw, ch := cGeom.Pieces() // Check size changes + moved := math.Abs(float64(cx-px)) > 0.0 || math.Abs(float64(cy-py)) > 0.0 resized := math.Abs(float64(cw-pw)) > 0.0 || math.Abs(float64(ch-ph)) > 0.0 directions := &store.Directions{Top: cy != py, Right: cx == px && cw != pw, Bottom: cy == py && ch != ph, Left: cx != px} // Check window lifetime lifetime := time.Since(c.Created) added := lifetime < 1000*time.Millisecond - initialized := (math.Abs(float64(cx-px)) > 0.0 || math.Abs(float64(cy-py)) > 0.0) && added + initialized := moved && added if resized || initialized { @@ -162,8 +188,8 @@ func (tr *Tracker) handleResizeClient(c *store.Client) { } func (tr *Tracker) handleMoveClient(c *store.Client) { - ws := tr.Workspaces[c.Latest.DeskNum] - if !ws.IsEnabled() || store.IsMaximized(c.Win.Id) { + ws := tr.ClientWorkspace(c) + if !tr.isTracked(c.Win.Id) || !ws.IsEnabled() || store.IsMaximized(c.Win.Id) { return } @@ -183,12 +209,12 @@ func (tr *Tracker) handleMoveClient(c *store.Client) { moved := math.Abs(float64(cx-px)) > 0.0 || math.Abs(float64(cy-py)) > 0.0 resized := math.Abs(float64(cw-pw)) > 0.0 || math.Abs(float64(ch-ph)) > 0.0 - if active && (moved && !resized) { + if active && moved && !resized { mg := ws.ActiveLayout().GetManager() - clients := mg.Clients(false) + swap = nil // Check if pointer hovers other client - swap = nil + clients := mg.Clients(false) for _, co := range clients { if c.Win.Id == co.Win.Id { continue @@ -200,23 +226,25 @@ func (tr *Tracker) handleMoveClient(c *store.Client) { Client1: c, Client2: co, } - break + return } } } } func (tr *Tracker) handleSwapClient(c *store.Client) { - ws := tr.Workspaces[c.Latest.DeskNum] - if !ws.IsEnabled() || store.IsMaximized(c.Win.Id) { + ws := tr.ClientWorkspace(c) + if !tr.isTracked(c.Win.Id) || !ws.IsEnabled() || store.IsMaximized(c.Win.Id) { return } if swap != nil { mg := ws.ActiveLayout().GetManager() - // Swap clients + // Swap clients on same desktop and screen mg.SwapClient(swap.Client1, swap.Client2) + + // Reset swap swap = nil } @@ -225,12 +253,15 @@ func (tr *Tracker) handleSwapClient(c *store.Client) { } func (tr *Tracker) handleMaximizedClient(c *store.Client) { - states, _ := ewmh.WmStateGet(common.X, c.Win.Id) + if !tr.isTracked(c.Win.Id) { + return + } // Client maximized + states, _ := ewmh.WmStateGet(common.X, c.Win.Id) for _, state := range states { if strings.Contains(state, "_NET_WM_STATE_MAXIMIZED") { - ws := tr.Workspaces[c.Latest.DeskNum] + ws := tr.ClientWorkspace(c) if !ws.IsEnabled() { return } @@ -241,8 +272,9 @@ func (tr *Tracker) handleMaximizedClient(c *store.Client) { ws.SetLayout(uint(i)) } } - c.Activate() tr.tileWorkspace(c) + c.Activate() + ShowLayout(ws) break } @@ -250,12 +282,15 @@ func (tr *Tracker) handleMaximizedClient(c *store.Client) { } func (tr *Tracker) handleMinimizedClient(c *store.Client) { - states, _ := ewmh.WmStateGet(common.X, c.Win.Id) + if !tr.isTracked(c.Win.Id) { + return + } // Client minimized + states, _ := ewmh.WmStateGet(common.X, c.Win.Id) for _, state := range states { if state == "_NET_WM_STATE_HIDDEN" { - ws := tr.Workspaces[c.Latest.DeskNum] + ws := tr.ClientWorkspace(c) if !ws.IsEnabled() { return } @@ -268,46 +303,58 @@ func (tr *Tracker) handleMinimizedClient(c *store.Client) { } } -func (tr *Tracker) handleDesktopChange(c *store.Client) { +func (tr *Tracker) handleWorkspaceChange(c *store.Client) { // Remove client from current workspace - tr.Workspaces[c.Latest.DeskNum].RemoveClient(c) - if tr.Workspaces[c.Latest.DeskNum].IsEnabled() { + ws := tr.ClientWorkspace(c) + ws.RemoveClient(c) + if ws.IsEnabled() { tr.tileWorkspace(c) } - // Update client desktop - success := c.Update() - if !success { + // Update client desktop and screen + if !tr.isTrackable(c.Win.Id) { return } + c.Update() // Add client to new workspace - tr.Workspaces[c.Latest.DeskNum].AddClient(c) - if tr.Workspaces[c.Latest.DeskNum].IsEnabled() { + ws = tr.ClientWorkspace(c) + ws.AddClient(c) + if ws.IsEnabled() { tr.tileWorkspace(c) } else { c.Restore() } } -func (tr *Tracker) handleWorkspaceUpdates(X *xgbutil.XUtil, ev xevent.PropertyNotifyEvent) { - aname, _ := xprop.AtomName(common.X, ev.Atom) - log.Trace("Workspace update event ", aname) +func (tr *Tracker) handleDesktopChange(c *store.Client) { + if !tr.isTracked(c.Win.Id) { + return + } + tr.handleWorkspaceChange(c) +} + +func (tr *Tracker) handleScreenChange(c *store.Client) { + if !tr.isTracked(c.Win.Id) || c.Latest.ScreenNum == common.CurrentScreen { + return + } + tr.handleWorkspaceChange(c) +} - clientAdded := aname == "_NET_CLIENT_LIST" || aname == "_NET_CLIENT_LIST_STACKING" - workspaceChanged := aname == "_NET_DESKTOP_LAYOUT" || aname == "_NET_DESKTOP_VIEWPORT" || aname == "_NET_WORKAREA" +func (tr *Tracker) onStateUpdate(aname string) { + clientAdded := common.IsInList(aname, []string{"_NET_CLIENT_LIST_STACKING"}) + workspacesChanged := common.DeskCount*common.ScreenCount != uint(len(tr.Workspaces)) + viewportChanged := common.IsInList(aname, []string{"_NET_NUMBER_OF_DESKTOPS", "_NET_DESKTOP_LAYOUT", "_NET_DESKTOP_GEOMETRY", "_NET_DESKTOP_VIEWPORT", "_NET_WORKAREA"}) - // Client added or workspace changed - if clientAdded || workspaceChanged { - tr.Update() + // Number of desktops or screens changed + if viewportChanged && workspacesChanged { + tr.Reset() + } - // Re-update as some wm minimize to outside - time.AfterFunc(200*time.Millisecond, func() { - if !tr.isTracked(common.ActiveWindow) { - tr.Update() - } - }) + // Viewport changed or client added + if viewportChanged || clientAdded { + tr.Update() } } @@ -318,13 +365,9 @@ func (tr *Tracker) attachHandlers(c *store.Client) { xevent.ConfigureNotifyFun(func(x *xgbutil.XUtil, ev xevent.ConfigureNotifyEvent) { log.Trace("Client structure event [", c.Latest.Class, "]") - // Handle structure changes - if tr.isTrackable(c.Win.Id) { - tr.handleResizeClient(c) - tr.handleMoveClient(c) - } else { - tr.Update() - } + // Handle structure events + tr.handleResizeClient(c) + tr.handleMoveClient(c) }).Connect(common.X, c.Win.Id) // Attach property events @@ -332,16 +375,12 @@ func (tr *Tracker) attachHandlers(c *store.Client) { aname, _ := xprop.AtomName(common.X, ev.Atom) log.Trace("Client property event ", aname, " [", c.Latest.Class, "]") - // Handle property changes - if tr.isTrackable(c.Win.Id) { - if aname == "_NET_WM_STATE" { - tr.handleMaximizedClient(c) - tr.handleMinimizedClient(c) - } else if aname == "_NET_WM_DESKTOP" { - tr.handleDesktopChange(c) - } - } else { - tr.Update() + // Handle property events + if aname == "_NET_WM_STATE" { + tr.handleMaximizedClient(c) + tr.handleMinimizedClient(c) + } else if aname == "_NET_WM_DESKTOP" { + tr.handleDesktopChange(c) } }).Connect(common.X, c.Win.Id) @@ -349,12 +388,15 @@ func (tr *Tracker) attachHandlers(c *store.Client) { xevent.FocusInFun(func(x *xgbutil.XUtil, ev xevent.FocusInEvent) { log.Trace("Client focus event [", c.Latest.Class, "]") - // Wait for structure changes - time.AfterFunc(200*time.Millisecond, func() { - if ev.Mode == xproto.NotifyModeUngrab { + // Handle ungrab events + if ev.Mode == xproto.NotifyModeUngrab { + tr.handleScreenChange(c) + + // Wait for structure events + time.AfterFunc(100*time.Millisecond, func() { tr.handleSwapClient(c) - } - }) + }) + } }).Connect(common.X, c.Win.Id) } @@ -364,5 +406,6 @@ func (tr *Tracker) isTracked(w xproto.Window) bool { } func (tr *Tracker) isTrackable(w xproto.Window) bool { - return store.IsInsideViewPort(w) && !store.IsIgnored(w) && !store.IsSpecial(w) + info := store.GetInfo(w) + return !store.IsSpecial(info) && !store.IsIgnored(info) } diff --git a/desktop/workspace.go b/desktop/workspace.go index a2704c4..65ca5af 100644 --- a/desktop/workspace.go +++ b/desktop/workspace.go @@ -14,38 +14,42 @@ type Workspace struct { ActiveLayoutNum uint // Active layout index } -func CreateWorkspaces() map[uint]*Workspace { - workspaces := make(map[uint]*Workspace) - - for i := uint(0); i < common.DeskCount; i++ { - - // Create layouts for each workspace - layouts := CreateLayouts(i) - ws := &Workspace{ - Layouts: layouts, - TilingEnabled: common.Config.TilingEnabled, - } +func CreateWorkspaces() map[Location]*Workspace { + workspaces := make(map[Location]*Workspace) + + for deskNum := uint(0); deskNum < common.DeskCount; deskNum++ { + for screenNum := uint(0); screenNum < common.ScreenCount; screenNum++ { + location := Location{DeskNum: deskNum, ScreenNum: screenNum} + + // Create layouts for each desktop and screen + layouts := CreateLayouts(location) + ws := &Workspace{ + Layouts: layouts, + TilingEnabled: common.Config.TilingEnabled, + } - // Activate default layout - for i, l := range layouts { - if l.GetName() == common.Config.TilingLayout { - ws.SetLayout(uint(i)) + // Activate default layout + for i, l := range layouts { + if l.GetName() == common.Config.TilingLayout { + ws.SetLayout(uint(i)) + } } - } - workspaces[i] = ws + // Map location to workspace + workspaces[location] = ws + } } return workspaces } -func CreateLayouts(deskNum uint) []Layout { +func CreateLayouts(l Location) []Layout { return []Layout{ - layout.CreateFullscreenLayout(deskNum), - layout.CreateVerticalLeftLayout(deskNum), - layout.CreateVerticalRightLayout(deskNum), - layout.CreateHorizontalTopLayout(deskNum), - layout.CreateHorizontalBottomLayout(deskNum), + layout.CreateFullscreenLayout(l.DeskNum, l.ScreenNum), + layout.CreateVerticalLeftLayout(l.DeskNum, l.ScreenNum), + layout.CreateVerticalRightLayout(l.DeskNum, l.ScreenNum), + layout.CreateHorizontalTopLayout(l.DeskNum, l.ScreenNum), + layout.CreateHorizontalBottomLayout(l.DeskNum, l.ScreenNum), } } @@ -77,10 +81,6 @@ func (ws *Workspace) UnTile() { } func (ws *Workspace) AddClient(c *store.Client) { - if c == nil { - return - } - log.Info("Add client for each layout [", c.Latest.Class, "]") // Add client to all layouts @@ -90,10 +90,6 @@ func (ws *Workspace) AddClient(c *store.Client) { } func (ws *Workspace) RemoveClient(c *store.Client) { - if c == nil { - return - } - log.Info("Remove client from each layout [", c.Latest.Class, "]") // Remove client from all layouts @@ -107,8 +103,5 @@ func (ws *Workspace) Enable(enable bool) { } func (ws *Workspace) IsEnabled() bool { - if ws == nil { - return false - } return ws.TilingEnabled } diff --git a/input/action.go b/input/action.go index 6072ef6..3641cd9 100644 --- a/input/action.go +++ b/input/action.go @@ -71,11 +71,11 @@ func Execute(a string, tr *desktop.Tracker) bool { // Notify socket if success { - type Action struct{ Workspace uint } + type Action struct{ Workspace, Screen uint } NotifySocket(Message[Action]{ Type: "Action", Name: a, - Data: Action{Workspace: common.CurrentDesk}, + Data: Action{Workspace: common.CurrentDesk, Screen: common.CurrentScreen}, }) } @@ -92,7 +92,7 @@ func Query(s string, tr *desktop.Tracker) bool { switch s { case "workspaces": - NotifySocket(Message[map[uint]*desktop.Workspace]{ + NotifySocket(Message[map[desktop.Location]*desktop.Workspace]{ Type: "State", Name: s, Data: tr.Workspaces, @@ -118,7 +118,7 @@ func Query(s string, tr *desktop.Tracker) bool { } func Tile(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() ws.Enable(true) tr.Update() @@ -128,7 +128,7 @@ func Tile(tr *desktop.Tracker) bool { } func UnTile(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -139,7 +139,7 @@ func UnTile(tr *desktop.Tracker) bool { } func SwitchLayout(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -151,7 +151,7 @@ func SwitchLayout(tr *desktop.Tracker) bool { } func FullscreenLayout(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -168,7 +168,7 @@ func FullscreenLayout(tr *desktop.Tracker) bool { } func VerticalLeftLayout(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -185,7 +185,7 @@ func VerticalLeftLayout(tr *desktop.Tracker) bool { } func VerticalRightLayout(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -202,7 +202,7 @@ func VerticalRightLayout(tr *desktop.Tracker) bool { } func HorizontalTopLayout(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -219,7 +219,7 @@ func HorizontalTopLayout(tr *desktop.Tracker) bool { } func HorizontalBottomLayout(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -236,18 +236,20 @@ func HorizontalBottomLayout(tr *desktop.Tracker) bool { } func MakeMaster(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } - ws.ActiveLayout().MakeMaster(tr.Clients[common.ActiveWindow]) - ws.Tile() - - return true + if c, ok := tr.Clients[common.ActiveWindow]; ok { + ws.ActiveLayout().MakeMaster(c) + ws.Tile() + return true + } + return false } func IncreaseMaster(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -260,7 +262,7 @@ func IncreaseMaster(tr *desktop.Tracker) bool { } func DecreaseMaster(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -273,7 +275,7 @@ func DecreaseMaster(tr *desktop.Tracker) bool { } func IncreaseSlave(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -286,7 +288,7 @@ func IncreaseSlave(tr *desktop.Tracker) bool { } func DecreaseSlave(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -299,7 +301,7 @@ func DecreaseSlave(tr *desktop.Tracker) bool { } func IncreaseProportion(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -310,7 +312,7 @@ func IncreaseProportion(tr *desktop.Tracker) bool { } func DecreaseProportion(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -321,7 +323,7 @@ func DecreaseProportion(tr *desktop.Tracker) bool { } func NextWindow(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } @@ -331,7 +333,7 @@ func NextWindow(tr *desktop.Tracker) bool { } func PreviousWindow(tr *desktop.Tracker) bool { - ws := tr.Workspaces[common.CurrentDesk] + ws := tr.ActiveWorkspace() if !ws.IsEnabled() { return false } diff --git a/input/mousebinding.go b/input/mousebinding.go index fba800f..3343ea8 100644 --- a/input/mousebinding.go +++ b/input/mousebinding.go @@ -16,8 +16,10 @@ func BindMouse(tr *desktop.Tracker) { poll(common.X, 50, func() { // Update pointer position - p, _ := xproto.QueryPointer(common.X.Conn(), common.X.RootWin()).Reply() - common.Pointer = p + common.Pointer, _ = xproto.QueryPointer(common.X.Conn(), common.X.RootWin()).Reply() + + // Update current screen + common.CurrentScreen = common.ScreenNumGet(common.Pointer) // Evaluate corner states for i := range common.Corners { diff --git a/layout/fullscreen.go b/layout/fullscreen.go index 4dbed9e..226fc74 100644 --- a/layout/fullscreen.go +++ b/layout/fullscreen.go @@ -14,9 +14,9 @@ type FullscreenLayout struct { Name string // Layout name } -func CreateFullscreenLayout(deskNum uint) *FullscreenLayout { +func CreateFullscreenLayout(deskNum uint, screenNum uint) *FullscreenLayout { return &FullscreenLayout{ - Manager: store.CreateManager(deskNum), + Manager: store.CreateManager(deskNum, screenNum), Name: "fullscreen", } } @@ -24,12 +24,12 @@ func CreateFullscreenLayout(deskNum uint) *FullscreenLayout { func (l *FullscreenLayout) Do() { clients := l.Clients(true) - dx, dy, dw, dh := common.DesktopDimensions() + dx, dy, dw, dh := common.DesktopDimensions(l.ScreenNum) gap := common.Config.WindowGapSize csize := len(clients) - log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.DeskNum, "]") + log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.DeskNum, "-", l.ScreenNum, "]") // Main area layout for _, c := range clients { @@ -37,7 +37,7 @@ func (l *FullscreenLayout) Do() { // Limit minimum dimensions minw := int(math.Round(float64(dw - 2*gap))) minh := int(math.Round(float64(dh - 2*gap))) - c.LimitDim(minw, minh) + c.LimitDimensions(minw, minh) // Move and resize client c.MoveResize(dx+gap, dy+gap, dw-2*gap, dh-2*gap) diff --git a/layout/horizontal.go b/layout/horizontal.go index c361746..96e1bc2 100644 --- a/layout/horizontal.go +++ b/layout/horizontal.go @@ -14,8 +14,8 @@ type HorizontalLayout struct { Name string // Layout name } -func CreateHorizontalTopLayout(deskNum uint) *HorizontalLayout { - manager := store.CreateManager(deskNum) +func CreateHorizontalTopLayout(deskNum uint, screenNum uint) *HorizontalLayout { + manager := store.CreateManager(deskNum, screenNum) manager.SetProportions(manager.Proportions.MasterSlave, common.Config.Proportion, 0, 1) return &HorizontalLayout{ @@ -24,8 +24,8 @@ func CreateHorizontalTopLayout(deskNum uint) *HorizontalLayout { } } -func CreateHorizontalBottomLayout(deskNum uint) *HorizontalLayout { - manager := store.CreateManager(deskNum) +func CreateHorizontalBottomLayout(deskNum uint, screenNum uint) *HorizontalLayout { + manager := store.CreateManager(deskNum, screenNum) manager.SetProportions(manager.Proportions.MasterSlave, common.Config.Proportion, 1, 0) return &HorizontalLayout{ @@ -37,7 +37,7 @@ func CreateHorizontalBottomLayout(deskNum uint) *HorizontalLayout { func (l *HorizontalLayout) Do() { clients := l.Clients(true) - dx, dy, dw, dh := common.DesktopDimensions() + dx, dy, dw, dh := common.DesktopDimensions(l.ScreenNum) gap := common.Config.WindowGapSize mmax := l.Masters.MaxAllowed @@ -52,7 +52,7 @@ func (l *HorizontalLayout) Do() { sy := my + mh sh := dh - mh - log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.DeskNum, "]") + log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.DeskNum, "-", l.ScreenNum, "]") // Swap values if master is on bottom if l.Name == "horizontal-bottom" && csize > mmax { @@ -92,7 +92,7 @@ func (l *HorizontalLayout) Do() { // Limit minimum dimensions minw := int(math.Round(float64(dw-(msize+1)*gap) * minpw)) minh := int(math.Round(float64(dh-2*gap) * minph)) - c.LimitDim(minw, minh) + c.LimitDimensions(minw, minh) // Move and resize master mp := l.Proportions.MasterMaster[i%msize] @@ -130,7 +130,7 @@ func (l *HorizontalLayout) Do() { // Limit minimum dimensions minw := int(math.Round(float64(dw-(ssize+1)*gap) * minpw)) minh := int(math.Round(float64(dh-2*gap) * minph)) - c.LimitDim(minw, minh) + c.LimitDimensions(minw, minh) // Move and resize slave sp := l.Proportions.SlaveSlave[i%ssize] @@ -146,7 +146,7 @@ func (l *HorizontalLayout) Do() { } func (l *HorizontalLayout) UpdateProportions(c *store.Client, d *store.Directions) { - _, _, dw, dh := common.DesktopDimensions() + _, _, dw, dh := common.DesktopDimensions(l.ScreenNum) _, _, cw, ch := c.OuterGeometry() gap := common.Config.WindowGapSize diff --git a/layout/vertical.go b/layout/vertical.go index b397291..516020d 100644 --- a/layout/vertical.go +++ b/layout/vertical.go @@ -14,8 +14,8 @@ type VerticalLayout struct { Name string // Layout name } -func CreateVerticalLeftLayout(deskNum uint) *VerticalLayout { - manager := store.CreateManager(deskNum) +func CreateVerticalLeftLayout(deskNum uint, screenNum uint) *VerticalLayout { + manager := store.CreateManager(deskNum, screenNum) manager.SetProportions(manager.Proportions.MasterSlave, common.Config.Proportion, 0, 1) return &VerticalLayout{ @@ -24,8 +24,8 @@ func CreateVerticalLeftLayout(deskNum uint) *VerticalLayout { } } -func CreateVerticalRightLayout(deskNum uint) *VerticalLayout { - manager := store.CreateManager(deskNum) +func CreateVerticalRightLayout(deskNum uint, screenNum uint) *VerticalLayout { + manager := store.CreateManager(deskNum, screenNum) manager.SetProportions(manager.Proportions.MasterSlave, common.Config.Proportion, 1, 0) return &VerticalLayout{ @@ -37,7 +37,7 @@ func CreateVerticalRightLayout(deskNum uint) *VerticalLayout { func (l *VerticalLayout) Do() { clients := l.Clients(true) - dx, dy, dw, dh := common.DesktopDimensions() + dx, dy, dw, dh := common.DesktopDimensions(l.ScreenNum) gap := common.Config.WindowGapSize mmax := l.Masters.MaxAllowed @@ -52,7 +52,7 @@ func (l *VerticalLayout) Do() { sx := mx + mw sw := dw - mw - log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.DeskNum, "]") + log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.DeskNum, "-", l.ScreenNum, "]") // Swap values if master is on right if l.Name == "vertical-right" && csize > mmax { @@ -92,7 +92,7 @@ func (l *VerticalLayout) Do() { // Limit minimum dimensions minw := int(math.Round(float64(dw-2*gap) * minpw)) minh := int(math.Round(float64(dh-(msize+1)*gap) * minph)) - c.LimitDim(minw, minh) + c.LimitDimensions(minw, minh) // Move and resize master mp := l.Proportions.MasterMaster[i%msize] @@ -130,7 +130,7 @@ func (l *VerticalLayout) Do() { // Limit minimum dimensions minw := int(math.Round(float64(dw-2*gap) * minpw)) minh := int(math.Round(float64(dh-(ssize+1)*gap) * minph)) - c.LimitDim(minw, minh) + c.LimitDimensions(minw, minh) // Move and resize slave sp := l.Proportions.SlaveSlave[i%ssize] @@ -146,7 +146,7 @@ func (l *VerticalLayout) Do() { } func (l *VerticalLayout) UpdateProportions(c *store.Client, d *store.Directions) { - _, _, dw, dh := common.DesktopDimensions() + _, _, dw, dh := common.DesktopDimensions(l.ScreenNum) _, _, cw, ch := c.OuterGeometry() gap := common.Config.WindowGapSize diff --git a/main.go b/main.go index 2390b75..131fbf4 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "runtime/debug" "syscall" "github.com/BurntSushi/xgbutil/xevent" @@ -34,11 +35,22 @@ func main() { defer InitLock().Close() InitLog() - // Init config and state + // Init config and root common.InitConfig(defaultConfig) - common.InitState() + common.InitRoot() - // Init workspace and tracker + // Run cortile + run() +} + +func run() { + defer func() { + if err := recover(); err != nil { + log.Fatal(fmt.Errorf("%s\n%s", err, debug.Stack())) + } + }() + + // Create workspaces and tracker workspaces := desktop.CreateWorkspaces() tracker := desktop.CreateTracker(workspaces) @@ -58,6 +70,7 @@ func InitLock() *os.File { fmt.Println(fmt.Errorf("cortile already running (%s)", err)) os.Exit(1) } + return file } @@ -110,5 +123,6 @@ func createLogFile(filename string) (*os.File, error) { fmt.Println(fmt.Errorf("FILE error (%s)", err)) return nil, err } + return file, nil } diff --git a/store/client.go b/store/client.go index 4d566ec..3c4027b 100644 --- a/store/client.go +++ b/store/client.go @@ -2,7 +2,6 @@ package store import ( "math" - "reflect" "regexp" "strings" "time" @@ -20,10 +19,6 @@ import ( log "github.com/sirupsen/logrus" ) -const ( - UNKNOWN = "" -) - type Client struct { Win *xwindow.Window `json:"-"` // X window object Created time.Time // Internal client creation time @@ -35,6 +30,7 @@ type Info struct { Class string // Client window application name Name string // Client window title name DeskNum uint // Client window desktop + ScreenNum uint // Client window screen Types []string // Client window types States []string // Client window states Dimensions Dimensions // Client window dimensions @@ -44,8 +40,8 @@ type Dimensions struct { Geometry xrect.Rect // Client window geometry Hints Hints // Client window dimension hints Extents ewmh.FrameExtents // Client window geometry extents - Position bool // Adjust position on move/resize - Size bool // Adjust size on move/resize + AdjPos bool // Adjust position on move/resize + AdjSize bool // Adjust size on move/resize } type Hints struct { @@ -67,20 +63,44 @@ func (c *Client) Activate() { ewmh.ActiveWindowReq(common.X, c.Win.Id) } +func (c *Client) UnDecorate() { + if common.Config.WindowDecoration || !motif.Decor(&c.Latest.Dimensions.Hints.Motif) { + return + } + + // Remove window decorations + motif.WmHintsSet(common.X, c.Win.Id, &motif.Hints{ + Flags: motif.HintDecorations, + Decoration: motif.DecorationNone, + }) +} + +func (c *Client) UnMaximize() { + + // Unmaximize window + for _, state := range c.Latest.States { + if strings.Contains(state, "_NET_WM_STATE_MAXIMIZED") { + ewmh.WmStateReq(common.X, c.Win.Id, 0, "_NET_WM_STATE_MAXIMIZED_VERT") + ewmh.WmStateReq(common.X, c.Win.Id, 0, "_NET_WM_STATE_MAXIMIZED_HORZ") + break + } + } +} + func (c *Client) MoveResize(x, y, w, h int) { c.UnDecorate() c.UnMaximize() // Decoration extents - extents := c.Latest.Dimensions.Extents + ext := c.Latest.Dimensions.Extents // Calculate dimensions offsets dx, dy, dw, dh := 0, 0, 0, 0 - if c.Latest.Dimensions.Position { - dx, dy = extents.Left, extents.Top + if c.Latest.Dimensions.AdjPos { + dx, dy = ext.Left, ext.Top } - if c.Latest.Dimensions.Size { - dw, dh = extents.Left+extents.Right, extents.Top+extents.Bottom + if c.Latest.Dimensions.AdjSize { + dw, dh = ext.Left+ext.Right, ext.Top+ext.Bottom } // Move and resize window @@ -93,11 +113,11 @@ func (c *Client) MoveResize(x, y, w, h int) { c.Update() } -func (c *Client) LimitDim(w, h int) { +func (c *Client) LimitDimensions(w, h int) { // Decoration extents - extents := c.Latest.Dimensions.Extents - dw, dh := extents.Left+extents.Right, extents.Top+extents.Bottom + ext := c.Latest.Dimensions.Extents + dw, dh := ext.Left+ext.Right, ext.Top+ext.Bottom // Set window size limits icccm.WmNormalHintsSet(common.X, c.Win.Id, &icccm.NormalHints{ @@ -107,56 +127,35 @@ func (c *Client) LimitDim(w, h int) { }) } -func (c *Client) UnDecorate() { - if common.Config.WindowDecoration || !motif.Decor(&c.Latest.Dimensions.Hints.Motif) { - return - } - - // Remove window decorations - motif.WmHintsSet(common.X, c.Win.Id, &motif.Hints{ - Flags: motif.HintDecorations, - Decoration: motif.DecorationNone, - }) -} - -func (c *Client) UnMaximize() { - - // Unmaximize window - for _, state := range c.Latest.States { - if strings.Contains(state, "_NET_WM_STATE_MAXIMIZED") { - ewmh.WmStateReq(common.X, c.Win.Id, 0, "_NET_WM_STATE_MAXIMIZED_VERT") - ewmh.WmStateReq(common.X, c.Win.Id, 0, "_NET_WM_STATE_MAXIMIZED_HORZ") - break - } - } -} - -func (c *Client) Update() (success bool) { +func (c *Client) Update() { info := GetInfo(c.Win.Id) - if info.Class == UNKNOWN { - return false - } // Update client info log.Debug("Update client info [", info.Class, "]") c.Latest = info - - return true } func (c *Client) Restore() { - - // Calculate decoration extents dw, dh := 0, 0 + + // Obtain decoration motif decoration := motif.DecorationNone if motif.Decor(&c.Original.Dimensions.Hints.Motif) { decoration = motif.DecorationAll + + // Obtain decoration extents if !common.Config.WindowDecoration { - extents := c.Original.Dimensions.Extents - dw, dh = extents.Left+extents.Right, extents.Top+extents.Bottom + ext := c.Original.Dimensions.Extents + dw, dh = ext.Left+ext.Right, ext.Top+ext.Bottom } } + // Obtain dimension adjustments + if c.Latest.Dimensions.AdjPos && c.Latest.Dimensions.AdjSize { + c.Latest.Dimensions.AdjPos = false + c.Latest.Dimensions.AdjSize = false + } + // Restore window decorations motif.WmHintsSet(common.X, c.Win.Id, &motif.Hints{ Flags: motif.HintDecorations, @@ -166,8 +165,8 @@ func (c *Client) Restore() { // Restore window size limits icccm.WmNormalHintsSet(common.X, c.Win.Id, &c.Original.Dimensions.Hints.Normal) - // Move window to original position - geom := c.Original.Dimensions.Geometry + // Move window to latest position considering decoration adjustments + geom := c.Latest.Dimensions.Geometry c.MoveResize(geom.X(), geom.Y(), geom.Width()-dw, geom.Height()-dh) } @@ -188,8 +187,8 @@ func (c *Client) OuterGeometry() (x, y, w, h int) { } // Decoration extents (l/r/t/b relative to outer window dimensions) - extents := c.Latest.Dimensions.Extents - dx, dy, dw, dh := extents.Left, extents.Top, extents.Left+extents.Right, extents.Top+extents.Bottom + ext := c.Latest.Dimensions.Extents + dx, dy, dw, dh := ext.Left, ext.Top, ext.Left+ext.Right, ext.Top+ext.Bottom // Calculate outer geometry (including server and client decorations) x, y, w, h = oGeom.X()+iGeom.X()-dx, oGeom.Y()+iGeom.Y()-dy, iGeom.Width()+dw, iGeom.Height()+dh @@ -197,12 +196,103 @@ func (c *Client) OuterGeometry() (x, y, w, h int) { return } +func IsSpecial(info Info) bool { + + // Check window types + types := []string{ + "_NET_WM_WINDOW_TYPE_DOCK", + "_NET_WM_WINDOW_TYPE_DESKTOP", + "_NET_WM_WINDOW_TYPE_TOOLBAR", + "_NET_WM_WINDOW_TYPE_UTILITY", + "_NET_WM_WINDOW_TYPE_TOOLTIP", + "_NET_WM_WINDOW_TYPE_SPLASH", + "_NET_WM_WINDOW_TYPE_DIALOG", + "_NET_WM_WINDOW_TYPE_COMBO", + "_NET_WM_WINDOW_TYPE_NOTIFICATION", + "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", + "_NET_WM_WINDOW_TYPE_POPUP_MENU", + "_NET_WM_WINDOW_TYPE_MENU", + "_NET_WM_WINDOW_TYPE_DND", + } + for _, typ := range info.Types { + if common.IsInList(typ, types) { + log.Info("Ignore window with type ", typ, " [", info.Class, "]") + return true + } + } + + // Check window states + states := []string{ + "_NET_WM_STATE_HIDDEN", + "_NET_WM_STATE_STICKY", + "_NET_WM_STATE_MODAL", + "_NET_WM_STATE_ABOVE", + "_NET_WM_STATE_BELOW", + "_NET_WM_STATE_SKIP_PAGER", + "_NET_WM_STATE_SKIP_TASKBAR", + } + for _, state := range info.States { + if common.IsInList(state, states) { + log.Info("Ignore window with state ", state, " [", info.Class, "]") + return true + } + } + + // Check pinned windows + if info.DeskNum > common.DeskCount { + log.Info("Ignore pinned window [", info.Class, "]") + return true + } + + return false +} + +func IsIgnored(info Info) bool { + + // Check ignored windows + for _, s := range common.Config.WindowIgnore { + conf_class := s[0] + conf_name := s[1] + + reg_class := regexp.MustCompile(strings.ToLower(conf_class)) + reg_name := regexp.MustCompile(strings.ToLower(conf_name)) + + // Ignore all windows with this class + class_match := reg_class.MatchString(strings.ToLower(info.Class)) + + // But allow the window with a special name + name_match := conf_name != "" && reg_name.MatchString(strings.ToLower(info.Name)) + + if class_match && !name_match { + log.Info("Ignore window with ", strings.TrimSpace(strings.Join(s, " ")), " from config [", info.Class, "]") + return true + } + } + + return false +} + +func IsMaximized(w xproto.Window) bool { + info := GetInfo(w) + + // Check maximized windows + for _, state := range info.States { + if strings.Contains(state, "_NET_WM_STATE_MAXIMIZED") { + log.Info("Ignore maximized window [", info.Class, "]") + return true + } + } + + return false +} + func GetInfo(w xproto.Window) (info Info) { var err error var class string var name string - var desk uint + var deskNum uint + var screenNum uint var types []string var states []string var dimensions Dimensions @@ -210,8 +300,8 @@ func GetInfo(w xproto.Window) (info Info) { // Window class (internal class name of the window) cls, err := icccm.WmClassGet(common.X, w) if err != nil { - log.Trace(err) - class = UNKNOWN + class = "UNKNOWN" + log.Trace(err, " [", class, "]") } else if cls != nil { class = cls.Class } @@ -220,15 +310,16 @@ func GetInfo(w xproto.Window) (info Info) { name, err = icccm.WmNameGet(common.X, w) if err != nil { log.Trace(err, " [", class, "]") - name = UNKNOWN + name = class } - // Window desktop (desktop workspace where the window is visible) - desk, err = ewmh.WmDesktopGet(common.X, w) + // Window desktop and screen (workspace where the window is located) + deskNum, err = ewmh.WmDesktopGet(common.X, w) if err != nil { log.Trace(err, " [", class, "]") - desk = math.MaxUint + deskNum = math.MaxUint } + screenNum = getScreenNum(w) // Window types (types of the window) types, err = ewmh.WmWindowTypeGet(common.X, w) @@ -287,146 +378,34 @@ func GetInfo(w xproto.Window) (info Info) { Top: int(ext[2]), Bottom: int(ext[3]), }, - Position: (extNet != nil && mhints.Flags&motif.HintDecorations > 0 && mhints.Decoration > 1) || (extGtk != nil), - Size: (extNet != nil) || (extGtk != nil), + AdjPos: (extNet != nil && mhints.Flags&motif.HintDecorations > 0 && mhints.Decoration > 1) || (extGtk != nil), + AdjSize: (extNet != nil) || (extGtk != nil), } return Info{ Class: class, Name: name, - DeskNum: desk, + DeskNum: deskNum, + ScreenNum: screenNum, Types: types, States: states, Dimensions: dimensions, } } -func IsMaximized(w xproto.Window) bool { - info := GetInfo(w) - if info.Class == UNKNOWN { - return false - } +func getScreenNum(w xproto.Window) uint { - // Check maximized windows - for _, state := range info.States { - if strings.Contains(state, "_NET_WM_STATE_MAXIMIZED") { - log.Info("Ignore maximized window [", info.Class, "]") - return true - } - } - - return false -} - -func IsInsideViewPort(w xproto.Window) bool { - info := GetInfo(w) - if info.Class == UNKNOWN { - return false - } - - // Viewport dimensions - vRect := xrect.New(common.DesktopDimensions()) - - // Substract viewport rectangle (r2) from window rectangle (r1) - sRects := xrect.Subtract(info.Dimensions.Geometry, vRect) - - // If r1 does not overlap r2, then only one rectangle is returned which is equivalent to r1 - isOutsideViewport := false - if len(sRects) == 1 { - isOutsideViewport = reflect.DeepEqual(sRects[0], info.Dimensions.Geometry) - } - - if isOutsideViewport { - log.Info("Ignore window outside viewport [", info.Class, "]") - } - - return !isOutsideViewport -} - -func IsIgnored(w xproto.Window) bool { - info := GetInfo(w) - if info.Class == UNKNOWN { - return true - } - - // Check ignored windows - for _, s := range common.Config.WindowIgnore { - conf_class := s[0] - conf_name := s[1] - - reg_class := regexp.MustCompile(strings.ToLower(conf_class)) - reg_name := regexp.MustCompile(strings.ToLower(conf_name)) - - // Ignore all windows with this class - class_match := reg_class.MatchString(strings.ToLower(info.Class)) - - // But allow the window with a special name - name_match := conf_name != "" && reg_name.MatchString(strings.ToLower(info.Name)) - - if class_match && !name_match { - log.Info("Ignore window with ", strings.TrimSpace(strings.Join(s, " ")), " from config [", info.Class, "]") - return true - } - } - - return false -} - -func IsSpecial(w xproto.Window) bool { - info := GetInfo(w) - if info.Class == UNKNOWN { - return true - } - - // Check window types - types := map[string]bool{} - for _, typ := range []string{ - "_NET_WM_WINDOW_TYPE_DOCK", - "_NET_WM_WINDOW_TYPE_DESKTOP", - "_NET_WM_WINDOW_TYPE_TOOLBAR", - "_NET_WM_WINDOW_TYPE_UTILITY", - "_NET_WM_WINDOW_TYPE_TOOLTIP", - "_NET_WM_WINDOW_TYPE_SPLASH", - "_NET_WM_WINDOW_TYPE_DIALOG", - "_NET_WM_WINDOW_TYPE_COMBO", - "_NET_WM_WINDOW_TYPE_NOTIFICATION", - "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", - "_NET_WM_WINDOW_TYPE_POPUP_MENU", - "_NET_WM_WINDOW_TYPE_MENU", - "_NET_WM_WINDOW_TYPE_DND"} { - types[typ] = true - } - for _, typ := range info.Types { - if types[typ] { - log.Info("Ignore window with type ", typ, " [", info.Class, "]") - return true - } - } - - // Check window states - states := map[string]bool{} - for _, state := range []string{ - "_NET_WM_STATE_HIDDEN", - "_NET_WM_STATE_STICKY", - "_NET_WM_STATE_MODAL", - "_NET_WM_STATE_ABOVE", - "_NET_WM_STATE_BELOW", - "_NET_WM_STATE_SKIP_PAGER", - "_NET_WM_STATE_SKIP_TASKBAR"} { - states[state] = true - } - for _, state := range info.States { - if states[state] { - log.Info("Ignore window with state ", state, " [", info.Class, "]") - return true - } + // Outer window dimensions + geom, err := xwindow.New(common.X, w).DecorGeometry() + if err != nil { + return 0 } - // Check pinned windows - if info.DeskNum > common.DeskCount { - log.Info("Ignore pinned window [", info.Class, "]") - return true + // Window center position + center := &xproto.QueryPointerReply{ + RootX: int16(geom.X() + (geom.Width() / 2)), + RootY: int16(geom.Y() + (geom.Height() / 2)), } - return false + return common.ScreenNumGet(center) } diff --git a/store/manager.go b/store/manager.go index d5da67a..50a7d7f 100644 --- a/store/manager.go +++ b/store/manager.go @@ -11,6 +11,7 @@ import ( type Manager struct { DeskNum uint // Index of managed desktop + ScreenNum uint // Index of managed screen Proportions *Proportions // Layout proportions of window clients Masters *Windows // List of master window clients Slaves *Windows // List of slave window clients @@ -34,9 +35,10 @@ type Windows struct { MaxAllowed int // Number of maximal allowed clients } -func CreateManager(deskNum uint) *Manager { +func CreateManager(deskNum uint, screenNum uint) *Manager { return &Manager{ - DeskNum: deskNum, + DeskNum: deskNum, + ScreenNum: screenNum, Proportions: &Proportions{ MasterSlave: calcProportions(2), MasterMaster: calcProportions(common.Config.WindowMastersMax), @@ -56,7 +58,7 @@ func CreateManager(deskNum uint) *Manager { func (mg *Manager) Undo() { clients := mg.Clients(true) - log.Info("Untile ", len(clients), " windows [workspace-", mg.DeskNum, "]") + log.Info("Untile ", len(clients), " windows [workspace-", mg.DeskNum, "-", mg.ScreenNum, "]") for _, c := range clients { c.Restore() @@ -64,11 +66,7 @@ func (mg *Manager) Undo() { } func (mg *Manager) AddClient(c *Client) { - if c == nil { - return - } - - log.Debug("Add client for manager [", c.Latest.Class, "]") + log.Debug("Add client for manager [", c.Latest.Class, ", workspace-", mg.DeskNum, "-", mg.ScreenNum, "]") // Fill up master area then slave area if len(mg.Masters.Clients) < mg.Masters.MaxAllowed { @@ -79,11 +77,7 @@ func (mg *Manager) AddClient(c *Client) { } func (mg *Manager) RemoveClient(c *Client) { - if c == nil { - return - } - - log.Debug("Remove client from manager [", c.Latest.Class, "]") + log.Debug("Remove client from manager [", c.Latest.Class, ", workspace-", mg.DeskNum, "-", mg.ScreenNum, "]") // Remove master window mi := mg.Index(mg.Masters, c) @@ -106,11 +100,7 @@ func (mg *Manager) RemoveClient(c *Client) { } func (mg *Manager) MakeMaster(c *Client) { - if c == nil { - return - } - - log.Info("Make window master [", c.Latest.Class, "]") + log.Info("Make window master [", c.Latest.Class, ", workspace-", mg.DeskNum, "-", mg.ScreenNum, "]") // Swap window with first master if len(mg.Masters.Clients) > 0 { @@ -119,11 +109,7 @@ func (mg *Manager) MakeMaster(c *Client) { } func (mg *Manager) SwapClient(c1 *Client, c2 *Client) { - if c1 == nil || c2 == nil { - return - } - - log.Info("Swap clients [", c1.Latest.Class, " - ", c2.Latest.Class, "]") + log.Info("Swap clients [", c1.Latest.Class, "-", c2.Latest.Class, ", workspace-", mg.DeskNum, "-", mg.ScreenNum, "]") mIndex1 := mg.Index(mg.Masters, c1) sIndex1 := mg.Index(mg.Slaves, c1) @@ -280,18 +266,18 @@ func (mg *Manager) SetProportions(ps []float64, pi float64, i int, j int) bool { } func (mg *Manager) IsMaster(c *Client) bool { - if c == nil { - return false - } // Check if window is master return mg.Index(mg.Masters, c) >= 0 } +func (mg *Manager) IsSlave(c *Client) bool { + + // Check if window is slave + return mg.Index(mg.Slaves, c) >= 0 +} + func (mg *Manager) Index(windows *Windows, c *Client) int { - if c == nil { - return -1 - } // Traverse client list for i, m := range windows.Clients {