Skip to content

Commit

Permalink
full multi monitor support
Browse files Browse the repository at this point in the history
  • Loading branch information
leukipp committed May 31, 2023
1 parent 7f0a5d9 commit 85c2b4d
Show file tree
Hide file tree
Showing 16 changed files with 570 additions and 525 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.assets
dist
cortile
dist
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)'"
```

</div></details>
Expand All @@ -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.

Expand All @@ -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).

Expand Down
66 changes: 32 additions & 34 deletions common/corner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
102 changes: 55 additions & 47 deletions common/state.go → common/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,25 @@ 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 {
Screens xinerama.Heads // Screen size (full monitor size)
Desktops xinerama.Heads // Desktop size (workarea without panels)
}

func InitState() {
func InitRoot() {
var err error

X := Connect()
Expand All @@ -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())
Expand All @@ -62,27 +64,29 @@ 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
}
i += 1
time.Sleep(100 * time.Millisecond)
}

log.Info("Connected to X server")

return X
}

Expand All @@ -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,
Expand All @@ -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]
Expand All @@ -149,50 +161,46 @@ 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 {
log.Warn("Error updating state ", err)
}
}

func stateCallbacks(aname string) {
for _, fun := range callbacks {
fun(aname)
}
}

func checkFatal(err error) {
if err != nil {
log.Fatal("Error on initialization ", err)
Expand Down
44 changes: 44 additions & 0 deletions common/utils.go
Original file line number Diff line number Diff line change
@@ -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]),
}
}
Loading

0 comments on commit 85c2b4d

Please sign in to comment.