diff --git a/assets/scripts/listen.sh b/assets/scripts/listen.sh index 65975e3..ab0bfb1 100755 --- a/assets/scripts/listen.sh +++ b/assets/scripts/listen.sh @@ -58,16 +58,16 @@ while json=$(nc -Ulw 1 $sockout | jq -r "."); do case ${type} in "Action") - desk=$(echo $data | jq -r ".Desk") - screen=$(echo $data | jq -r ".Screen") + desk=$(echo $data | jq -r ".DeskNum") + screen=$(echo $data | jq -r ".ScreenNum") # EXAMPLE: retrieve action event on active workspace echo "Received 'action' with name '$name' on 'desktop = $desk' and 'screen = $screen'";; "State") case ${name} in "workspaces") - desk=$(echo $data | jq -r ".Desk") - screen=$(echo $data | jq -r ".Screen") + desk=$(echo $data | jq -r ".DeskNum") + screen=$(echo $data | jq -r ".ScreenNum") workspace=$(echo $data | jq -r ".Workspaces[] | select((.Location.DeskNum==$desk) and (.Location.ScreenNum==$screen))") enabled=$(echo $workspace | jq -r ".TilingEnabled") layout=$(echo $workspace | jq -r ".ActiveLayoutNum") diff --git a/common/cache.go b/common/cache.go index 377d7c9..ae10bd8 100644 --- a/common/cache.go +++ b/common/cache.go @@ -2,6 +2,7 @@ package common import ( "os" + "strings" "path/filepath" @@ -15,14 +16,14 @@ type Cache[T any] struct { } func InitCache() { - if Args.Cache == "0" { + if !CacheEnabled() { return } // Create cache folder if not exists cacheFolderPath := Args.Cache if _, err := os.Stat(cacheFolderPath); os.IsNotExist(err) { - os.MkdirAll(cacheFolderPath, 0700) + os.MkdirAll(cacheFolderPath, 0755) } } @@ -36,3 +37,8 @@ func CacheFolderPath(name string) string { return filepath.Join(userCacheDir, name) } + +func CacheEnabled() bool { + arg := strings.ToLower(strings.TrimSpace(Args.Cache)) + return !IsInList(arg, []string{"", "0", "off", "false", "disabled"}) +} diff --git a/common/config.go b/common/config.go index 78afe53..d0a98ae 100644 --- a/common/config.go +++ b/common/config.go @@ -26,7 +26,6 @@ type Configuration struct { WindowSlavesMax int `toml:"window_slaves_max"` // Maximum number of allowed slaves WindowGapSize int `toml:"window_gap_size"` // Gap size between windows WindowDecoration bool `toml:"window_decoration"` // Show window decorations - Proportion float64 `toml:"proportion"` // Master-slave area initial proportion ProportionStep float64 `toml:"proportion_step"` // Master-slave area step size proportion ProportionMin float64 `toml:"proportion_min"` // Window size minimum proportion EdgeMargin []int `toml:"edge_margin"` // Margin values of tiling area @@ -44,7 +43,7 @@ func InitConfig() { // Create config folder if not exists configFolderPath := filepath.Dir(Args.Config) if _, err := os.Stat(configFolderPath); os.IsNotExist(err) { - os.MkdirAll(configFolderPath, 0700) + os.MkdirAll(configFolderPath, 0755) } // Write default config if not exists diff --git a/config.toml b/config.toml index 12efc8d..9186483 100644 --- a/config.toml +++ b/config.toml @@ -29,6 +29,7 @@ tiling_icon = [ ["slave_increase", "Add Slave"], ["slave_decrease", "Remove Slave"], ["", ""], + ["reset", "Reset"], ["exit", "Exit"], ] @@ -63,9 +64,6 @@ window_decoration = true ################################## Proportion ################################## -# Initial division of master-slave area (0.0 - 1.0). -proportion = 0.6 - # How much to increment/decrement master-slave area (0.0 - 1.0). proportion_step = 0.05 @@ -124,6 +122,9 @@ restore = "Control-Shift-R" # Toggle between enable and disable on the current screen. toggle = "Control-Shift-T" +# Reset layouts to default proportions (BackSpace = Delete_Left) +reset = "Control-Shift-BackSpace" + # Cycles through next layouts (Next = Page_Down). cycle_next = "Control-Shift-Next" diff --git a/desktop/layout.go b/desktop/layout.go index 2288843..ec8d955 100644 --- a/desktop/layout.go +++ b/desktop/layout.go @@ -3,6 +3,7 @@ package desktop import "github.com/leukipp/cortile/v2/store" type Layout interface { + Reset() Apply() AddClient(c *store.Client) RemoveClient(c *store.Client) diff --git a/desktop/tracker.go b/desktop/tracker.go index bd4214b..9999b19 100644 --- a/desktop/tracker.go +++ b/desktop/tracker.go @@ -37,10 +37,10 @@ type HandlerClient struct { Target *store.Client // Stores hovered client } -func CreateTracker(ws map[store.Location]*Workspace) *Tracker { +func CreateTracker() *Tracker { tr := Tracker{ Clients: make(map[xproto.Window]*store.Client), - Workspaces: ws, + Workspaces: CreateWorkspaces(), Action: make(chan string), Handler: &Handler{ ResizeClient: &HandlerClient{}, @@ -54,11 +54,6 @@ func CreateTracker(ws map[store.Location]*Workspace) *Tracker { store.OnStateUpdate(tr.onStateUpdate) store.OnPointerUpdate(tr.onPointerUpdate) - // Populate clients on startup - if common.Config.TilingEnabled { - tr.Update() - } - return &tr } @@ -102,6 +97,19 @@ func (tr *Tracker) Reset() { tr.Workspaces = CreateWorkspaces() } +func (tr *Tracker) Write() { + + // Write client cache + for _, c := range tr.Clients { + c.Write() + } + + // Write workspace cache + for _, ws := range tr.Workspaces { + ws.Write() + } +} + func (tr *Tracker) ActiveWorkspace() *Workspace { location := store.Location{DeskNum: store.CurrentDesk, ScreenNum: store.CurrentScreen} @@ -126,6 +134,19 @@ func (tr *Tracker) ClientWorkspace(c *store.Client) *Workspace { return ws } +func (tr *Tracker) unlockClients() { + ws := tr.ActiveWorkspace() + mg := ws.ActiveLayout().GetManager() + + // Unlock clients + for _, c := range mg.Clients(store.Stacked) { + if c == nil { + continue + } + c.UnLock() + } +} + func (tr *Tracker) trackWindow(w xproto.Window) bool { if tr.isTracked(w) { return false @@ -391,19 +412,6 @@ func (tr *Tracker) handleWorkspaceChange(c *store.Client) { tr.Handler.SwapScreen.Active = false } -func (tr *Tracker) unlockClients() { - ws := tr.ActiveWorkspace() - mg := ws.ActiveLayout().GetManager() - - // Unlock clients - for _, c := range mg.Clients(store.Stacked) { - if c == nil { - continue - } - c.UnLock() - } -} - func (tr *Tracker) onStateUpdate(aname string) { viewportChanged := common.IsInList(aname, []string{"_NET_NUMBER_OF_DESKTOPS", "_NET_DESKTOP_LAYOUT", "_NET_DESKTOP_GEOMETRY", "_NET_DESKTOP_VIEWPORT", "_NET_WORKAREA"}) clientsChanged := common.IsInList(aname, []string{"_NET_CLIENT_LIST_STACKING", "_NET_ACTIVE_WINDOW"}) @@ -441,6 +449,9 @@ func (tr *Tracker) onStateUpdate(aname string) { // Update trackable clients tr.Update() } + + // Write client and workspace cache + tr.Write() } func (tr *Tracker) onPointerUpdate(button uint16) { diff --git a/desktop/workspace.go b/desktop/workspace.go index fa32ca9..642f071 100644 --- a/desktop/workspace.go +++ b/desktop/workspace.go @@ -1,6 +1,13 @@ package desktop import ( + "fmt" + "math" + "os" + + "encoding/json" + "path/filepath" + "github.com/leukipp/cortile/v2/common" "github.com/leukipp/cortile/v2/layout" "github.com/leukipp/cortile/v2/store" @@ -9,6 +16,7 @@ import ( ) type Workspace struct { + Name string // Workspace location name Location store.Location // Desktop and screen location Layouts []Layout // List of available layouts TilingEnabled bool // Tiling is enabled or not @@ -23,21 +31,38 @@ func CreateWorkspaces() map[store.Location]*Workspace { location := store.Location{DeskNum: deskNum, ScreenNum: screenNum} // Create layouts for each desktop and screen - layouts := CreateLayouts(location) ws := &Workspace{ + Name: fmt.Sprintf("workspace-%d-%d", location.DeskNum, location.ScreenNum), Location: location, - Layouts: layouts, + Layouts: CreateLayouts(location), TilingEnabled: common.Config.TilingEnabled, ActiveLayoutNum: 0, } - // Activate default layout - for i, l := range layouts { + // Set default layout + for i, l := range ws.Layouts { if l.GetName() == common.Config.TilingLayout { ws.SetLayout(uint(i)) } } + // Read workspace from cache + cached := ws.Read() + + // Overwrite default layout, proportions and tiling state + ws.SetLayout(cached.ActiveLayoutNum) + for _, l := range ws.Layouts { + for _, cl := range cached.Layouts { + if l.GetName() == cl.GetName() { + mg, cmg := l.GetManager(), cl.GetManager() + mg.Masters.MaxAllowed = int(math.Min(float64(cmg.Masters.MaxAllowed), float64(common.Config.WindowMastersMax))) + mg.Slaves.MaxAllowed = int(math.Min(float64(cmg.Slaves.MaxAllowed), float64(common.Config.WindowSlavesMax))) + mg.Proportions = cmg.Proportions + } + } + } + ws.TilingEnabled = cached.TilingEnabled + // Map location to workspace workspaces[location] = ws } @@ -46,13 +71,19 @@ func CreateWorkspaces() map[store.Location]*Workspace { return workspaces } -func CreateLayouts(l store.Location) []Layout { +func CreateLayouts(loc store.Location) []Layout { return []Layout{ - 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), + layout.CreateFullscreenLayout(loc), + layout.CreateVerticalLeftLayout(loc), + layout.CreateVerticalRightLayout(loc), + layout.CreateHorizontalTopLayout(loc), + layout.CreateHorizontalBottomLayout(loc), + } +} + +func (ws *Workspace) ResetLayouts() { + for _, l := range ws.Layouts { + l.Reset() } } @@ -110,7 +141,7 @@ func (ws *Workspace) Restore(flag uint8) { mg := ws.ActiveLayout().GetManager() clients := mg.Clients(store.Stacked) - log.Info("Untile ", len(clients), " windows [workspace-", mg.DeskNum, "-", mg.ScreenNum, "]") + log.Info("Untile ", len(clients), " windows [", ws.Name, "]") // Restore client dimensions for _, c := range clients { @@ -138,7 +169,82 @@ func (ws *Workspace) Enabled() bool { func (ws *Workspace) Disabled() bool { if ws == nil { - return false + return true } return !ws.TilingEnabled } + +func (ws *Workspace) Write() { + if !common.CacheEnabled() { + return + } + + // Obtain cache object + cache := ws.Cache() + + // Parse workspace cache + data, err := json.MarshalIndent(cache.Data, "", " ") + if err != nil { + log.Warn("Error parsing workspace cache [", ws.Name, "]") + return + } + + // Write workspace cache + path := filepath.Join(cache.Folder, cache.Name) + err = os.WriteFile(path, data, 0644) + if err != nil { + log.Warn("Error writing workspace cache [", ws.Name, "]") + return + } + + log.Debug("Write workspace cache data ", cache.Name, " [", ws.Name, "]") +} + +func (ws *Workspace) Read() *Workspace { + if !common.CacheEnabled() { + return ws + } + + // Obtain cache object + cache := ws.Cache() + + // Read workspace cache + path := filepath.Join(cache.Folder, cache.Name) + data, err := os.ReadFile(path) + if os.IsNotExist(err) { + log.Info("No workspace cache found [", ws.Name, "]") + return ws + } + + // Parse workspace cache + cached := &Workspace{Layouts: CreateLayouts(ws.Location)} + err = json.Unmarshal([]byte(data), &cached) + if err != nil { + log.Warn("Error reading workspace cache [", ws.Name, "]") + return ws + } + + log.Debug("Read workspace cache data ", cache.Name, " [", ws.Name, "]") + + return cached +} + +func (ws *Workspace) Cache() common.Cache[*Workspace] { + name := fmt.Sprintf("workspace-%d", ws.Location.DeskNum) + hash := fmt.Sprintf("%s-%d", name, ws.Location.ScreenNum) + + // Create workspace cache folder + folder := filepath.Join(common.Args.Cache, store.Displays.Name, "workspaces", name) + if _, err := os.Stat(folder); os.IsNotExist(err) { + os.MkdirAll(folder, 0755) + } + + // Create workspace cache object + cache := common.Cache[*Workspace]{ + Folder: folder, + Name: common.Hash(hash) + ".json", + Data: ws, + } + + return cache +} diff --git a/input/action.go b/input/action.go index bcc2101..eb6f570 100644 --- a/input/action.go +++ b/input/action.go @@ -86,6 +86,8 @@ func Execute(action string, mod string, tr *desktop.Tracker) bool { success = NextWindow(tr, ws) case "window_previous": success = PreviousWindow(tr, ws) + case "reset": + success = Reset(tr, ws) case "exit": success = Exit(tr) default: @@ -98,13 +100,13 @@ func Execute(action string, mod string, tr *desktop.Tracker) bool { // Notify socket type Action struct { - Desk uint - Screen uint + DeskNum uint + ScreenNum uint } NotifySocket(Message[Action]{ Type: "Action", Name: action, - Data: Action{Desk: ws.Location.DeskNum, Screen: ws.Location.ScreenNum}, + Data: Action{DeskNum: ws.Location.DeskNum, ScreenNum: ws.Location.ScreenNum}, }) } @@ -128,14 +130,14 @@ func Query(state string, tr *desktop.Tracker) bool { switch state { case "workspaces": type Workspaces struct { - Desk uint - Screen uint + DeskNum uint + ScreenNum uint Workspaces []*desktop.Workspace } NotifySocket(Message[Workspaces]{ Type: "State", Name: state, - Data: Workspaces{Desk: ws.Location.DeskNum, Screen: ws.Location.ScreenNum, Workspaces: maps.Values(tr.Workspaces)}, + Data: Workspaces{DeskNum: ws.Location.DeskNum, ScreenNum: ws.Location.ScreenNum, Workspaces: maps.Values(tr.Workspaces)}, }) success = true case "arguments": @@ -453,7 +455,22 @@ func PreviousWindow(tr *desktop.Tracker, ws *desktop.Workspace) bool { return true } +func Reset(tr *desktop.Tracker, ws *desktop.Workspace) bool { + if ws.Disabled() { + return false + } + ws.ResetLayouts() + ws.Tile() + + ui.ShowLayout(ws) + ui.UpdateIcon(ws) + + return true +} + func Exit(tr *desktop.Tracker) bool { + tr.Write() + for _, ws := range tr.Workspaces { if ws.Disabled() { continue diff --git a/layout/fullscreen.go b/layout/fullscreen.go index b6e9fca..8c1bf4d 100644 --- a/layout/fullscreen.go +++ b/layout/fullscreen.go @@ -10,26 +10,35 @@ import ( ) type FullscreenLayout struct { - *store.Manager // Layout store manager Name string // Layout name + *store.Manager // Layout store manager } -func CreateFullscreenLayout(deskNum uint, screenNum uint) *FullscreenLayout { - return &FullscreenLayout{ - Manager: store.CreateManager(deskNum, screenNum), +func CreateFullscreenLayout(loc store.Location) *FullscreenLayout { + layout := &FullscreenLayout{ Name: "fullscreen", + Manager: store.CreateManager(loc), } + layout.Reset() + return layout +} + +func (l *FullscreenLayout) Reset() { + mg := store.CreateManager(*l.Location) + + // Reset layout proportions + l.Manager.Proportions = mg.Proportions } func (l *FullscreenLayout) Apply() { clients := l.Clients(store.Stacked) - dx, dy, dw, dh := store.DesktopDimensions(l.ScreenNum) + dx, dy, dw, dh := store.DesktopDimensions(l.Location.ScreenNum) gap := common.Config.WindowGapSize csize := len(clients) - log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.DeskNum, "-", l.ScreenNum, "]") + log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.Location.DeskNum, "-", l.Location.ScreenNum, "]") // Main area layout for _, c := range clients { @@ -45,7 +54,7 @@ func (l *FullscreenLayout) Apply() { } func (l *FullscreenLayout) UpdateProportions(c *store.Client, d *store.Directions) { - l.Proportions.MasterSlave = []float64{1.0} + l.Reset() } func (l *FullscreenLayout) GetManager() *store.Manager { diff --git a/layout/horizontal.go b/layout/horizontal.go index b336d7a..aef8409 100644 --- a/layout/horizontal.go +++ b/layout/horizontal.go @@ -10,34 +10,55 @@ import ( ) type HorizontalLayout struct { - *store.Manager // Layout store manager Name string // Layout name + *store.Manager // Layout store manager } -func CreateHorizontalTopLayout(deskNum uint, screenNum uint) *HorizontalLayout { - manager := store.CreateManager(deskNum, screenNum) - manager.SetProportions(manager.Proportions.MasterSlave, common.Config.Proportion, 0, 1) - - return &HorizontalLayout{ - Manager: manager, +func CreateHorizontalTopLayout(loc store.Location) *HorizontalLayout { + layout := &HorizontalLayout{ Name: "horizontal-top", + Manager: store.CreateManager(loc), } + layout.Reset() + return layout } -func CreateHorizontalBottomLayout(deskNum uint, screenNum uint) *HorizontalLayout { - manager := store.CreateManager(deskNum, screenNum) - manager.SetProportions(manager.Proportions.MasterSlave, common.Config.Proportion, 1, 0) - - return &HorizontalLayout{ - Manager: manager, +func CreateHorizontalBottomLayout(loc store.Location) *HorizontalLayout { + layout := &HorizontalLayout{ Name: "horizontal-bottom", + Manager: store.CreateManager(loc), } + layout.Reset() + return layout +} + +func (l *HorizontalLayout) Reset() { + mg := store.CreateManager(*l.Location) + + // Reset number of masters + for l.Masters.MaxAllowed < mg.Masters.MaxAllowed { + l.IncreaseMaster() + } + for l.Masters.MaxAllowed > mg.Masters.MaxAllowed { + l.DecreaseMaster() + } + + // Reset number of slaves + for l.Slaves.MaxAllowed < mg.Slaves.MaxAllowed { + l.IncreaseSlave() + } + for l.Slaves.MaxAllowed > mg.Slaves.MaxAllowed { + l.DecreaseSlave() + } + + // Reset layout proportions + l.Manager.Proportions = mg.Proportions } func (l *HorizontalLayout) Apply() { clients := l.Clients(store.Stacked) - dx, dy, dw, dh := store.DesktopDimensions(l.ScreenNum) + dx, dy, dw, dh := store.DesktopDimensions(l.Location.ScreenNum) gap := common.Config.WindowGapSize mmax := l.Masters.MaxAllowed @@ -48,11 +69,11 @@ func (l *HorizontalLayout) Apply() { csize := len(clients) my := dy - mh := int(math.Round(float64(dh) * l.Proportions.MasterSlave[0])) + mh := int(math.Round(float64(dh) * l.Proportions.MasterSlave[2][0])) sy := my + mh sh := dh - mh - log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.DeskNum, "-", l.ScreenNum, "]") + log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.Location.DeskNum, "-", l.Location.ScreenNum, "]") // Swap values if master is on bottom if l.Name == "horizontal-bottom" && csize > mmax { @@ -95,7 +116,7 @@ func (l *HorizontalLayout) Apply() { c.LimitDimensions(minw, minh) // Move and resize master - mp := l.Proportions.MasterMaster[i%msize] + mp := l.Proportions.MasterMaster[msize][i%msize] mw := int(math.Round(float64(dw-(msize+1)*gap) * mp)) c.MoveResize(mx, my+gap, mw, mh-2*gap) @@ -133,7 +154,7 @@ func (l *HorizontalLayout) Apply() { c.LimitDimensions(minw, minh) // Move and resize slave - sp := l.Proportions.SlaveSlave[i%ssize] + sp := l.Proportions.SlaveSlave[ssize][i%ssize] sw := int(math.Round(float64(dw-(ssize+1)*gap) * sp)) c.MoveResize(sx, sy, sw, sh-gap) @@ -144,7 +165,7 @@ func (l *HorizontalLayout) Apply() { } func (l *HorizontalLayout) UpdateProportions(c *store.Client, d *store.Directions) { - _, _, dw, dh := store.DesktopDimensions(l.ScreenNum) + _, _, dw, dh := store.DesktopDimensions(l.Location.ScreenNum) _, _, cw, ch := c.OuterGeometry() gap := common.Config.WindowGapSize @@ -178,14 +199,14 @@ func (l *HorizontalLayout) UpdateProportions(c *store.Client, d *store.Direction // Set master-slave proportions if d.Top { - l.Manager.SetProportions(l.Proportions.MasterSlave, py, idxms, idxms^1) + l.Manager.SetProportions(l.Proportions.MasterSlave[2], py, idxms, idxms^1) } // Set master-master proportions if d.Left { - l.Manager.SetProportions(l.Proportions.MasterMaster, px, idxmm, idxmm-1) + l.Manager.SetProportions(l.Proportions.MasterMaster[msize], px, idxmm, idxmm-1) } else if d.Right { - l.Manager.SetProportions(l.Proportions.MasterMaster, px, idxmm, idxmm+1) + l.Manager.SetProportions(l.Proportions.MasterMaster[msize], px, idxmm, idxmm+1) } } else { py := float64(ch+gap) / float64(dh) @@ -194,14 +215,14 @@ func (l *HorizontalLayout) UpdateProportions(c *store.Client, d *store.Direction // Set master-slave proportions if d.Bottom { - l.Manager.SetProportions(l.Proportions.MasterSlave, py, idxms, idxms^1) + l.Manager.SetProportions(l.Proportions.MasterSlave[2], py, idxms, idxms^1) } // Set slave-slave proportions if d.Left { - l.Manager.SetProportions(l.Proportions.SlaveSlave, px, idxss, idxss-1) + l.Manager.SetProportions(l.Proportions.SlaveSlave[ssize], px, idxss, idxss-1) } else if d.Right { - l.Manager.SetProportions(l.Proportions.SlaveSlave, px, idxss, idxss+1) + l.Manager.SetProportions(l.Proportions.SlaveSlave[ssize], px, idxss, idxss+1) } } } diff --git a/layout/vertical.go b/layout/vertical.go index b889ab5..bb173bf 100644 --- a/layout/vertical.go +++ b/layout/vertical.go @@ -10,34 +10,55 @@ import ( ) type VerticalLayout struct { - *store.Manager // Layout store manager Name string // Layout name + *store.Manager // Layout store manager } -func CreateVerticalLeftLayout(deskNum uint, screenNum uint) *VerticalLayout { - manager := store.CreateManager(deskNum, screenNum) - manager.SetProportions(manager.Proportions.MasterSlave, common.Config.Proportion, 0, 1) - - return &VerticalLayout{ - Manager: manager, +func CreateVerticalLeftLayout(loc store.Location) *VerticalLayout { + layout := &VerticalLayout{ Name: "vertical-left", + Manager: store.CreateManager(loc), } + layout.Reset() + return layout } -func CreateVerticalRightLayout(deskNum uint, screenNum uint) *VerticalLayout { - manager := store.CreateManager(deskNum, screenNum) - manager.SetProportions(manager.Proportions.MasterSlave, common.Config.Proportion, 1, 0) - - return &VerticalLayout{ - Manager: manager, +func CreateVerticalRightLayout(loc store.Location) *VerticalLayout { + layout := &VerticalLayout{ Name: "vertical-right", + Manager: store.CreateManager(loc), } + layout.Reset() + return layout +} + +func (l *VerticalLayout) Reset() { + mg := store.CreateManager(*l.Location) + + // Reset number of masters + for l.Masters.MaxAllowed < mg.Masters.MaxAllowed { + l.IncreaseMaster() + } + for l.Masters.MaxAllowed > mg.Masters.MaxAllowed { + l.DecreaseMaster() + } + + // Reset number of slaves + for l.Slaves.MaxAllowed < mg.Slaves.MaxAllowed { + l.IncreaseSlave() + } + for l.Slaves.MaxAllowed > mg.Slaves.MaxAllowed { + l.DecreaseSlave() + } + + // Reset layout proportions + l.Manager.Proportions = mg.Proportions } func (l *VerticalLayout) Apply() { clients := l.Clients(store.Stacked) - dx, dy, dw, dh := store.DesktopDimensions(l.ScreenNum) + dx, dy, dw, dh := store.DesktopDimensions(l.Location.ScreenNum) gap := common.Config.WindowGapSize mmax := l.Masters.MaxAllowed @@ -48,11 +69,11 @@ func (l *VerticalLayout) Apply() { csize := len(clients) mx := dx - mw := int(math.Round(float64(dw) * l.Proportions.MasterSlave[0])) + mw := int(math.Round(float64(dw) * l.Proportions.MasterSlave[2][0])) sx := mx + mw sw := dw - mw - log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.DeskNum, "-", l.ScreenNum, "]") + log.Info("Tile ", csize, " windows with ", l.Name, " layout [workspace-", l.Location.DeskNum, "-", l.Location.ScreenNum, "]") // Swap values if master is on right if l.Name == "vertical-right" && csize > mmax { @@ -95,7 +116,7 @@ func (l *VerticalLayout) Apply() { c.LimitDimensions(minw, minh) // Move and resize master - mp := l.Proportions.MasterMaster[i%msize] + mp := l.Proportions.MasterMaster[msize][i%msize] mh := int(math.Round(float64(dh-(msize+1)*gap) * mp)) c.MoveResize(mx+gap, my, mw-2*gap, mh) @@ -133,7 +154,7 @@ func (l *VerticalLayout) Apply() { c.LimitDimensions(minw, minh) // Move and resize slave - sp := l.Proportions.SlaveSlave[i%ssize] + sp := l.Proportions.SlaveSlave[ssize][i%ssize] sh := int(math.Round(float64(dh-(ssize+1)*gap) * sp)) c.MoveResize(sx, sy, sw-gap, sh) @@ -144,7 +165,7 @@ func (l *VerticalLayout) Apply() { } func (l *VerticalLayout) UpdateProportions(c *store.Client, d *store.Directions) { - _, _, dw, dh := store.DesktopDimensions(l.ScreenNum) + _, _, dw, dh := store.DesktopDimensions(l.Location.ScreenNum) _, _, cw, ch := c.OuterGeometry() gap := common.Config.WindowGapSize @@ -178,14 +199,14 @@ func (l *VerticalLayout) UpdateProportions(c *store.Client, d *store.Directions) // Set master-slave proportions if d.Left { - l.Manager.SetProportions(l.Proportions.MasterSlave, px, idxms, idxms^1) + l.Manager.SetProportions(l.Proportions.MasterSlave[2], px, idxms, idxms^1) } // Set master-master proportions if d.Top { - l.Manager.SetProportions(l.Proportions.MasterMaster, py, idxmm, idxmm-1) + l.Manager.SetProportions(l.Proportions.MasterMaster[msize], py, idxmm, idxmm-1) } else if d.Bottom { - l.Manager.SetProportions(l.Proportions.MasterMaster, py, idxmm, idxmm+1) + l.Manager.SetProportions(l.Proportions.MasterMaster[msize], py, idxmm, idxmm+1) } } else { px := float64(cw+gap) / float64(dw) @@ -194,14 +215,14 @@ func (l *VerticalLayout) UpdateProportions(c *store.Client, d *store.Directions) // Set master-slave proportions if d.Right { - l.Manager.SetProportions(l.Proportions.MasterSlave, px, idxms, idxms^1) + l.Manager.SetProportions(l.Proportions.MasterSlave[2], px, idxms, idxms^1) } // Set slave-slave proportions if d.Top { - l.Manager.SetProportions(l.Proportions.SlaveSlave, py, idxss, idxss-1) + l.Manager.SetProportions(l.Proportions.SlaveSlave[ssize], py, idxss, idxss-1) } else if d.Bottom { - l.Manager.SetProportions(l.Proportions.SlaveSlave, py, idxss, idxss+1) + l.Manager.SetProportions(l.Proportions.SlaveSlave[ssize], py, idxss, idxss+1) } } } diff --git a/main.go b/main.go index 3698e1a..ba12db9 100644 --- a/main.go +++ b/main.go @@ -76,11 +76,8 @@ func run() { } }() - // Create workspaces and tracker - workspaces := desktop.CreateWorkspaces() - tracker := desktop.CreateTracker(workspaces) - - // Show initial layout + // Create tracker + tracker := desktop.CreateTracker() ws := tracker.ActiveWorkspace() if !ws.Disabled() { ui.ShowLayout(ws) diff --git a/store/client.go b/store/client.go index 20a574f..7e2a28d 100644 --- a/store/client.go +++ b/store/client.go @@ -1,6 +1,7 @@ package store import ( + "fmt" "os" "reflect" "regexp" @@ -84,7 +85,7 @@ func CreateClient(w xproto.Window) *Client { Latest: GetInfo(w), } - // Read client geometry from cache + // Read client from cache cached := c.Read() // Overwrite states, geometry and location @@ -95,13 +96,13 @@ func CreateClient(w xproto.Window) *Client { c.Cached.Dimensions.Geometry = geom c.Cached.Location.ScreenNum = GetScreenNum(geom.Rect) - c.Latest.States = cached.States - c.Latest.Dimensions.Geometry = geom - c.Latest.Location.ScreenNum = GetScreenNum(geom.Rect) - // Restore window position c.Restore(Cached) + c.Latest.States = c.Cached.States + c.Latest.Dimensions.Geometry = c.Cached.Dimensions.Geometry + c.Latest.Location.ScreenNum = c.Cached.Location.ScreenNum + return c } @@ -198,23 +199,20 @@ func (c *Client) Update() { // Update client info c.Latest = info - - // Write client cache - c.Write() } func (c *Client) Write() { - if common.Args.Cache == "0" { + if !common.CacheEnabled() { return } // Obtain cache object cache := c.Cache() - // Parse client info + // Parse client cache data, err := json.MarshalIndent(cache.Data, "", " ") if err != nil { - log.Warn("Error parsing client info [", c.Latest.Class, "]") + log.Warn("Error parsing client cache [", c.Latest.Class, "]") return } @@ -230,14 +228,14 @@ func (c *Client) Write() { } func (c *Client) Read() *Info { - if common.Args.Cache == "0" { + if !common.CacheEnabled() { return c.Latest } // Obtain cache object cache := c.Cache() - // Read client info + // Read client cache path := filepath.Join(cache.Folder, cache.Name) data, err := os.ReadFile(path) if os.IsNotExist(err) { @@ -245,9 +243,9 @@ func (c *Client) Read() *Info { return c.Latest } - // Parse client info - var info *Info - err = json.Unmarshal([]byte(data), &info) + // Parse client cache + cached := &Info{} + err = json.Unmarshal([]byte(data), &cached) if err != nil { log.Warn("Error reading client cache [", c.Latest.Class, "]") return c.Latest @@ -255,22 +253,23 @@ func (c *Client) Read() *Info { log.Debug("Read client cache data ", cache.Name, " [", c.Latest.Class, "]") - return info + return cached } func (c *Client) Cache() common.Cache[*Info] { + name := c.Latest.Class + hash := fmt.Sprintf("%s-%d", c.Latest.Class, c.Latest.Location.DeskNum) // Create client cache folder - folder := filepath.Join(common.Args.Cache, "clients", c.Latest.Class) + folder := filepath.Join(common.Args.Cache, Displays.Name, "clients", name) if _, err := os.Stat(folder); os.IsNotExist(err) { - os.MkdirAll(folder, 0700) + os.MkdirAll(folder, 0755) } // Create client cache object - hash := common.Hash(c.Latest.Class) cache := common.Cache[*Info]{ Folder: folder, - Name: hash + ".json", + Name: common.Hash(hash) + ".json", Data: c.Latest, } @@ -289,7 +288,7 @@ func (c *Client) Restore(flag uint8) { motif.WmHintsSet(X, c.Win.Id, &c.Cached.Dimensions.Hints.Motif) // Restore window states - if common.IsInList("_NET_WM_STATE_STICKY", c.Latest.States) { + if common.IsInList("_NET_WM_STATE_STICKY", c.Cached.States) { ewmh.WmStateReq(X, c.Win.Id, 1, "_NET_WM_STATE_STICKY") ewmh.WmDesktopSet(X, c.Win.Id, ^uint(0)) } diff --git a/store/manager.go b/store/manager.go index f46dc1d..2e62cfd 100644 --- a/store/manager.go +++ b/store/manager.go @@ -1,6 +1,7 @@ package store import ( + "fmt" "math" "github.com/leukipp/cortile/v2/common" @@ -9,9 +10,9 @@ import ( ) type Manager struct { - DeskNum uint // Index of managed desktop - ScreenNum uint // Index of managed screen - Proportions *Proportions // Layout proportions of window clients + Name string // Manager name with window clients + Location *Location // Manager workspace and screen location + Proportions *Proportions // Manager proportions of window clients Masters *Clients // List of master window clients Slaves *Clients // List of slave window clients } @@ -24,13 +25,13 @@ type Directions struct { } type Proportions struct { - MasterSlave []float64 // Master-slave proportions - MasterMaster []float64 // Master-master proportions - SlaveSlave []float64 // Slave-slave proportions + MasterSlave map[int][]float64 // Master-slave proportions + MasterMaster map[int][]float64 // Master-master proportions + SlaveSlave map[int][]float64 // Slave-slave proportions } type Clients struct { - Items []*Client // List of stored window clients + Items []*Client `json:"-"` // List of stored window clients MaxAllowed int // Currently maximum allowed clients } @@ -39,10 +40,10 @@ const ( Visible uint8 = 2 // Flag for visible (top) clients ) -func CreateManager(deskNum uint, screenNum uint) *Manager { +func CreateManager(loc Location) *Manager { return &Manager{ - DeskNum: deskNum, - ScreenNum: screenNum, + Name: fmt.Sprintf("manager-%d-%d", loc.DeskNum, loc.ScreenNum), + Location: &loc, Proportions: &Proportions{ MasterSlave: calcProportions(2), MasterMaster: calcProportions(common.Config.WindowMastersMax), @@ -50,11 +51,11 @@ func CreateManager(deskNum uint, screenNum uint) *Manager { }, Masters: &Clients{ Items: make([]*Client, 0), - MaxAllowed: int(math.Min(float64(common.Config.WindowMastersMax), 1)), + MaxAllowed: 1, }, Slaves: &Clients{ Items: make([]*Client, 0), - MaxAllowed: int(math.Max(float64(common.Config.WindowSlavesMax), 1)), + MaxAllowed: common.Config.WindowSlavesMax, }, } } @@ -64,39 +65,39 @@ func (mg *Manager) AddClient(c *Client) { return } - log.Debug("Add client for manager [", c.Latest.Class, ", workspace-", mg.DeskNum, "-", mg.ScreenNum, "]") + log.Debug("Add client for manager [", c.Latest.Class, ", ", mg.Name, "]") // Fill up master area then slave area if len(mg.Masters.Items) < mg.Masters.MaxAllowed { - mg.updateMasters(addClient(mg.Masters.Items, c)) + mg.Masters.Items = addClient(mg.Masters.Items, c) } else { - mg.updateSlaves(addClient(mg.Slaves.Items, c)) + mg.Slaves.Items = addClient(mg.Slaves.Items, c) } } func (mg *Manager) RemoveClient(c *Client) { - log.Debug("Remove client from manager [", c.Latest.Class, ", workspace-", mg.DeskNum, "-", mg.ScreenNum, "]") + log.Debug("Remove client from manager [", c.Latest.Class, ", ", mg.Name, "]") // Remove master window mi := mg.Index(mg.Masters, c) if mi >= 0 { if len(mg.Slaves.Items) > 0 { mg.SwapClient(mg.Masters.Items[mi], mg.Slaves.Items[0]) - mg.updateSlaves(mg.Slaves.Items[1:]) + mg.Slaves.Items = mg.Slaves.Items[1:] } else { - mg.updateMasters(removeClient(mg.Masters.Items, mi)) + mg.Masters.Items = removeClient(mg.Masters.Items, mi) } } // Remove slave window si := mg.Index(mg.Slaves, c) if si >= 0 { - mg.updateSlaves(removeClient(mg.Slaves.Items, si)) + mg.Slaves.Items = removeClient(mg.Slaves.Items, si) } } func (mg *Manager) MakeMaster(c *Client) { - log.Info("Make window master [", c.Latest.Class, ", workspace-", mg.DeskNum, "-", mg.ScreenNum, "]") + log.Info("Make window master [", c.Latest.Class, ", ", mg.Name, "]") // Swap window with first master if len(mg.Masters.Items) > 0 { @@ -105,7 +106,7 @@ func (mg *Manager) MakeMaster(c *Client) { } func (mg *Manager) SwapClient(c1 *Client, c2 *Client) { - log.Info("Swap clients [", c1.Latest.Class, "-", c2.Latest.Class, ", workspace-", mg.DeskNum, "-", mg.ScreenNum, "]") + log.Info("Swap clients [", c1.Latest.Class, "-", c2.Latest.Class, ", ", mg.Name, "]") mIndex1 := mg.Index(mg.Masters, c1) sIndex1 := mg.Index(mg.Slaves, c1) @@ -191,8 +192,8 @@ func (mg *Manager) IncreaseMaster() { // Increase master area if len(mg.Slaves.Items) > 1 && mg.Masters.MaxAllowed < common.Config.WindowMastersMax { mg.Masters.MaxAllowed += 1 - mg.updateMasters(append(mg.Masters.Items, mg.Slaves.Items[0])) - mg.updateSlaves(mg.Slaves.Items[1:]) + mg.Masters.Items = append(mg.Masters.Items, mg.Slaves.Items[0]) + mg.Slaves.Items = mg.Slaves.Items[1:] } log.Info("Increase masters to ", mg.Masters.MaxAllowed) @@ -203,8 +204,8 @@ func (mg *Manager) DecreaseMaster() { // Decrease master area if len(mg.Masters.Items) > 0 { mg.Masters.MaxAllowed -= 1 - mg.updateSlaves(append([]*Client{mg.Masters.Items[len(mg.Masters.Items)-1]}, mg.Slaves.Items...)) - mg.updateMasters(mg.Masters.Items[:len(mg.Masters.Items)-1]) + mg.Slaves.Items = append([]*Client{mg.Masters.Items[len(mg.Masters.Items)-1]}, mg.Slaves.Items...) + mg.Masters.Items = mg.Masters.Items[:len(mg.Masters.Items)-1] } log.Info("Decrease masters to ", mg.Masters.MaxAllowed) @@ -215,7 +216,6 @@ func (mg *Manager) IncreaseSlave() { // Increase slave area if mg.Slaves.MaxAllowed < common.Config.WindowSlavesMax { mg.Slaves.MaxAllowed += 1 - mg.updateSlaves(mg.Slaves.Items) } log.Info("Increase slaves to ", mg.Slaves.MaxAllowed) @@ -226,7 +226,6 @@ func (mg *Manager) DecreaseSlave() { // Decrease slave area if mg.Slaves.MaxAllowed > 1 { mg.Slaves.MaxAllowed -= 1 - mg.updateSlaves(mg.Slaves.Items) } log.Info("Decrease slaves to ", mg.Slaves.MaxAllowed) @@ -234,18 +233,18 @@ func (mg *Manager) DecreaseSlave() { func (mg *Manager) IncreaseProportion() { precision := 1.0 / common.Config.ProportionStep + proportion := math.Round(mg.Proportions.MasterSlave[2][0]*precision)/precision + common.Config.ProportionStep // Increase root proportion - proportion := math.Round(mg.Proportions.MasterSlave[0]*precision)/precision + common.Config.ProportionStep - mg.SetProportions(mg.Proportions.MasterSlave, proportion, 0, 1) + mg.SetProportions(mg.Proportions.MasterSlave[2], proportion, 0, 1) } func (mg *Manager) DecreaseProportion() { precision := 1.0 / common.Config.ProportionStep + proportion := math.Round(mg.Proportions.MasterSlave[2][0]*precision)/precision - common.Config.ProportionStep // Decrease root proportion - proportion := math.Round(mg.Proportions.MasterSlave[0]*precision)/precision - common.Config.ProportionStep - mg.SetProportions(mg.Proportions.MasterSlave, proportion, 0, 1) + mg.SetProportions(mg.Proportions.MasterSlave[2], proportion, 0, 1) } func (mg *Manager) SetProportions(ps []float64, pi float64, i int, j int) bool { @@ -332,19 +331,8 @@ func (mg *Manager) Clients(flag uint8) []*Client { return append(mg.Masters.Items, mg.Slaves.Items...) case Visible: return append(mg.Visible(mg.Masters), mg.Visible(mg.Slaves)...) - default: - return make([]*Client, 0) } -} - -func (mg *Manager) updateMasters(cs []*Client) { - mg.Masters.Items = mg.Ordered(&Clients{Items: cs}) - mg.Proportions.MasterMaster = calcProportions(int(math.Min(float64(len(mg.Masters.Items)), float64(mg.Masters.MaxAllowed)))) -} - -func (mg *Manager) updateSlaves(cs []*Client) { - mg.Slaves.Items = mg.Ordered(&Clients{Items: cs}) - mg.Proportions.SlaveSlave = calcProportions(int(math.Min(float64(len(mg.Slaves.Items)), float64(mg.Slaves.MaxAllowed)))) + return make([]*Client, 0) } func addClient(cs []*Client, c *Client) []*Client { @@ -355,10 +343,12 @@ func removeClient(cs []*Client, i int) []*Client { return append(cs[:i], cs[i+1:]...) } -func calcProportions(n int) []float64 { - p := []float64{} - for i := 0; i < n; i++ { - p = append(p, 1.0/float64(n)) +func calcProportions(n int) map[int][]float64 { + p := map[int][]float64{} + for i := 1; i <= n; i++ { + for j := 1; j <= i; j++ { + p[i] = append(p[i], 1.0/float64(i)) + } } return p } diff --git a/store/root.go b/store/root.go index 1a35c7d..c279ddb 100644 --- a/store/root.go +++ b/store/root.go @@ -1,6 +1,9 @@ package store import ( + "fmt" + "sort" + "strings" "time" "github.com/BurntSushi/xgb/randr" @@ -37,6 +40,7 @@ var ( ) type Heads struct { + Name string // Unique heads name (display summary) Screens []Head // Screen dimensions (full display size) Desktops []Head // Desktop dimensions (desktop without panels) } @@ -161,6 +165,7 @@ func ClientListStackingGet(X *xgbutil.XUtil) []xproto.Window { } func DisplaysGet(X *xgbutil.XUtil) Heads { + var name string // Get geometry of root window rGeom, err := xwindow.New(X, X.RootWin()).Geometry() @@ -172,13 +177,20 @@ func DisplaysGet(X *xgbutil.XUtil) Heads { screens := PhysicalHeadsGet(X) desktops := PhysicalHeadsGet(X) - // Get bounding rects - rects := []xrect.Rect{} + // Get heads name + for _, screen := range screens { + x, y, w, h := screen.Rect.Pieces() + name += fmt.Sprintf("%s-%d-%d-%d-%d-%d-", screen.Name, screen.Id, x, y, w, h) + } + name = strings.Trim(name, "-") + + // Get desktop rects + dRects := []xrect.Rect{} for _, desktop := range desktops { - rects = append(rects, desktop.Rect) + dRects = append(dRects, desktop.Rect) } - // Adjust desktop geometry + // Account for desktop panels for _, win := range Windows { strut, err := ewmh.WmStrutPartialGet(X, win) if err != nil { @@ -186,7 +198,8 @@ func DisplaysGet(X *xgbutil.XUtil) Heads { } // Apply in place struts to desktop - xrect.ApplyStrut(rects, uint(rGeom.Width()), uint(rGeom.Height()), + _, _, w, h := rGeom.Pieces() + xrect.ApplyStrut(dRects, uint(w), uint(h), strut.Left, strut.Right, strut.Top, strut.Bottom, strut.LeftStartY, strut.LeftEndY, strut.RightStartY, strut.RightEndY, strut.TopStartX, strut.TopEndX, strut.BottomStartX, strut.BottomEndX, @@ -199,7 +212,11 @@ func DisplaysGet(X *xgbutil.XUtil) Heads { log.Info("Screens ", screens) log.Info("Desktops ", desktops) - return Heads{Screens: screens, Desktops: desktops} + return Heads{ + Name: name, + Screens: screens, + Desktops: desktops, + } } func PhysicalHeadsGet(X *xgbutil.XUtil) []Head { @@ -267,6 +284,11 @@ func PhysicalHeadsGet(X *xgbutil.XUtil) []Head { } } + // Sort output heads + sort.Slice(heads, func(i, j int) bool { + return heads[i].X() < heads[j].X() + }) + return heads }