From 26ff3a02da515fcc442057b751ed45301b97feb4 Mon Sep 17 00:00:00 2001 From: leukipp Date: Fri, 12 Apr 2024 10:39:33 +0200 Subject: [PATCH] feat: extended external communication via dbus #51 --- common/args.go | 114 +++++---- common/info.go | 91 +++++++ common/utils.go | 81 +++++-- desktop/tracker.go | 368 +++++++++++++++------------- desktop/workspace.go | 16 +- input/action.go | 208 +++++++++------- input/dbusbinding.go | 540 +++++++++++++++++++++++++++++++++++++++++ input/keybinding.go | 13 +- input/mousebinding.go | 35 +-- input/signalbinding.go | 2 +- input/socketbinding.go | 12 +- input/traybinding.go | 60 +++-- layout/fullscreen.go | 4 +- layout/horizontal.go | 24 +- layout/vertical.go | 24 +- main.go | 60 ++++- store/client.go | 180 ++++++-------- store/corner.go | 71 ++++-- store/manager.go | 70 +++--- store/root.go | 265 +++++++++++++------- ui/gui.go | 19 +- 21 files changed, 1572 insertions(+), 685 deletions(-) create mode 100644 common/info.go create mode 100644 input/dbusbinding.go diff --git a/common/args.go b/common/args.go index f080b90..bbc2b04 100644 --- a/common/args.go +++ b/common/args.go @@ -4,28 +4,14 @@ import ( "flag" "fmt" "os" - "path" - "strings" - "net/http" "path/filepath" ) var ( - Build BuildInfo // Build information - Args Arguments // Parsed arguments + Args Arguments // Parsed arguments ) -type BuildInfo struct { - Name string // Build name - Version string // Build version - Commit string // Build commit - Date string // Build date - Source string // Build source - Latest string // Build latest - Summary string // Build summary -} - type Arguments struct { Cache string // Argument for cache folder path Config string // Argument for config file path @@ -35,26 +21,21 @@ type Arguments struct { VVV bool // Argument for very very verbose mode VV bool // Argument for very verbose mode V bool // Argument for verbose mode + Dbus struct { + Listen bool // Argument for dbus listen flag + Method string // Argument for dbus method name + Property string // Argument for dbus property name + Args []string // Argument for dbus method arguments + } } -func InitArgs(name, version, commit, date, source string) { - - // Build information - Build = BuildInfo{ - Name: name, - Version: version, - Commit: Truncate(commit, 7), - Date: date, - Source: source, - Latest: version, - } - Build.Summary = fmt.Sprintf("%s v%s-%s, built on %s", Build.Name, Build.Version, Build.Commit, Build.Date) +func InitArgs(introspect map[string][]string) { // Command line arguments flag.StringVar(&Args.Cache, "cache", filepath.Join(CacheFolderPath(Build.Name), Build.Version), "cache folder path") flag.StringVar(&Args.Config, "config", filepath.Join(ConfigFolderPath(Build.Name), "config.toml"), "config file path") flag.StringVar(&Args.Lock, "lock", filepath.Join(os.TempDir(), fmt.Sprintf("%s.lock", Build.Name)), "lock file path") - flag.StringVar(&Args.Sock, "sock", filepath.Join(os.TempDir(), fmt.Sprintf("%s.sock", Build.Name)), "sock file path") + flag.StringVar(&Args.Sock, "sock", filepath.Join(os.TempDir(), fmt.Sprintf("%s.sock", Build.Name)), "sock file path (deprecated)") flag.StringVar(&Args.Log, "log", filepath.Join(os.TempDir(), fmt.Sprintf("%s.log", Build.Name)), "log file path") flag.BoolVar(&Args.VVV, "vvv", false, "very very verbose mode") flag.BoolVar(&Args.VV, "vv", false, "very verbose mode") @@ -67,32 +48,69 @@ func InitArgs(name, version, commit, date, source string) { } flag.Parse() - // Version checker - suspended := false - if _, err := os.Stat(filepath.Join(Args.Cache, "no-version-check")); !os.IsNotExist(err) { - suspended = true - } - if !suspended && VersionToInt(Build.Version) > 0 { - Build.Latest = Latest(source) - if VersionToInt(Build.Latest) > VersionToInt(Build.Version) { - Build.Summary = fmt.Sprintf("%s, >>> %s v%s available <<<", Build.Summary, Build.Name, Build.Latest) + // Subcommand line arguments + dbus := flag.NewFlagSet("dbus", flag.ExitOnError) + dbus.BoolVar(&Args.Dbus.Listen, "listen", false, "dbus listen mode") + dbus.StringVar(&Args.Dbus.Method, "method", "", "dbus method caller") + dbus.StringVar(&Args.Dbus.Property, "property", "", "dbus property reader") + Args.Dbus.Args = []string{} + + // Subcommand line usage text + if len(os.Args) > 1 { + switch os.Args[1] { + case "dbus": + dbus.Usage = func() { + fmt.Fprintf(dbus.Output(), "%s\n\nUsage:\n", Build.Summary) + dbus.PrintDefaults() + + if len(introspect) > 0 { + if methods, ok := introspect["Methods"]; ok { + fmt.Fprintf(dbus.Output(), "\nMethods:\n") + for _, method := range methods { + fmt.Fprintf(dbus.Output(), " %s dbus -method %s\n", Build.Name, method) + } + } + if properties, ok := introspect["Properties"]; ok { + fmt.Fprintf(dbus.Output(), "\nProperties:\n") + for _, property := range properties { + fmt.Fprintf(dbus.Output(), " %s dbus -property %s\n", Build.Name, property) + } + } + } else { + fmt.Fprintf(dbus.Output(), "\n>>> start %s to see further information's <<<\n", Build.Name) + } + } + Args.Dbus.Args = ParseArgs(dbus, os.Args[2:]) + } + + // Check number of arguments + if flag.NArg() == 1 { + dbus.Usage() + os.Exit(1) } } } -func Latest(source string) string { +func ParseArgs(flags *flag.FlagSet, args []string) []string { + pargs := []string{} - // Request latest version from github - res, err := http.Get(source + "/releases/latest") - if err != nil { - return Build.Version - } + for { + // Parse named arguments + flags.Parse(args) - // Parse latest version from redirect url - version := path.Base(res.Request.URL.Path) - if !strings.HasPrefix(version, "v") { - return Build.Version + // Check named arguments + args = args[len(args)-flags.NArg():] + if len(args) == 0 { + break + } + + // Check positional arguments + pargs = append(pargs, args[0]) + args = args[1:] } - return version[1:] + // Parse positional arguments + flags.Parse(pargs) + + return flags.Args() } diff --git a/common/info.go b/common/info.go new file mode 100644 index 0000000..b322471 --- /dev/null +++ b/common/info.go @@ -0,0 +1,91 @@ +package common + +import ( + "fmt" + "os" + "path" + "runtime" + "strings" + + "net/http" + "path/filepath" +) + +var ( + Process ProcessInfo // Process information + Build BuildInfo // Build information +) + +type ProcessInfo struct { + Id int // Process id + Path string // Process path + Host string // Process host + System string // Process system +} + +type BuildInfo struct { + Name string // Build name + Version string // Build version + Commit string // Build commit + Date string // Build date + Source string // Build source + Latest string // Build latest + Summary string // Build summary +} + +func InitInfo(name, version, commit, date, source string) { + + // Process information + Process = ProcessInfo{ + Id: os.Getpid(), + System: fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH), + } + Process.Host, _ = os.Hostname() + Process.Path, _ = os.Executable() + + // Build information + Build = BuildInfo{ + Name: name, + Version: version, + Commit: TruncateString(commit, 7), + Date: date, + Source: source, + Latest: version, + } + Build.Summary = fmt.Sprintf("%s v%s-%s, built on %s", Build.Name, Build.Version, Build.Commit, Build.Date) + + // Check latest version + if !Feature("disable-version-check") && VersionToInt(Build.Version) > 0 { + Build.Latest = Latest(source) + if VersionToInt(Build.Latest) > VersionToInt(Build.Version) { + Build.Summary = fmt.Sprintf("%s, >>> %s v%s available <<<", Build.Summary, Build.Name, Build.Latest) + } + } +} + +func Latest(source string) string { + + // Request latest version from github + res, err := http.Get(source + "/releases/latest") + if err != nil { + return Build.Version + } + + // Parse latest version from redirect url + version := path.Base(res.Request.URL.Path) + if !strings.HasPrefix(version, "v") { + return Build.Version + } + + return version[1:] +} + +func Feature(name string) bool { + file := filepath.Join(Args.Cache, name) + + // Check if feature file exists + if _, err := os.Stat(file); !os.IsNotExist(err) { + return true + } + return false +} diff --git a/common/utils.go b/common/utils.go index 5f54644..b22fe3d 100644 --- a/common/utils.go +++ b/common/utils.go @@ -1,7 +1,6 @@ package common import ( - "reflect" "regexp" "strconv" "strings" @@ -9,36 +8,87 @@ import ( "crypto/sha1" "encoding/hex" - "github.com/jezek/xgb/render" - "github.com/jezek/xgbutil/xrect" ) -func Hash(text string) string { +type Point struct { + X int // Object point x position + Y int // Object point y position +} + +func CreatePoint(x int, y int) *Point { + return &Point{ + X: x, + Y: y, + } +} + +type Geometry struct { + X int // Object geometry x position + Y int // Object geometry y position + Width int // Object geometry width dimension + Height int // Object geometry height dimension +} + +func CreateGeometry(r xrect.Rect) *Geometry { + return &Geometry{ + X: r.X(), + Y: r.Y(), + Width: r.Width(), + Height: r.Height(), + } +} + +func (g *Geometry) Center() Point { + return *CreatePoint(g.X+g.Width/2, g.Y+g.Height/2) +} + +func (g *Geometry) Rect() xrect.Rect { + return xrect.New(g.X, g.Y, g.Width, g.Height) +} + +func (g *Geometry) Pieces() (int, int, int, int) { + return g.X, g.Y, g.Width, g.Height +} + +type Map = map[string]interface{} // Generic map type + +func HashString(text string) string { hash := sha1.New() hash.Write([]byte(text)) return hex.EncodeToString(hash.Sum(nil)) } -func Truncate(s string, max int) string { +func TruncateString(s string, max int) string { if max > len(s) { return s } return s[:max] } -func IsType(a interface{}, b interface{}) bool { - return reflect.TypeOf(a) == reflect.TypeOf(b) -} - -func IsZero(items []uint) bool { +func AllZero(items []uint) bool { mask := uint(0) - for _, s := range items { - mask |= s + for _, item := range items { + mask |= item } return mask == 0 } +func AllTrue(items []bool) bool { + mask := true + for _, item := range items { + mask = mask && item + } + return mask +} + +func IsInsideRect(p Point, g Geometry) bool { + x, y, w, h := g.Pieces() + xInRect := int(p.X) >= x && int(p.X) <= (x+w) + yInRect := int(p.Y) >= y && int(p.Y) <= (y+h) + return xInRect && yInRect +} + func IsInList(item string, items []string) bool { for i := 0; i < len(items); i++ { if items[i] == item { @@ -48,13 +98,6 @@ func IsInList(item string, items []string) bool { return false } -func IsInsideRect(p render.Pointfix, r xrect.Rect) bool { - x, y, w, h := r.Pieces() - xInRect := int(p.X) >= x && int(p.X) <= (x+w) - yInRect := int(p.Y) >= y && int(p.Y) <= (y+h) - return xInRect && yInRect -} - func ReverseList[T any](items []T) []T { for i, j := 0, len(items)-1; i < j; { items[i], items[j] = items[j], items[i] diff --git a/desktop/tracker.go b/desktop/tracker.go index 3b868d9..c0674e5 100644 --- a/desktop/tracker.go +++ b/desktop/tracker.go @@ -1,13 +1,11 @@ package desktop import ( - "strings" "time" "github.com/jezek/xgb/xproto" "github.com/jezek/xgbutil" - "github.com/jezek/xgbutil/ewmh" "github.com/jezek/xgbutil/xevent" "github.com/jezek/xgbutil/xprop" @@ -20,42 +18,50 @@ import ( type Tracker struct { Clients map[xproto.Window]*store.Client // List of tracked clients Workspaces map[store.Location]*Workspace // List of workspaces per location - Channel *Channel // Helper for channel communication - Handler *Handler // Helper for event handlers + Channels *Channels // Helper for channel communication + Handlers *Handlers // Helper for event handlers } -type Channel struct { +type Channels struct { Event chan string // Channel for events Action chan string // Channel for actions } +type Handlers struct { + Timer *time.Timer // Timer to handle delayed structure events + ResizeClient *Handler // Stores client for proportion change + MoveClient *Handler // Stores client for tiling after move + SwapClient *Handler // Stores clients for window swap + SwapScreen *Handler // Stores client for screen swap +} + type Handler struct { - Timer *time.Timer // Timer to handle delayed structure events - ResizeClient *HandlerClient // Stores client for proportion change - MoveClient *HandlerClient // Stores client for tiling after move - SwapClient *HandlerClient // Stores clients for window swap - SwapScreen *HandlerClient // Stores client for screen swap + Dragging bool // Indicates pointer dragging event + Source interface{} // Stores moved/resized client + Target interface{} // Stores client/workspace +} + +func (h *Handler) Active() bool { + return h.Source != nil } -type HandlerClient struct { - Active bool // Indicates active handler event - Source *store.Client // Stores moving/resizing client - Target *store.Client // Stores hovered client +func (h *Handler) Reset() { + *h = Handler{} } func CreateTracker() *Tracker { tr := Tracker{ Clients: make(map[xproto.Window]*store.Client), Workspaces: CreateWorkspaces(), - Channel: &Channel{ + Channels: &Channels{ Event: make(chan string), Action: make(chan string), }, - Handler: &Handler{ - ResizeClient: &HandlerClient{}, - MoveClient: &HandlerClient{}, - SwapClient: &HandlerClient{}, - SwapScreen: &HandlerClient{}, + Handlers: &Handlers{ + ResizeClient: &Handler{}, + MoveClient: &Handler{}, + SwapClient: &Handler{}, + SwapScreen: &Handler{}, }, } @@ -76,7 +82,7 @@ func (tr *Tracker) Update() { // Map trackable windows trackable := make(map[xproto.Window]bool) for _, w := range store.Windows.Stacked { - trackable[w] = tr.isTrackable(w) + trackable[w.Id] = tr.isTrackable(w.Id) } // Remove untrackable windows @@ -88,8 +94,8 @@ func (tr *Tracker) Update() { // Add trackable windows for _, w := range store.Windows.Stacked { - if trackable[w] { - tr.trackWindow(w) + if trackable[w.Id] { + tr.trackWindow(w.Id) } } } @@ -104,6 +110,9 @@ func (tr *Tracker) Reset() { // Reset workspaces tr.Workspaces = CreateWorkspaces() + + // Communicate workplace change + tr.Channels.Event <- "workplace_change" } func (tr *Tracker) Write() { @@ -117,30 +126,60 @@ func (tr *Tracker) Write() { for _, ws := range tr.Workspaces { ws.Write() } + + // Communicate windows change + tr.Channels.Event <- "windows_change" +} + +func (tr *Tracker) Tile(ws *Workspace) { + if ws.Disabled() { + return + } + + // Tile workspace + ws.Tile() + + // Communicate clients change + tr.Channels.Event <- "clients_change" + + // Communicate workspaces change + tr.Channels.Event <- "workspaces_change" } func (tr *Tracker) ActiveWorkspace() *Workspace { - location := store.Location{DeskNum: store.Workplace.CurrentDesk, ScreenNum: store.Workplace.CurrentScreen} + return tr.WorkspaceAt(store.Workplace.CurrentDesk, store.Workplace.CurrentScreen) +} + +func (tr *Tracker) ClientWorkspace(c *store.Client) *Workspace { + return tr.WorkspaceAt(c.Latest.Location.DeskNum, c.Latest.Location.ScreenNum) +} + +func (tr *Tracker) WorkspaceAt(deskNum uint, screenNum uint) *Workspace { + location := store.Location{DeskNum: deskNum, ScreenNum: screenNum} - // Validate active workspace + // Validate workspace ws := tr.Workspaces[location] if ws == nil { - log.Warn("Invalid active workspace [workspace-", location.DeskNum, "-", location.ScreenNum, "]") + log.Warn("Invalid workspace [workspace-", location.DeskNum, "-", location.ScreenNum, "]") } return ws } -func (tr *Tracker) ClientWorkspace(c *store.Client) *Workspace { - location := store.Location{DeskNum: c.Latest.Location.DeskNum, ScreenNum: c.Latest.Location.ScreenNum} +func (tr *Tracker) ClientAt(ws *Workspace, p common.Point) *store.Client { + mg := ws.ActiveLayout().GetManager() - // Validate client workspace - ws := tr.Workspaces[location] - if ws == nil { - log.Warn("Invalid client workspace [workspace-", location.DeskNum, "-", location.ScreenNum, "]") + // Check if point hovers visible client + for _, c := range mg.Clients(store.Visible) { + if c == nil { + continue + } + if common.IsInsideRect(p, c.Latest.Dimensions.Geometry) { + return c + } } - return ws + return nil } func (tr *Tracker) unlockClients() { @@ -163,13 +202,13 @@ func (tr *Tracker) trackWindow(w xproto.Window) bool { // Add new client c := store.CreateClient(w) - tr.Clients[c.Win.Id] = c + tr.Clients[c.Window.Id] = c ws := tr.ClientWorkspace(c) ws.AddClient(c) // Attach handlers tr.attachHandlers(c) - ws.Tile() + tr.Tile(ws) return true } @@ -192,65 +231,61 @@ func (tr *Tracker) untrackWindow(w xproto.Window) bool { // Remove client ws.RemoveClient(c) delete(tr.Clients, w) - ws.Tile() + + // Tile workspace + tr.Tile(ws) return true } func (tr *Tracker) handleMaximizedClient(c *store.Client) { - if !tr.isTracked(c.Win.Id) { + if !tr.isTracked(c.Window.Id) { return } // Client maximized - states, _ := ewmh.WmStateGet(store.X, c.Win.Id) - for _, state := range states { - if strings.HasPrefix(state, "_NET_WM_STATE_MAXIMIZED") { - ws := tr.ClientWorkspace(c) - if ws.Disabled() { - return - } - log.Debug("Client maximized handler fired [", c.Latest.Class, "]") + if store.IsMaximized(store.GetInfo(c.Window.Id)) { + ws := tr.ClientWorkspace(c) + if ws.Disabled() { + return + } + log.Debug("Client maximized handler fired [", c.Latest.Class, "]") - // Update client states - c.Update() + // Update client states + c.Update() - // Set fullscreen layout - c.UnMaximize() - tr.Channel.Action <- "layout_fullscreen" - c.Activate() + // Reset maximization + c.UnMaximize() - break + // Check window lifetime + if !c.IsNew() { + tr.Channels.Action <- "layout_fullscreen" } + store.ActiveWindowSet(store.X, c.Window) } } func (tr *Tracker) handleMinimizedClient(c *store.Client) { - if !tr.isTracked(c.Win.Id) { + if !tr.isTracked(c.Window.Id) { return } // Client minimized - states, _ := ewmh.WmStateGet(store.X, c.Win.Id) - for _, state := range states { - if state == "_NET_WM_STATE_HIDDEN" { - ws := tr.ClientWorkspace(c) - if ws.Disabled() { - return - } - log.Debug("Client minimized handler fired [", c.Latest.Class, "]") - - // Untrack client - tr.untrackWindow(c.Win.Id) - - break + if store.IsMinimized(store.GetInfo(c.Window.Id)) { + ws := tr.ClientWorkspace(c) + if ws.Disabled() { + return } + log.Debug("Client minimized handler fired [", c.Latest.Class, "]") + + // Untrack client + tr.untrackWindow(c.Window.Id) } } func (tr *Tracker) handleResizeClient(c *store.Client) { ws := tr.ClientWorkspace(c) - if ws.Disabled() || !tr.isTracked(c.Win.Id) || store.IsMaximized(c.Win.Id) { + if ws.Disabled() || !tr.isTracked(c.Window.Id) || store.IsMaximized(store.GetInfo(c.Window.Id)) { return } @@ -259,7 +294,7 @@ func (tr *Tracker) handleResizeClient(c *store.Client) { px, py, pw, ph := pGeom.Pieces() // Current dimensions - cGeom, err := c.Win.DecorGeometry() + cGeom, err := c.Window.Instance.DecorGeometry() if err != nil { return } @@ -268,23 +303,22 @@ func (tr *Tracker) handleResizeClient(c *store.Client) { // Check size changes resized := cw != pw || ch != ph moved := (cx != px || cy != py) && (cw == pw && ch == ph) - added := time.Since(c.Created) < 1000*time.Millisecond - if resized && !moved && !tr.Handler.MoveClient.Active { - al := ws.ActiveLayout() + if resized && !moved && !tr.Handlers.MoveClient.Active() { + pt := store.PointerUpdate(store.X) // Set client resize event - if !tr.Handler.ResizeClient.Active { - tr.Handler.ResizeClient = &HandlerClient{Active: true, Source: c} + if !tr.Handlers.ResizeClient.Active() { + tr.Handlers.ResizeClient = &Handler{Dragging: pt.Dragging(500), Source: c} } log.Debug("Client resize handler fired [", c.Latest.Class, "]") // Check window lifetime - if !added { + if tr.Handlers.ResizeClient.Dragging && !c.IsNew() { // Set client resize lock - if tr.Handler.ResizeClient.Active { - tr.Handler.ResizeClient.Source.Lock() + if tr.Handlers.ResizeClient.Active() { + tr.Handlers.ResizeClient.Source.(*store.Client).Lock() log.Debug("Client resize handler active [", c.Latest.Class, "]") } @@ -295,17 +329,17 @@ func (tr *Tracker) handleResizeClient(c *store.Client) { Bottom: cy == py && ch != ph, Left: cx != px, } - al.UpdateProportions(c, dir) + ws.ActiveLayout().UpdateProportions(c, dir) } // Tile workspace - ws.Tile() + tr.Tile(ws) } } func (tr *Tracker) handleMoveClient(c *store.Client) { ws := tr.ClientWorkspace(c) - if !tr.isTracked(c.Win.Id) || store.IsMaximized(c.Win.Id) { + if !tr.isTracked(c.Window.Id) || store.IsMaximized(store.GetInfo(c.Window.Id)) { return } @@ -314,7 +348,7 @@ func (tr *Tracker) handleMoveClient(c *store.Client) { px, py, pw, ph := pGeom.Pieces() // Current dimensions - cGeom, err := c.Win.DecorGeometry() + cGeom, err := c.Window.Instance.DecorGeometry() if err != nil { return } @@ -323,61 +357,62 @@ func (tr *Tracker) handleMoveClient(c *store.Client) { // Check position changes moved := cx != px || cy != py resized := cw != pw || ch != ph - active := c.Win.Id == store.Windows.Active - if active && moved && !resized && !tr.Handler.ResizeClient.Active { - mg := ws.ActiveLayout().GetManager() + if moved && !resized && !tr.Handlers.ResizeClient.Active() { pt := store.PointerUpdate(store.X) // Set client move event - if !tr.Handler.MoveClient.Active { - tr.Handler.MoveClient = &HandlerClient{Active: true, Source: c} + if !tr.Handlers.MoveClient.Active() { + tr.Handlers.MoveClient = &Handler{Dragging: pt.Dragging(500), Source: c} } log.Debug("Client move handler fired [", c.Latest.Class, "]") - // Check if pointer hovers another client - tr.Handler.SwapClient.Active = false - for _, co := range mg.Clients(store.Visible) { - if co == nil || c.Win.Id == co.Win.Id { - continue - } - - // Store moved client and hovered client - if common.IsInsideRect(pt.Position, co.Latest.Dimensions.Geometry.Rect) { - tr.Handler.SwapClient = &HandlerClient{Active: true, Source: c, Target: co} - log.Debug("Client move handler active [", c.Latest.Class, "-", co.Latest.Class, "]") - break - } + // Obtain targets based on dragging indicator + targetPoint := *common.CreatePoint(cx, cy) + if tr.Handlers.MoveClient.Dragging { + targetPoint = pt.Position + } + targetDesk := store.Workplace.CurrentDesk + targetScreen := store.ScreenNumGet(targetPoint) + + // Check if target point hovers another client + tr.Handlers.SwapClient.Reset() + if co := tr.ClientAt(ws, targetPoint); co != nil { + tr.Handlers.SwapClient = &Handler{Source: c, Target: co} + log.Debug("Client swap handler active [", c.Latest.Class, "-", co.Latest.Class, "]") } - // Check if pointer moves to another screen - tr.Handler.SwapScreen.Active = false - if c.Latest.Location.ScreenNum != store.Workplace.CurrentScreen { - tr.Handler.SwapScreen = &HandlerClient{Active: true, Source: c} + // Check if target point moves to another screen + tr.Handlers.SwapScreen.Reset() + if c.Latest.Location.ScreenNum != targetScreen { + tr.Handlers.SwapScreen = &Handler{Source: c, Target: tr.WorkspaceAt(targetDesk, targetScreen)} + log.Debug("Screen swap handler active [", c.Latest.Class, "]") } } } -func (tr *Tracker) handleSwapClient(c *store.Client) { +func (tr *Tracker) handleSwapClient(h *Handler) { + c, target := h.Source.(*store.Client), h.Target.(*store.Client) ws := tr.ClientWorkspace(c) - if !tr.isTracked(c.Win.Id) { + if !tr.isTracked(c.Window.Id) { return } - log.Debug("Client swap handler fired [", tr.Handler.SwapClient.Source.Latest.Class, "-", tr.Handler.SwapClient.Target.Latest.Class, "]") + log.Debug("Client swap handler fired [", c.Latest.Class, "-", target.Latest.Class, "]") // Swap clients on same desktop and screen mg := ws.ActiveLayout().GetManager() - mg.SwapClient(tr.Handler.SwapClient.Source, tr.Handler.SwapClient.Target) + mg.SwapClient(c, target) - // Reset client swapping event - tr.Handler.SwapClient.Active = false + // Reset client swapping handler + h.Reset() // Tile workspace - ws.Tile() + tr.Tile(ws) } -func (tr *Tracker) handleWorkspaceChange(c *store.Client) { - if !tr.isTracked(c.Win.Id) { +func (tr *Tracker) handleWorkspaceChange(h *Handler) { + c, target := h.Source.(*store.Client), h.Target.(*Workspace) + if !tr.isTracked(c.Window.Id) { return } log.Debug("Client workspace handler fired [", c.Latest.Class, "]") @@ -390,19 +425,19 @@ func (tr *Tracker) handleWorkspaceChange(c *store.Client) { // Tile current workspace if ws.Enabled() { - ws.Tile() + tr.Tile(ws) } // Update client desktop and screen - if !tr.isTrackable(c.Win.Id) { + if !tr.isTrackable(c.Window.Id) { return } c.Update() // Add client to new workspace ws = tr.ClientWorkspace(c) - if tr.Handler.SwapScreen.Active && tr.ActiveWorkspace().Enabled() { - ws = tr.ActiveWorkspace() + if tr.Handlers.SwapScreen.Active() && target.Enabled() { + ws = target } mg = ws.ActiveLayout().GetManager() ws.AddClient(c) @@ -412,45 +447,46 @@ func (tr *Tracker) handleWorkspaceChange(c *store.Client) { // Tile new workspace if ws.Enabled() { - ws.Tile() + tr.Tile(ws) } else { c.Restore(store.Latest) } - // Reset screen swapping event - tr.Handler.SwapScreen.Active = false + // Reset screen swapping handler + h.Reset() } func (tr *Tracker) onStateUpdate(state string, desk uint, screen uint) { + workplaceChanged := store.Workplace.DeskCount*store.Workplace.ScreenCount != uint(len(tr.Workspaces)) + workspaceChanged := common.IsInList(state, []string{"_NET_CURRENT_DESKTOP"}) + viewportChanged := common.IsInList(state, []string{"_NET_NUMBER_OF_DESKTOPS", "_NET_DESKTOP_LAYOUT", "_NET_DESKTOP_GEOMETRY", "_NET_DESKTOP_VIEWPORT", "_NET_WORKAREA"}) - clientsChanged := common.IsInList(state, []string{"_NET_CLIENT_LIST_STACKING", "_NET_ACTIVE_WINDOW"}) + clientsChanged := common.IsInList(state, []string{"_NET_CLIENT_LIST_STACKING"}) + focusChanged := common.IsInList(state, []string{"_NET_ACTIVE_WINDOW"}) - workspacesChanged := store.Workplace.DeskCount*store.Workplace.ScreenCount != uint(len(tr.Workspaces)) - workspaceChanged := common.IsInList(state, []string{"_NET_CURRENT_DESKTOP"}) + if workplaceChanged { - // Number of desktops or screens changed - if workspacesChanged { + // Reset clients and workspaces tr.Reset() } - // Active desktop changed if workspaceChanged { + + // Update sticky windows for _, c := range tr.Clients { - sticky := common.IsInList("_NET_WM_STATE_STICKY", c.Latest.States) - if sticky && c.Latest.Location.DeskNum != store.Workplace.CurrentDesk { - ewmh.WmDesktopSet(store.X, c.Win.Id, ^uint(0)) + if store.IsSticky(c.Latest) && c.Latest.Location.DeskNum != store.Workplace.CurrentDesk { + c.MoveDesktop(^uint32(0)) } } } - // Viewport changed or clients changed - if viewportChanged || clientsChanged { + if viewportChanged || clientsChanged || focusChanged { // Deactivate handlers - tr.Handler.ResizeClient.Active = false - tr.Handler.MoveClient.Active = false - tr.Handler.SwapClient.Active = false - tr.Handler.SwapScreen.Active = false + tr.Handlers.ResizeClient.Reset() + tr.Handlers.MoveClient.Reset() + tr.Handlers.SwapClient.Reset() + tr.Handlers.SwapScreen.Reset() // Unlock clients tr.unlockClients() @@ -459,70 +495,70 @@ func (tr *Tracker) onStateUpdate(state string, desk uint, screen uint) { tr.Update() } - // Write client and workspace cache - tr.Write() + if clientsChanged { - // Communicate state update - tr.Channel.Event <- "state_update" + // Write client and workspace cache + tr.Write() + } } -func (tr *Tracker) onPointerUpdate(button uint16, desk uint, screen uint) { - release := button == 0 +func (tr *Tracker) onPointerUpdate(pointer store.XPointer, desk uint, screen uint) { + buttonReleased := !pointer.Pressed() // Reset timer - if tr.Handler.Timer != nil { - tr.Handler.Timer.Stop() + if tr.Handlers.Timer != nil { + tr.Handlers.Timer.Stop() } // Wait on button release var t time.Duration = 0 - if release { + if buttonReleased { t = 50 } // Wait for structure events - tr.Handler.Timer = time.AfterFunc(t*time.Millisecond, func() { + tr.Handlers.Timer = time.AfterFunc(t*time.Millisecond, func() { // Window moved to another screen - if tr.Handler.SwapScreen.Active { - tr.handleWorkspaceChange(tr.Handler.SwapScreen.Source) + if tr.Handlers.SwapScreen.Active() { + tr.handleWorkspaceChange(tr.Handlers.SwapScreen) } // Window moved over another window - if tr.Handler.SwapClient.Active { - tr.handleSwapClient(tr.Handler.SwapClient.Source) + if tr.Handlers.SwapClient.Active() { + tr.handleSwapClient(tr.Handlers.SwapClient) } // Window moved or resized - if tr.Handler.MoveClient.Active || tr.Handler.ResizeClient.Active { - tr.Handler.MoveClient.Active = false - tr.Handler.ResizeClient.Active = false + if tr.Handlers.MoveClient.Active() || tr.Handlers.ResizeClient.Active() { + tr.Handlers.MoveClient.Reset() + tr.Handlers.ResizeClient.Reset() // Unlock clients tr.unlockClients() // Tile workspace - if release { - tr.ActiveWorkspace().Tile() + if buttonReleased { + tr.Tile(tr.ActiveWorkspace()) } } }) } func (tr *Tracker) attachHandlers(c *store.Client) { - c.Win.Listen(xproto.EventMaskStructureNotify | xproto.EventMaskPropertyChange | xproto.EventMaskFocusChange) + c.Window.Instance.Listen(xproto.EventMaskStructureNotify | xproto.EventMaskPropertyChange | xproto.EventMaskFocusChange) // Attach structure events - xevent.ConfigureNotifyFun(func(x *xgbutil.XUtil, ev xevent.ConfigureNotifyEvent) { + xevent.ConfigureNotifyFun(func(X *xgbutil.XUtil, ev xevent.ConfigureNotifyEvent) { log.Trace("Client structure event [", c.Latest.Class, "]") // Handle structure events tr.handleResizeClient(c) tr.handleMoveClient(c) - }).Connect(store.X, c.Win.Id) + }).Connect(store.X, c.Window.Id) // Attach property events - xevent.PropertyNotifyFun(func(x *xgbutil.XUtil, ev xevent.PropertyNotifyEvent) { + xevent.PropertyNotifyFun(func(X *xgbutil.XUtil, ev xevent.PropertyNotifyEvent) { aname, _ := xprop.AtomName(store.X, ev.Atom) log.Trace("Client property event ", aname, " [", c.Latest.Class, "]") @@ -531,25 +567,9 @@ func (tr *Tracker) attachHandlers(c *store.Client) { tr.handleMaximizedClient(c) tr.handleMinimizedClient(c) } else if aname == "_NET_WM_DESKTOP" { - tr.handleWorkspaceChange(c) + tr.handleWorkspaceChange(&Handler{Source: c, Target: tr.ActiveWorkspace()}) } - }).Connect(store.X, c.Win.Id) - - // Attach focus in events - xevent.FocusInFun(func(x *xgbutil.XUtil, ev xevent.FocusInEvent) { - log.Trace("Client focus in event [", c.Latest.Class, "]") - - // Update active window - store.Windows.Active = store.ActiveWindowGet(store.X) - }).Connect(store.X, c.Win.Id) - - // Attach focus out events - xevent.FocusOutFun(func(x *xgbutil.XUtil, ev xevent.FocusOutEvent) { - log.Trace("Client focus out event [", c.Latest.Class, "]") - - // Update active window - store.Windows.Active = store.ActiveWindowGet(store.X) - }).Connect(store.X, c.Win.Id) + }).Connect(store.X, c.Window.Id) } func (tr *Tracker) isTracked(w xproto.Window) bool { diff --git a/desktop/workspace.go b/desktop/workspace.go index 4a87c0e..39de5fc 100644 --- a/desktop/workspace.go +++ b/desktop/workspace.go @@ -19,8 +19,8 @@ type Workspace struct { Name string // Workspace location name Location store.Location // Desktop and screen location Layouts []Layout // List of available layouts + ActiveLayoutNum uint // Index of active layout TilingEnabled bool // Tiling is enabled or not - ActiveLayoutNum uint // Active layout index } func CreateWorkspaces() map[store.Location]*Workspace { @@ -35,8 +35,8 @@ func CreateWorkspaces() map[store.Location]*Workspace { Name: fmt.Sprintf("workspace-%d-%d", location.DeskNum, location.ScreenNum), Location: location, Layouts: CreateLayouts(location), - TilingEnabled: common.Config.TilingEnabled, ActiveLayoutNum: 0, + TilingEnabled: common.Config.TilingEnabled, } // Set default layout @@ -55,8 +55,8 @@ func CreateWorkspaces() map[store.Location]*Workspace { 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.Masters.Maximum = int(math.Min(float64(cmg.Masters.Maximum), float64(common.Config.WindowMastersMax))) + mg.Slaves.Maximum = int(math.Min(float64(cmg.Slaves.Maximum), float64(common.Config.WindowSlavesMax))) mg.Proportions = cmg.Proportions } } @@ -96,9 +96,6 @@ func (ws *Workspace) ActiveLayout() Layout { } func (ws *Workspace) CycleLayout(step int) { - if ws.Disabled() { - return - } // Calculate cycle direction i := (int(ws.ActiveLayoutNum) + step) % len(ws.Layouts) @@ -107,7 +104,6 @@ func (ws *Workspace) CycleLayout(step int) { } ws.SetLayout(uint(i)) - ws.Tile() } func (ws *Workspace) AddClient(c *store.Client) { @@ -197,7 +193,7 @@ func (ws *Workspace) Write() { return } - log.Debug("Write workspace cache data ", cache.Name, " [", ws.Name, "]") + log.Trace("Write workspace cache data ", cache.Name, " [", ws.Name, "]") } func (ws *Workspace) Read() *Workspace { @@ -242,7 +238,7 @@ func (ws *Workspace) Cache() common.Cache[*Workspace] { // Create workspace cache object cache := common.Cache[*Workspace]{ Folder: folder, - Name: common.Hash(hash) + ".json", + Name: common.HashString(hash) + ".json", Data: ws, } diff --git a/input/action.go b/input/action.go index d77f7ef..4daebb2 100644 --- a/input/action.go +++ b/input/action.go @@ -8,6 +8,8 @@ import ( "golang.org/x/exp/maps" + "github.com/jezek/xgbutil/xevent" + "github.com/leukipp/cortile/v2/common" "github.com/leukipp/cortile/v2/desktop" "github.com/leukipp/cortile/v2/store" @@ -20,16 +22,93 @@ var ( executeCallbacksFun []func(string, uint, uint) // Execute events callback functions ) -func Execute(action string, mod string, tr *desktop.Tracker) bool { +func ExecuteAction(action string, tr *desktop.Tracker, ws *desktop.Workspace) bool { success := false - if len(strings.TrimSpace(action)) == 0 { + if tr == nil || ws == nil { + return false + } + + log.Info("Execute action ", action, " [", ws.Name, "]") + + // Choose action command + switch action { + case "": + success = false + case "enable": + success = Enable(tr, ws) + case "disable": + success = Disable(tr, ws) + case "restore": + success = Restore(tr, ws) + case "toggle": + success = Toggle(tr, ws) + case "cycle_next": + success = CycleNext(tr, ws) + case "cycle_previous": + success = CyclePrevious(tr, ws) + case "layout_fullscreen": + success = FullscreenLayout(tr, ws) + case "layout_vertical_left": + success = VerticalLeftLayout(tr, ws) + case "layout_vertical_right": + success = VerticalRightLayout(tr, ws) + case "layout_horizontal_top": + success = HorizontalTopLayout(tr, ws) + case "layout_horizontal_bottom": + success = HorizontalBottomLayout(tr, ws) + case "master_make": + success = MakeMaster(tr, ws) + case "master_make_next": + success = MakeMasterNext(tr, ws) + case "master_make_previous": + success = MakeMasterPrevious(tr, ws) + case "master_increase": + success = IncreaseMaster(tr, ws) + case "master_decrease": + success = DecreaseMaster(tr, ws) + case "slave_increase": + success = IncreaseSlave(tr, ws) + case "slave_decrease": + success = DecreaseSlave(tr, ws) + case "proportion_increase": + success = IncreaseProportion(tr, ws) + case "proportion_decrease": + success = DecreaseProportion(tr, ws) + case "window_next": + success = NextWindow(tr, ws) + case "window_previous": + success = PreviousWindow(tr, ws) + case "reset": + success = Reset(tr, ws) + case "exit": + success = Exit(tr) + default: + success = External(action) + } + + // Check success + if !success { return false } - log.Info("Execute action [", action, "-", mod, "]") + // Notify socket (deprecated) + NotifySocket(Message[store.Location]{ + Type: "Action", + Name: action, + Data: ws.Location, + }) + + // Execute callbacks + executeCallbacks(action, ws.Location.DeskNum, ws.Location.ScreenNum) + return true +} + +func ExecuteActions(action string, tr *desktop.Tracker, mod string) bool { + results := []bool{} + + active := tr.ActiveWorkspace() for _, ws := range tr.Workspaces { - active := tr.ActiveWorkspace() // Execute only on active screen if mod == "current" && ws.Location != active.Location { @@ -41,76 +120,12 @@ func Execute(action string, mod string, tr *desktop.Tracker) bool { continue } - // Choose action command - switch action { - case "enable": - success = Enable(tr, ws) - case "disable": - success = Disable(tr, ws) - case "restore": - success = Restore(tr, ws) - case "toggle": - success = Toggle(tr, ws) - case "cycle_next": - success = CycleNext(tr, ws) - case "cycle_previous": - success = CyclePrevious(tr, ws) - case "layout_fullscreen": - success = FullscreenLayout(tr, ws) - case "layout_vertical_left": - success = VerticalLeftLayout(tr, ws) - case "layout_vertical_right": - success = VerticalRightLayout(tr, ws) - case "layout_horizontal_top": - success = HorizontalTopLayout(tr, ws) - case "layout_horizontal_bottom": - success = HorizontalBottomLayout(tr, ws) - case "master_make": - success = MakeMaster(tr, ws) - case "master_make_next": - success = MakeMasterNext(tr, ws) - case "master_make_previous": - success = MakeMasterPrevious(tr, ws) - case "master_increase": - success = IncreaseMaster(tr, ws) - case "master_decrease": - success = DecreaseMaster(tr, ws) - case "slave_increase": - success = IncreaseSlave(tr, ws) - case "slave_decrease": - success = DecreaseSlave(tr, ws) - case "proportion_increase": - success = IncreaseProportion(tr, ws) - case "proportion_decrease": - success = DecreaseProportion(tr, ws) - case "window_next": - success = NextWindow(tr, ws) - case "window_previous": - success = PreviousWindow(tr, ws) - case "reset": - success = Reset(tr, ws) - case "exit": - success = Exit(tr) - default: - success = External(action) - } - - if !success { - return false - } - - // Notify socket (deprecated) - NotifySocket(Message[store.Location]{ - Type: "Action", - Name: action, - Data: ws.Location, - }) - - // Execute callbacks - executeCallbacks(action, ws.Location.DeskNum, ws.Location.ScreenNum) + // Execute action and store results + success := ExecuteAction(action, tr, ws) + results = append(results, success) } - return true + return common.AllTrue(results) } func Query(state string, tr *desktop.Tracker) bool { @@ -162,7 +177,7 @@ func Query(state string, tr *desktop.Tracker) bool { func Enable(tr *desktop.Tracker, ws *desktop.Workspace) bool { ws.Enable() tr.Update() - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -208,6 +223,7 @@ func CycleNext(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } ws.CycleLayout(1) + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -220,6 +236,7 @@ func CyclePrevious(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } ws.CycleLayout(-1) + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -236,7 +253,7 @@ func FullscreenLayout(tr *desktop.Tracker, ws *desktop.Workspace) bool { ws.SetLayout(uint(i)) } } - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -253,7 +270,7 @@ func VerticalLeftLayout(tr *desktop.Tracker, ws *desktop.Workspace) bool { ws.SetLayout(uint(i)) } } - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -270,7 +287,7 @@ func VerticalRightLayout(tr *desktop.Tracker, ws *desktop.Workspace) bool { ws.SetLayout(uint(i)) } } - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -287,7 +304,7 @@ func HorizontalTopLayout(tr *desktop.Tracker, ws *desktop.Workspace) bool { ws.SetLayout(uint(i)) } } - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -304,7 +321,7 @@ func HorizontalBottomLayout(tr *desktop.Tracker, ws *desktop.Workspace) bool { ws.SetLayout(uint(i)) } } - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -316,9 +333,9 @@ func MakeMaster(tr *desktop.Tracker, ws *desktop.Workspace) bool { if ws.Disabled() { return false } - if c, ok := tr.Clients[store.Windows.Active]; ok { + if c, ok := tr.Clients[store.Windows.Active.Id]; ok { ws.ActiveLayout().MakeMaster(c) - ws.Tile() + tr.Tile(ws) return true } @@ -335,7 +352,7 @@ func MakeMasterNext(tr *desktop.Tracker, ws *desktop.Workspace) bool { } ws.ActiveLayout().MakeMaster(c) - ws.Tile() + tr.Tile(ws) return NextWindow(tr, ws) } @@ -350,7 +367,7 @@ func MakeMasterPrevious(tr *desktop.Tracker, ws *desktop.Workspace) bool { } ws.ActiveLayout().MakeMaster(c) - ws.Tile() + tr.Tile(ws) return PreviousWindow(tr, ws) } @@ -360,7 +377,7 @@ func IncreaseMaster(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } ws.ActiveLayout().IncreaseMaster() - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -373,7 +390,7 @@ func DecreaseMaster(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } ws.ActiveLayout().DecreaseMaster() - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -386,7 +403,7 @@ func IncreaseSlave(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } ws.ActiveLayout().IncreaseSlave() - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -399,7 +416,7 @@ func DecreaseSlave(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } ws.ActiveLayout().DecreaseSlave() - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -412,7 +429,7 @@ func IncreaseProportion(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } ws.ActiveLayout().IncreaseProportion() - ws.Tile() + tr.Tile(ws) return true } @@ -422,7 +439,7 @@ func DecreaseProportion(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } ws.ActiveLayout().DecreaseProportion() - ws.Tile() + tr.Tile(ws) return true } @@ -436,7 +453,7 @@ func NextWindow(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } - c.Activate() + store.ActiveWindowSet(store.X, c.Window) return true } @@ -450,7 +467,7 @@ func PreviousWindow(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } - c.Activate() + store.ActiveWindowSet(store.X, c.Window) return true } @@ -460,7 +477,7 @@ func Reset(tr *desktop.Tracker, ws *desktop.Workspace) bool { return false } ws.ResetLayouts() - ws.Tile() + tr.Tile(ws) ui.ShowLayout(ws) ui.UpdateIcon(ws) @@ -471,6 +488,8 @@ func Reset(tr *desktop.Tracker, ws *desktop.Workspace) bool { func Exit(tr *desktop.Tracker) bool { tr.Write() + xevent.Detach(store.X, store.X.RootWin()) + for _, ws := range tr.Workspaces { if ws.Disabled() { continue @@ -492,7 +511,12 @@ func Exit(tr *desktop.Tracker) bool { func External(command string) bool { params := strings.Split(command, " ") - log.Info("Execute command ", params[0], " ", params[1:]) + if !common.Feature("enable-external-commands") { + log.Warn("Executing external command \"", params[0], "\" disabled") + return false + } + + log.Info("Executing external command \"", params[0], " ", params[1:], "\"") // Execute external command cmd := exec.Command(params[0], params[1:]...) diff --git a/input/dbusbinding.go b/input/dbusbinding.go new file mode 100644 index 0000000..d760436 --- /dev/null +++ b/input/dbusbinding.go @@ -0,0 +1,540 @@ +package input + +import ( + "fmt" + "os" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "encoding/json" + "net/url" + + "golang.org/x/exp/maps" + + "github.com/jezek/xgb/xproto" + "github.com/jezek/xgbutil/ewmh" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/godbus/dbus/v5/prop" + + "github.com/leukipp/cortile/v2/common" + "github.com/leukipp/cortile/v2/desktop" + "github.com/leukipp/cortile/v2/store" + + log "github.com/sirupsen/logrus" +) + +var ( + iface string // Dbus interface name + opath dbus.ObjectPath // Dbus object path + props *prop.Properties // Dbus properties + methods *Methods // Dbus methods +) + +type Methods struct { + Naming map[string][]string // Method and arguments names + Tracker *desktop.Tracker // Workspace tracker instance +} + +func (m Methods) ActionExecute(name string, desk int32, screen int32) (string, *dbus.Error) { + success := false + + // Execute action + ws := m.Tracker.WorkspaceAt(uint(desk), uint(screen)) + if ws != nil { + success = ExecuteAction(name, m.Tracker, ws) + } + + // Return result + result := common.Map{"Success": success} + + return dataMap("Result", "ActionExecute", result), nil +} + +func (m Methods) WindowActivate(id int32) (string, *dbus.Error) { + success := false + + // Activate window + if c, ok := m.Tracker.Clients[xproto.Window(id)]; ok { + store.ActiveWindowSet(store.X, c.Window) + success = true + } + + // Return result + result := common.Map{"Success": success} + + return dataMap("Result", "WindowActivate", result), nil +} + +func (m Methods) WindowToPosition(id int32, x int32, y int32) (string, *dbus.Error) { + success := false + + // Move window to position + valid := x >= 0 && y >= 0 + if c, ok := m.Tracker.Clients[xproto.Window(id)]; ok && valid { + ewmh.MoveWindow(store.X, c.Window.Id, int(x), int(y)) + store.Pointer.Press() + success = true + } + + // Return result + result := common.Map{"Success": success} + + return dataMap("Result", "WindowToPosition", result), nil +} + +func (m Methods) WindowToDesktop(id int32, desk int32) (string, *dbus.Error) { + success := false + + // Move window to desktop + valid := desk >= 0 && uint(desk) < store.Workplace.DeskCount + if c, ok := m.Tracker.Clients[xproto.Window(id)]; ok && valid { + c.MoveDesktop(uint32(desk)) + success = true + } + + // Return result + result := common.Map{"Success": success} + + return dataMap("Result", "WindowToDesktop", result), nil +} + +func (m Methods) WindowToScreen(id int32, screen int32) (string, *dbus.Error) { + success := false + + // Move window to screen + valid := screen >= 0 && uint(screen) < store.Workplace.ScreenCount + if c, ok := m.Tracker.Clients[xproto.Window(id)]; ok && valid { + p := store.Workplace.Displays.Screens[screen].Geometry.Center() + ewmh.MoveWindow(store.X, c.Window.Id, int(p.X), int(p.Y)) + store.Pointer.Press() + success = true + } + + // Return result + result := common.Map{"Success": success} + + return dataMap("Result", "WindowToScreen", result), nil +} + +func (m Methods) DesktopSwitch(desk int32) (string, *dbus.Error) { + success := false + + // Switch current desktop + valid := desk >= 0 && uint(desk) < store.Workplace.DeskCount + if valid { + store.CurrentDesktopSet(store.X, uint(desk)) + success = true + } + + // Return result + result := common.Map{"Success": success} + + return dataMap("Result", "DesktopSwitch", result), nil +} + +func (m Methods) Introspection() []introspect.Method { + typ := reflect.TypeOf(m) + ims := make([]introspect.Method, 0, typ.NumMethod()) + + for i := 0; i < typ.NumMethod(); i++ { + if typ.Method(i).PkgPath != "" { + continue + } + + // Validate return types + mt := typ.Method(i).Type + if mt.NumOut() == 0 || mt.Out(mt.NumOut()-1) != reflect.TypeOf(&dbus.Error{}) { + continue + } + + // Introspect method + im := introspect.Method{ + Name: typ.Method(i).Name, + Annotations: make([]introspect.Annotation, 0), + Args: make([]introspect.Arg, 0, mt.NumIn()+mt.NumOut()-2), + } + + // Arguments in + for j := 1; j < mt.NumIn(); j++ { + styp := dbus.SignatureOfType(mt.In(j)).String() + im.Args = append(im.Args, introspect.Arg{Name: m.Naming[im.Name][j-1], Type: styp, Direction: "in"}) + } + + // Arguments out + for j := 0; j < mt.NumOut()-1; j++ { + styp := dbus.SignatureOfType(mt.Out(j)).String() + im.Args = append(im.Args, introspect.Arg{Name: "json", Type: styp, Direction: "out"}) + } + + ims = append(ims, im) + } + + return ims +} + +func BindDbus(tr *desktop.Tracker) { + + // Export interfaces + go export(tr) + + // Bind event channel + go event(tr.Channels.Event, tr) + + // Attach execute events + OnExecute(func(action string, desk uint, screen uint) { + SetProperty("Action", struct { + Name string + Location store.Location + }{ + Name: action, + Location: store.Location{DeskNum: desk, ScreenNum: screen}, + }) + }) + + // Attach pointer events + store.OnPointerUpdate(func(pointer store.XPointer, desk uint, screen uint) { + SetProperty("Pointer", struct { + Device store.XPointer + Location store.Location + }{ + Device: pointer, + Location: store.Location{DeskNum: desk, ScreenNum: screen}, + }) + }) +} + +func event(ch chan string, tr *desktop.Tracker) { + for { + switch <-ch { + case "clients_change": + SetProperty("Clients", common.Map{"Values": maps.Values(tr.Clients)}) + case "workspaces_change": + SetProperty("Workspaces", common.Map{"Values": maps.Values(tr.Workspaces)}) + case "workplace_change": + SetProperty("Workplace", store.Workplace) + case "windows_change": + SetProperty("Windows", store.Windows) + case "corner_change": + for _, hc := range store.Workplace.Displays.Corners { + if !hc.Active { + continue + } + SetProperty("Corner", struct { + Name string + Location store.Location + }{ + Name: hc.Name, + Location: tr.ActiveWorkspace().Location, + }) + } + } + } +} + +func connect() (*dbus.Conn, error) { + source, _ := url.Parse(common.Build.Source) + hostname := strings.TrimPrefix(source.Hostname(), "www.") + reverse := strings.Join(common.ReverseList(strings.Split(hostname, ".")), ".") + project := strings.Replace(strings.Trim(source.Path, "/"), "/", ".", -1) + + // Init interface and path + iface = fmt.Sprintf("%s.%s", reverse, project) + opath = dbus.ObjectPath(fmt.Sprintf("/%s", strings.Replace(iface, ".", "/", -1))) + + // Init session bus + return dbus.ConnectSessionBus() +} + +func export(tr *desktop.Tracker) { + conn, err := connect() + if err != nil { + log.Warn("Error initializing dbus server: ", err) + return + } + defer conn.Close() + + // Request dbus name + reply, err := conn.RequestName(iface, dbus.NameFlagDoNotQueue) + if err != nil || reply != dbus.RequestNameReplyPrimaryOwner { + log.Warn("Error requesting dbus name ", iface, ": ", err) + return + } + + // Export dbus properties + mapping := map[string]interface{}{ + "Build": structToMap(common.Build), + "Process": structToMap(common.Process), + "Arguments": structToMap(common.Args), + "Configuration": structToMap(common.Config), + "Workspaces": common.Map{}, + "Workplace": common.Map{}, + "Windows": common.Map{}, + "Clients": common.Map{}, + "Pointer": common.Map{}, + "Action": common.Map{}, + "Corner": common.Map{}, + } + properties := map[string]*prop.Prop{} + for name, value := range mapping { + properties[name] = &prop.Prop{ + Value: value, + Emit: prop.EmitTrue, + Writable: len(value.(common.Map)) == 0, + } + } + props, err = prop.Export(conn, opath, prop.Map{iface: properties}) + if err != nil { + log.Warn("Error exporting dbus properties: ", err) + return + } + + // Export dbus methods + methods = &Methods{ + Naming: map[string][]string{ + "ActionExecute": {"name", "desk", "screen"}, + "WindowActivate": {"id"}, + "WindowToPosition": {"id", "x", "y"}, + "WindowToDesktop": {"id", "desk"}, + "WindowToScreen": {"id", "screen"}, + "DesktopSwitch": {"desk"}, + }, + Tracker: tr, + } + err = conn.Export(methods, opath, iface) + if err != nil { + log.Warn("Error exporting dbus methods: ", err) + return + } + + // Export dbus interfaces + intro := introspect.NewIntrospectable(&introspect.Node{ + Name: string(opath), + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + { + Name: iface, + Methods: methods.Introspection(), + Properties: props.Introspection(iface), + }, + }, + }) + err = conn.Export(intro, opath, "org.freedesktop.DBus.Introspectable") + if err != nil { + log.Warn("Error exporting dbus interfaces: ", err) + return + } + + select {} +} + +func Introspect() map[string][]string { + conn, err := connect() + if err != nil { + return map[string][]string{} + } + defer conn.Close() + + // Call introspect method + node, err := introspect.Call(conn.Object(iface, opath)) + if err != nil { + return map[string][]string{} + } + + // Iterate node interfaces + methods := []string{} + properties := []string{} + for _, item := range node.Interfaces { + if item.Name != iface { + continue + } + + // Get dbus methods + for _, method := range item.Methods { + description := method.Name + for _, arg := range method.Args { + if arg.Direction != "in" { + continue + } + switch arg.Type { + case "s": + description += fmt.Sprintf(" str:%s", arg.Name) + case "i": + description += fmt.Sprintf(" int:%s", arg.Name) + } + } + methods = append(methods, description) + } + + // Get dbus properties + for _, property := range item.Properties { + properties = append(properties, property.Name) + } + } + sort.Strings(methods) + sort.Strings(properties) + + return map[string][]string{ + "Methods": methods, + "Properties": properties, + } +} + +func Method(name string, args []string) { + conn, err := connect() + if err != nil { + fatal("Error initializing dbus server", err) + } + defer conn.Close() + + // Convert arguments + variants := make([]interface{}, len(args)) + for i, value := range args { + integer, err := strconv.Atoi(value) + if err == nil { + variants[i] = dbus.MakeVariant(integer) + } else { + variants[i] = dbus.MakeVariant(value) + } + } + + // Call dbus method + call := conn.Object(iface, opath).Call(fmt.Sprintf("%s.%s", iface, name), 0, variants...) + if call.Err != nil { + fatal("Error calling dbus method", call.Err) + } + + // Print reply + var reply string + call.Store(&reply) + fmt.Println(reply) +} + +func Property(name string) { + conn, err := connect() + if err != nil { + fatal("Error initializing dbus server", err) + } + defer conn.Close() + + // Convert arguments + variants := []interface{}{dbus.MakeVariant(iface), dbus.MakeVariant(name)} + + // Receive dbus property + call := conn.Object(iface, opath).Call("org.freedesktop.DBus.Properties.Get", 0, variants...) + if call.Err != nil { + fatal("Error receiving dbus property", call.Err) + } + + // Print reply + var reply dbus.Variant + call.Store(&reply) + print("Property", name, variantToMap(reply)) +} + +func Listen(args []string) { + conn, err := connect() + if err != nil { + fatal("Error initializing dbus server", err) + } + defer conn.Close() + + // Monitor property changes and method calls + call := conn.BusObject().Call("org.freedesktop.DBus.Monitoring.BecomeMonitor", 0, []string{ + fmt.Sprintf("type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path='%s'", opath), + fmt.Sprintf("type='method_call',interface='%s',path='%s'", iface, opath), + }, uint(0)) + if call.Err != nil { + fatal("Error becoming dbus monitor", call.Err) + } + + // Listen to channel events + ch := make(chan *dbus.Message, 10) + conn.Eavesdrop(ch) + + var method string + for msg := range ch { + msg.Headers[3].Store(&method) + + // Print reply + switch method { + case "PropertiesChanged": + typ := "Property" + for name, variant := range msg.Body[1].(map[string]dbus.Variant) { + filter := fmt.Sprintf("%s:%s", typ, name) + if len(args) == 0 || common.IsInList(filter, args) { + print(typ, name, variantToMap(variant)) + } + } + default: + typ := "Method" + filter := fmt.Sprintf("%s:%s", typ, method) + if len(args) == 0 || common.IsInList(filter, args) { + print(typ, method, common.Map{"Body": bodyToString(msg.Body)}) + } + } + } +} + +func GetProperty(name string) common.Map { + if props == nil { + return common.Map{} + } + variant := dbus.MakeVariant(props.GetMust(iface, name)) + return variantToMap(variant) +} + +func SetProperty(name string, obj interface{}) { + if props == nil { + return + } + variant := dbus.MakeVariant(structToMap(obj)) + props.SetMust(iface, name, variant) +} + +func variantToMap(variant dbus.Variant) (value common.Map) { + variant.Store(&value) + return value +} + +func structToMap(obj interface{}) (value common.Map) { + data, err := json.Marshal(obj) + if err != nil { + return value + } + json.Unmarshal(data, &value) + return value +} + +func mapToString(obj interface{}) string { + data, err := json.Marshal(obj) + if err != nil { + return "{}" + } + return string(data) +} + +func bodyToString(obj interface{}) string { + r := strings.NewReplacer(":", "", "[", "", "]", "", "\"", "") + return r.Replace(fmt.Sprint(obj)) +} + +func dataMap(typ string, name string, data common.Map) string { + time := time.Now().UnixMilli() + process := common.Process.Id + return mapToString(common.Map{"Process": process, "Time": time, "Type": typ, "Name": name, "Data": data}) +} + +func print(typ string, name string, data common.Map) { + fmt.Println(dataMap(typ, name, data)) +} + +func fatal(msg string, err error) { + print("Error", "Fatal", common.Map{"Message": fmt.Sprintf("%s: %s", msg, err)}) + os.Exit(1) +} diff --git a/input/keybinding.go b/input/keybinding.go index 08f89aa..9a888fb 100644 --- a/input/keybinding.go +++ b/input/keybinding.go @@ -39,14 +39,23 @@ func BindKeys(tr *desktop.Tracker) { } } } + + // Bind action channel + go action(tr.Channels.Action, tr) } func bind(key string, action string, mod string, tr *desktop.Tracker) { err := keybind.KeyPressFun(func(X *xgbutil.XUtil, ev xevent.KeyPressEvent) { - Execute(action, mod, tr) + ExecuteActions(action, tr, mod) }).Connect(store.X, store.X.RootWin(), key, true) if err != nil { - log.Warn("Error on action for ", action, ": ", err) + log.Warn("Error on action ", action, ": ", err) + } +} + +func action(ch chan string, tr *desktop.Tracker) { + for { + ExecuteAction(<-ch, tr, tr.ActiveWorkspace()) } } diff --git a/input/mousebinding.go b/input/mousebinding.go index 96009f0..e7271c9 100644 --- a/input/mousebinding.go +++ b/input/mousebinding.go @@ -12,39 +12,42 @@ import ( ) var ( - workspace *desktop.Workspace // Stores last active workspace + workspace *desktop.Workspace // Stores last active workspace (for comparison only) ) func BindMouse(tr *desktop.Tracker) { poll(100, func() { - pt := store.PointerUpdate(store.X) + store.PointerUpdate(store.X) - // Update systray icon + // Compare active workspace ws := tr.ActiveWorkspace() if ws != workspace { + log.Info("Active workspace changed [", ws.Name, "]") + + // Communicate workplace change + tr.Channels.Event <- "workplace_change" + + // Update systray icon ui.UpdateIcon(ws) + + // Store last workspace workspace = ws } // Evaluate corner states - for i := range store.Workplace.Displays.Corners { - hc := store.Workplace.Displays.Corners[i] - - wasActive := hc.Active - isActive := hc.IsActive(pt) - - if !wasActive && isActive { - log.Debug("Corner at position ", hc.Area, " is hot [", hc.Name, "]") - Execute(common.Config.Corners[hc.Name], "current", tr) - } else if wasActive && !isActive { - log.Debug("Corner at position ", hc.Area, " is cold [", hc.Name, "]") - } + hc := store.HotCorner() + if hc != nil { + + // Communicate corner change + tr.Channels.Event <- "corner_change" + + // Execute action + ExecuteAction(common.Config.Corners[hc.Name], tr, tr.ActiveWorkspace()) } }) } func poll(t time.Duration, fun func()) { - fun() go func() { for range time.Tick(t * time.Millisecond) { fun() diff --git a/input/signalbinding.go b/input/signalbinding.go index 0429c95..075012b 100644 --- a/input/signalbinding.go +++ b/input/signalbinding.go @@ -19,5 +19,5 @@ func BindSignal(tr *desktop.Tracker) { func exit(ch chan os.Signal, tr *desktop.Tracker) { <-ch - Execute("exit", "current", tr) + ExecuteAction("exit", tr, tr.ActiveWorkspace()) } diff --git a/input/socketbinding.go b/input/socketbinding.go index 5049bc1..2692286 100644 --- a/input/socketbinding.go +++ b/input/socketbinding.go @@ -52,7 +52,10 @@ func NotifySocket[T any](m Message[T]) { } msg := string(data) - log.Info("Send socket message ", common.Truncate(msg, 100), "...") + log.Info("Send socket message ", common.TruncateString(msg, 100), "...") + + // Warn about socket communication (deprecated) + log.Warn("Socket communication is deprecated and will be removed in future releases (issue #51)") // Write outgoing data _, err = dialer.Write([]byte(msg)) @@ -80,7 +83,7 @@ func listen(listener net.Listener, tr *desktop.Tracker) { } msg := strings.TrimSpace(string(data[:n])) - log.Info("Receive socket message ", common.Truncate(msg, 100), "...") + log.Info("Receive socket message ", common.TruncateString(msg, 100), "...") // Parse incoming data var kv map[string]string @@ -90,9 +93,12 @@ func listen(listener net.Listener, tr *desktop.Tracker) { return } + // Warn about socket communication (deprecated) + log.Warn("Socket communication is deprecated and will be removed in future releases (issue #51)") + // Execute action if v, ok := kv["Action"]; ok { - Execute(v, "current", tr) + ExecuteAction(v, tr, tr.ActiveWorkspace()) } // Query state diff --git a/input/traybinding.go b/input/traybinding.go index 5b96982..8c0857d 100644 --- a/input/traybinding.go +++ b/input/traybinding.go @@ -2,7 +2,6 @@ package input import ( "fmt" - "os" "strings" "time" @@ -12,8 +11,6 @@ import ( "github.com/godbus/dbus/v5" - "github.com/jezek/xgb/xproto" - "github.com/leukipp/cortile/v2/common" "github.com/leukipp/cortile/v2/desktop" "github.com/leukipp/cortile/v2/store" @@ -22,10 +19,10 @@ import ( ) var ( - clicked bool // Tray clicked state from dbus - pointer uint16 // Pointer button state of device - timer *time.Timer // Timer to compress pointer events - menu *Menu // Items collection of systray menu + clicked bool // Tray clicked state from dbus + button store.XButton // Pointer button state of device + timer *time.Timer // Timer to compress pointer events + menu *Menu // Items collection of systray menu ) type Menu struct { @@ -50,8 +47,8 @@ func BindTray(tr *desktop.Tracker) { }) // Attach pointer events - store.OnPointerUpdate(func(button uint16, desk uint, screen uint) { - onPointerClick(tr, button) + store.OnPointerUpdate(func(pointer store.XPointer, desk uint, screen uint) { + onPointerClick(tr, pointer) }) } @@ -114,7 +111,7 @@ func items(tr *desktop.Tracker) { go func() { for { <-item.ClickedCh - Execute(action, "current", tr) + ExecuteAction(action, tr, tr.ActiveWorkspace()) } }() } @@ -126,20 +123,20 @@ func messages(tr *desktop.Tracker) { // Request owner of shared session conn, err := dbus.SessionBus() if err != nil { - log.Warn("Error initializing tray owner ", err) + log.Warn("Error initializing tray owner: ", err) return } - name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) + name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", common.Process.Id) conn.BusObject().Call("org.freedesktop.DBus.GetNameOwner", 0, name).Store(&destination) if len(destination) == 0 { - log.Warn("Error requesting tray owner ", name) + log.Warn("Error requesting tray owner: ", name) return } // Monitor method calls in separate session conn, err = dbus.ConnectSessionBus() if err != nil { - log.Warn("Error initializing tray methods ", err) + log.Warn("Error initializing tray methods: ", err) return } call := conn.BusObject().Call("org.freedesktop.DBus.Monitoring.BecomeMonitor", 0, []string{ @@ -147,7 +144,7 @@ func messages(tr *desktop.Tracker) { fmt.Sprintf("type='method_call',path='/StatusNotifierItem',interface='org.kde.StatusNotifierItem',destination='%s'", destination), }, uint(0)) if call.Err != nil { - log.Warn("Error monitoring tray methods ", call.Err) + log.Warn("Error monitoring tray methods: ", call.Err) return } @@ -210,9 +207,9 @@ func onActivate(tr *desktop.Tracker) { } } -func onPointerClick(tr *desktop.Tracker, button uint16) { - if button != 0 { - pointer = button +func onPointerClick(tr *desktop.Tracker, pointer store.XPointer) { + if pointer.Pressed() { + button = pointer.Button } // Reset timer @@ -222,17 +219,16 @@ func onPointerClick(tr *desktop.Tracker, button uint16) { // Wait for dbus events timer = time.AfterFunc(150*time.Millisecond, func() { - if clicked { - switch pointer { - case pointer & xproto.ButtonMask1: - Execute(common.Config.Systray["click_left"], "current", tr) - case pointer & xproto.ButtonMask2: - Execute(common.Config.Systray["click_middle"], "current", tr) - case pointer & xproto.ButtonMask3: - Execute(common.Config.Systray["click_right"], "current", tr) - } - clicked = false + if clicked && button.Left { + ExecuteAction(common.Config.Systray["click_left"], tr, tr.ActiveWorkspace()) + } + if clicked && button.Middle { + ExecuteAction(common.Config.Systray["click_middle"], tr, tr.ActiveWorkspace()) + } + if clicked && button.Right { + ExecuteAction(common.Config.Systray["click_right"], tr, tr.ActiveWorkspace()) } + clicked = false }) } @@ -248,15 +244,15 @@ func onPointerScroll(tr *desktop.Tracker, delta int32, orientation string) { switch orientation { case "vertical": if delta >= 0 { - Execute(common.Config.Systray["scroll_down"], "current", tr) + ExecuteAction(common.Config.Systray["scroll_down"], tr, tr.ActiveWorkspace()) } else { - Execute(common.Config.Systray["scroll_up"], "current", tr) + ExecuteAction(common.Config.Systray["scroll_up"], tr, tr.ActiveWorkspace()) } case "horizontal": if delta >= 0 { - Execute(common.Config.Systray["scroll_right"], "current", tr) + ExecuteAction(common.Config.Systray["scroll_right"], tr, tr.ActiveWorkspace()) } else { - Execute(common.Config.Systray["scroll_left"], "current", tr) + ExecuteAction(common.Config.Systray["scroll_left"], tr, tr.ActiveWorkspace()) } } }) diff --git a/layout/fullscreen.go b/layout/fullscreen.go index 8c1bf4d..912de9b 100644 --- a/layout/fullscreen.go +++ b/layout/fullscreen.go @@ -33,7 +33,7 @@ func (l *FullscreenLayout) Reset() { func (l *FullscreenLayout) Apply() { clients := l.Clients(store.Stacked) - dx, dy, dw, dh := store.DesktopDimensions(l.Location.ScreenNum) + dx, dy, dw, dh := store.DesktopGeometry(l.Location.ScreenNum).Pieces() gap := common.Config.WindowGapSize csize := len(clients) @@ -49,7 +49,7 @@ func (l *FullscreenLayout) Apply() { c.LimitDimensions(minw, minh) // Move and resize client - c.MoveResize(dx+gap, dy+gap, dw-2*gap, dh-2*gap) + c.MoveWindow(dx+gap, dy+gap, dw-2*gap, dh-2*gap) } } diff --git a/layout/horizontal.go b/layout/horizontal.go index 2c316d9..f0403eb 100644 --- a/layout/horizontal.go +++ b/layout/horizontal.go @@ -36,18 +36,18 @@ func (l *HorizontalLayout) Reset() { mg := store.CreateManager(*l.Location) // Reset number of masters - for l.Masters.MaxAllowed < mg.Masters.MaxAllowed { + for l.Masters.Maximum < mg.Masters.Maximum { l.IncreaseMaster() } - for l.Masters.MaxAllowed > mg.Masters.MaxAllowed { + for l.Masters.Maximum > mg.Masters.Maximum { l.DecreaseMaster() } // Reset number of slaves - for l.Slaves.MaxAllowed < mg.Slaves.MaxAllowed { + for l.Slaves.Maximum < mg.Slaves.Maximum { l.IncreaseSlave() } - for l.Slaves.MaxAllowed > mg.Slaves.MaxAllowed { + for l.Slaves.Maximum > mg.Slaves.Maximum { l.DecreaseSlave() } @@ -58,11 +58,11 @@ func (l *HorizontalLayout) Reset() { func (l *HorizontalLayout) Apply() { clients := l.Clients(store.Stacked) - dx, dy, dw, dh := store.DesktopDimensions(l.Location.ScreenNum) + dx, dy, dw, dh := store.DesktopGeometry(l.Location.ScreenNum).Pieces() gap := common.Config.WindowGapSize - mmax := l.Masters.MaxAllowed - smax := l.Slaves.MaxAllowed + mmax := l.Masters.Maximum + smax := l.Slaves.Maximum msize := int(math.Min(float64(len(l.Masters.Stacked)), float64(mmax))) ssize := int(math.Min(float64(len(l.Slaves.Stacked)), float64(smax))) @@ -118,7 +118,7 @@ func (l *HorizontalLayout) Apply() { // Move and resize master 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) + c.MoveWindow(mx, my+gap, mw, mh-2*gap) // Add x offset mx += mw + gap @@ -156,7 +156,7 @@ func (l *HorizontalLayout) Apply() { // Move and resize slave sp := l.Proportions.SlaveSlave[ssize][i%ssize] sw := int(math.Round(float64(dw-(ssize+1)*gap) * sp)) - c.MoveResize(sx, sy, sw, sh-gap) + c.MoveWindow(sx, sy, sw, sh-gap) // Add x offset sx += sw + gap @@ -165,13 +165,13 @@ func (l *HorizontalLayout) Apply() { } func (l *HorizontalLayout) UpdateProportions(c *store.Client, d *store.Directions) { - _, _, dw, dh := store.DesktopDimensions(l.Location.ScreenNum) + _, _, dw, dh := store.DesktopGeometry(l.Location.ScreenNum).Pieces() _, _, cw, ch := c.OuterGeometry() gap := common.Config.WindowGapSize - mmax := l.Masters.MaxAllowed - smax := l.Slaves.MaxAllowed + mmax := l.Masters.Maximum + smax := l.Slaves.Maximum msize := int(math.Min(float64(len(l.Masters.Stacked)), float64(mmax))) ssize := int(math.Min(float64(len(l.Slaves.Stacked)), float64(smax))) diff --git a/layout/vertical.go b/layout/vertical.go index 96a366d..6d79841 100644 --- a/layout/vertical.go +++ b/layout/vertical.go @@ -36,18 +36,18 @@ func (l *VerticalLayout) Reset() { mg := store.CreateManager(*l.Location) // Reset number of masters - for l.Masters.MaxAllowed < mg.Masters.MaxAllowed { + for l.Masters.Maximum < mg.Masters.Maximum { l.IncreaseMaster() } - for l.Masters.MaxAllowed > mg.Masters.MaxAllowed { + for l.Masters.Maximum > mg.Masters.Maximum { l.DecreaseMaster() } // Reset number of slaves - for l.Slaves.MaxAllowed < mg.Slaves.MaxAllowed { + for l.Slaves.Maximum < mg.Slaves.Maximum { l.IncreaseSlave() } - for l.Slaves.MaxAllowed > mg.Slaves.MaxAllowed { + for l.Slaves.Maximum > mg.Slaves.Maximum { l.DecreaseSlave() } @@ -58,11 +58,11 @@ func (l *VerticalLayout) Reset() { func (l *VerticalLayout) Apply() { clients := l.Clients(store.Stacked) - dx, dy, dw, dh := store.DesktopDimensions(l.Location.ScreenNum) + dx, dy, dw, dh := store.DesktopGeometry(l.Location.ScreenNum).Pieces() gap := common.Config.WindowGapSize - mmax := l.Masters.MaxAllowed - smax := l.Slaves.MaxAllowed + mmax := l.Masters.Maximum + smax := l.Slaves.Maximum msize := int(math.Min(float64(len(l.Masters.Stacked)), float64(mmax))) ssize := int(math.Min(float64(len(l.Slaves.Stacked)), float64(smax))) @@ -118,7 +118,7 @@ func (l *VerticalLayout) Apply() { // Move and resize master 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) + c.MoveWindow(mx+gap, my, mw-2*gap, mh) // Add y offset my += mh + gap @@ -156,7 +156,7 @@ func (l *VerticalLayout) Apply() { // Move and resize slave sp := l.Proportions.SlaveSlave[ssize][i%ssize] sh := int(math.Round(float64(dh-(ssize+1)*gap) * sp)) - c.MoveResize(sx, sy, sw-gap, sh) + c.MoveWindow(sx, sy, sw-gap, sh) // Add y offset sy += sh + gap @@ -165,13 +165,13 @@ func (l *VerticalLayout) Apply() { } func (l *VerticalLayout) UpdateProportions(c *store.Client, d *store.Directions) { - _, _, dw, dh := store.DesktopDimensions(l.Location.ScreenNum) + _, _, dw, dh := store.DesktopGeometry(l.Location.ScreenNum).Pieces() _, _, cw, ch := c.OuterGeometry() gap := common.Config.WindowGapSize - mmax := l.Masters.MaxAllowed - smax := l.Slaves.MaxAllowed + mmax := l.Masters.Maximum + smax := l.Slaves.Maximum msize := int(math.Min(float64(len(l.Masters.Stacked)), float64(mmax))) ssize := int(math.Min(float64(len(l.Slaves.Stacked)), float64(smax))) diff --git a/main.go b/main.go index f5b7147..de4e129 100644 --- a/main.go +++ b/main.go @@ -48,38 +48,71 @@ var ( func main() { + // Init process and build informations + common.InitInfo(name, version, commit, date, source) + // Init command line arguments - common.InitArgs(name, version, commit, date, source) + common.InitArgs(input.Introspect()) // Init embedded files common.InitFiles(toml, icon) - // Init lock and log files - defer InitLock().Close() - InitLog() + // Run dbus instance + runDbus() - // Init cache and config - common.InitCache() - common.InitConfig() + // Run main instance + runMain() +} - // Init root window properties - store.InitRoot() +func runDbus() { + property := len(common.Args.Dbus.Property) > 0 + method := len(common.Args.Dbus.Method) > 0 + listen := common.Args.Dbus.Listen + + // Receive dbus property + if property { + input.Property(common.Args.Dbus.Property) + } - // Run main application - run() + // Execute dbus method + if method { + input.Method(common.Args.Dbus.Method, common.Args.Dbus.Args) + } + + // Listen to dbus events + if listen { + go input.Listen(common.Args.Dbus.Args) + select {} + } + + // Prevent main instance start + if property || method || listen { + os.Exit(1) + } } -func run() { +func runMain() { defer func() { if err := recover(); err != nil { log.Fatal(fmt.Errorf("%s\n%s", err, debug.Stack())) } }() + // Init lock and log files + defer InitLock().Close() + InitLog() + + // Init cache and config + common.InitCache() + common.InitConfig() + + // Init root properties + store.InitRoot() + // Create tracker tracker := desktop.CreateTracker() ws := tracker.ActiveWorkspace() - if !ws.Disabled() { + if ws.Enabled() { ui.ShowLayout(ws) } @@ -89,6 +122,7 @@ func run() { input.BindMouse(tracker) input.BindKeys(tracker) input.BindTray(tracker) + input.BindDbus(tracker) // Run X event loop xevent.Main(store.X) diff --git a/store/client.go b/store/client.go index 061505e..1a37918 100644 --- a/store/client.go +++ b/store/client.go @@ -11,7 +11,6 @@ import ( "encoding/json" "path/filepath" - "github.com/jezek/xgb/render" "github.com/jezek/xgb/xproto" "github.com/jezek/xgbutil/ewmh" @@ -27,12 +26,11 @@ import ( ) type Client struct { - Win *xwindow.Window `json:"-"` // X window object - Created time.Time // Internal client creation time - Locked bool // Internal client move/resize lock - Original *Info // Original client window information - Cached *Info // Cached client window information - Latest *Info // Latest client window information + Window *XWindow // X window object + Original *Info `json:"-"` // Original client window information + Cached *Info `json:"-"` // Cached client window information + Latest *Info // Latest client window information + Locked bool // Internal client move/resize lock } type Info struct { @@ -44,13 +42,8 @@ type Info struct { Dimensions Dimensions // Client window dimensions } -type Location struct { - DeskNum uint // Workspace desktop number - ScreenNum uint // Workspace screen number -} - type Dimensions struct { - Geometry Geometry // Client window geometry + Geometry common.Geometry // Client window geometry Hints Hints // Client window dimension hints Extents ewmh.FrameExtents // Client window geometry extents AdjPos bool // Position adjustments on move/resize @@ -58,14 +51,6 @@ type Dimensions struct { AdjRestore bool // Disable adjustments on restore } -type Geometry struct { - xrect.Rect `json:"-"` // Client window geometry functions - X int // Client window geometry x position - Y int // Client window geometry y position - Width int // Client window geometry width dimension - Height int // Client window geometry height dimension -} - type Hints struct { Normal icccm.NormalHints // Client window geometry hints Motif motif.Hints // Client window decoration hints @@ -79,24 +64,20 @@ const ( func CreateClient(w xproto.Window) *Client { c := &Client{ - Win: xwindow.New(X, w), - Created: time.Now(), - Locked: false, + Window: CreateXWindow(w), Original: GetInfo(w), Cached: GetInfo(w), Latest: GetInfo(w), + Locked: false, } // Read client from cache cached := c.Read() // Overwrite states, geometry and location - geom := cached.Dimensions.Geometry - geom.Rect = xrect.New(geom.X, geom.Y, geom.Width, geom.Height) - - c.Cached.States = cached.States - c.Cached.Dimensions.Geometry = geom - c.Cached.Location.ScreenNum = GetScreenNum(geom.Rect) + c.Cached.States = cached.Latest.States + c.Cached.Dimensions.Geometry = cached.Latest.Dimensions.Geometry + c.Cached.Location.ScreenNum = ScreenNumGet(cached.Latest.Dimensions.Geometry.Center()) // Restore window position c.Restore(Cached) @@ -108,10 +89,6 @@ func CreateClient(w xproto.Window) *Client { return c } -func (c *Client) Activate() { - ewmh.ActiveWindowReq(X, c.Win.Id) -} - func (c *Client) Lock() { c.Locked = true } @@ -129,22 +106,29 @@ func (c *Client) UnDecorate() { mhints := c.Cached.Dimensions.Hints.Motif mhints.Flags |= motif.HintDecorations mhints.Decoration = motif.DecorationNone - motif.WmHintsSet(X, c.Win.Id, &mhints) + motif.WmHintsSet(X, c.Window.Id, &mhints) } func (c *Client) UnMaximize() { // Unmaximize window - for _, state := range c.Latest.States { - if strings.HasPrefix(state, "_NET_WM_STATE_MAXIMIZED") { - ewmh.WmStateReq(X, c.Win.Id, 0, "_NET_WM_STATE_MAXIMIZED_VERT") - ewmh.WmStateReq(X, c.Win.Id, 0, "_NET_WM_STATE_MAXIMIZED_HORZ") - break - } + if common.IsInList("_NET_WM_STATE_MAXIMIZED_VERT", c.Latest.States) || common.IsInList("_NET_WM_STATE_MAXIMIZED_HORZ", c.Latest.States) { + ewmh.WmStateReq(X, c.Window.Id, ewmh.StateRemove, "_NET_WM_STATE_MAXIMIZED_VERT") + ewmh.WmStateReq(X, c.Window.Id, ewmh.StateRemove, "_NET_WM_STATE_MAXIMIZED_HORZ") } } -func (c *Client) MoveResize(x, y, w, h int) { +func (c *Client) MoveDesktop(deskNum uint32) { + if deskNum == ^uint32(0) { + ewmh.WmStateReq(X, c.Window.Id, ewmh.StateAdd, "_NET_WM_STATE_STICKY") + } + + // Set client desktop + ewmh.WmDesktopSet(X, c.Window.Id, uint(deskNum)) + ewmh.ClientEvent(X, c.Window.Id, "_NET_WM_DESKTOP", int(deskNum), int(2)) +} + +func (c *Client) MoveWindow(x, y, w, h int) { if c.Locked { log.Info("Reject window move/resize [", c.Latest.Class, "]") @@ -153,7 +137,7 @@ func (c *Client) MoveResize(x, y, w, h int) { return } - // Remove unwanted + // Remove unwanted properties c.UnDecorate() c.UnMaximize() @@ -168,10 +152,11 @@ func (c *Client) MoveResize(x, y, w, h int) { dw, dh = ext.Left+ext.Right, ext.Top+ext.Bottom } - // Move and resize window - err := ewmh.MoveresizeWindow(X, c.Win.Id, x+dx, y+dy, w-dw, h-dh) - if err != nil { - log.Warn("Error on window move/resize [", c.Latest.Class, "]") + // Move and/or resize window + if w > 0 && h > 0 { + ewmh.MoveresizeWindow(X, c.Window.Id, x+dx, y+dy, w-dw, h-dh) + } else { + ewmh.MoveWindow(X, c.Window.Id, x+dx, y+dy) } // Update stored dimensions @@ -189,11 +174,11 @@ func (c *Client) LimitDimensions(w, h int) { nhints.Flags |= icccm.SizeHintPMinSize nhints.MinWidth = uint(w - dw) nhints.MinHeight = uint(h - dh) - icccm.WmNormalHintsSet(X, c.Win.Id, &nhints) + icccm.WmNormalHintsSet(X, c.Window.Id, &nhints) } func (c *Client) Update() { - info := GetInfo(c.Win.Id) + info := GetInfo(c.Window.Id) if len(info.Class) == 0 { return } @@ -226,12 +211,12 @@ func (c *Client) Write() { return } - log.Debug("Write client cache data ", cache.Name, " [", c.Latest.Class, "]") + log.Trace("Write client cache data ", cache.Name, " [", c.Latest.Class, "]") } -func (c *Client) Read() *Info { +func (c *Client) Read() *Client { if !common.CacheEnabled() { - return c.Latest + return c } // Obtain cache object @@ -242,15 +227,15 @@ func (c *Client) Read() *Info { data, err := os.ReadFile(path) if os.IsNotExist(err) { log.Info("No client cache found [", c.Latest.Class, "]") - return c.Latest + return c } // Parse client cache - cached := &Info{} + cached := &Client{} err = json.Unmarshal([]byte(data), &cached) if err != nil { log.Warn("Error reading client cache [", c.Latest.Class, "]") - return c.Latest + return c } log.Debug("Read client cache data ", cache.Name, " [", c.Latest.Class, "]") @@ -258,7 +243,7 @@ func (c *Client) Read() *Info { return cached } -func (c *Client) Cache() common.Cache[*Info] { +func (c *Client) Cache() common.Cache[*Client] { name := c.Latest.Class hash := fmt.Sprintf("%s-%d", c.Latest.Class, c.Latest.Location.DeskNum) @@ -269,10 +254,10 @@ func (c *Client) Cache() common.Cache[*Info] { } // Create client cache object - cache := common.Cache[*Info]{ + cache := common.Cache[*Client]{ Folder: folder, - Name: common.Hash(hash) + ".json", - Data: c.Latest, + Name: common.HashString(hash) + ".json", + Data: c, } return cache @@ -283,17 +268,18 @@ func (c *Client) Restore(flag uint8) { c.Update() } + // Restore window states + if flag == Cached { + if IsSticky(c.Cached) { + c.MoveDesktop(^uint32(0)) + } + } + // Restore window size limits - icccm.WmNormalHintsSet(X, c.Win.Id, &c.Cached.Dimensions.Hints.Normal) + icccm.WmNormalHintsSet(X, c.Window.Id, &c.Cached.Dimensions.Hints.Normal) // Restore window decorations - motif.WmHintsSet(X, c.Win.Id, &c.Cached.Dimensions.Hints.Motif) - - // Restore window 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)) - } + motif.WmHintsSet(X, c.Window.Id, &c.Cached.Dimensions.Hints.Motif) // Disable adjustments on restore if c.Latest.Dimensions.AdjRestore { @@ -309,19 +295,19 @@ func (c *Client) Restore(flag uint8) { case Cached: geom = c.Cached.Dimensions.Geometry } - c.MoveResize(geom.X, geom.Y, geom.Width, geom.Height) + c.MoveWindow(geom.X, geom.Y, geom.Width, geom.Height) } func (c *Client) OuterGeometry() (x, y, w, h int) { // Outer window dimensions (x/y relative to workspace) - oGeom, err := c.Win.DecorGeometry() + oGeom, err := c.Window.Instance.DecorGeometry() if err != nil { return } // Inner window dimensions (x/y relative to outer window) - iGeom, err := xwindow.RawGeometry(X, xproto.Drawable(c.Win.Id)) + iGeom, err := xwindow.RawGeometry(X, xproto.Drawable(c.Window.Id)) if err != nil { return } @@ -342,6 +328,11 @@ func (c *Client) OuterGeometry() (x, y, w, h int) { return } +func (c *Client) IsNew() bool { + created := time.UnixMilli(c.Window.Created) + return time.Since(created) < 1000*time.Millisecond +} + func IsSpecial(info *Info) bool { // Check internal windows @@ -423,18 +414,16 @@ func IsIgnored(info *Info) bool { return false } -func IsMaximized(w xproto.Window) bool { - info := GetInfo(w) +func IsMaximized(info *Info) bool { + return common.IsInList("_NET_WM_STATE_MAXIMIZED_VERT", info.States) && common.IsInList("_NET_WM_STATE_MAXIMIZED_HORZ", info.States) +} - // Check maximized windows - for _, state := range info.States { - if strings.HasPrefix(state, "_NET_WM_STATE_MAXIMIZED") { - log.Info("Ignore maximized window [", info.Class, "]") - return true - } - } +func IsMinimized(info *Info) bool { + return common.IsInList("_NET_WM_STATE_HIDDEN", info.States) +} - return false +func IsSticky(info *Info) bool { + return common.IsInList("_NET_WM_STATE_STICKY", info.States) } func GetInfo(w xproto.Window) *Info { @@ -450,7 +439,7 @@ func GetInfo(w xproto.Window) *Info { // Window class (internal class name of the window) cls, err := icccm.WmClassGet(X, w) if err != nil { - log.Trace("Error on request ", err) + log.Trace("Error on request: ", err) } else if cls != nil { class = cls.Class } @@ -462,7 +451,7 @@ func GetInfo(w xproto.Window) *Info { } // Window geometry (dimensions of the window) - geom, err := xwindow.New(X, w).DecorGeometry() + geom, err := CreateXWindow(w).Instance.DecorGeometry() if err != nil { geom = &xrect.XRect{} } @@ -475,7 +464,7 @@ func GetInfo(w xproto.Window) *Info { } location = Location{ DeskNum: deskNum, - ScreenNum: GetScreenNum(geom), + ScreenNum: ScreenNumGet(common.CreateGeometry(geom).Center()), } // Window types (types of the window) @@ -519,13 +508,7 @@ func GetInfo(w xproto.Window) *Info { // Window dimensions (geometry/extent information for move/resize) dimensions = Dimensions{ - Geometry: Geometry{ - Rect: geom, - X: geom.X(), - Y: geom.Y(), - Width: geom.Width(), - Height: geom.Height(), - }, + Geometry: *common.CreateGeometry(geom), Hints: Hints{ Normal: *nhints, Motif: *mhints, @@ -536,9 +519,9 @@ func GetInfo(w xproto.Window) *Info { Top: int(ext[2]), Bottom: int(ext[3]), }, - AdjPos: !common.IsZero(extNet) && (mhints.Decoration > 1 || nhints.WinGravity > 1) || !common.IsZero(extGtk), - AdjSize: !common.IsZero(extNet) || !common.IsZero(extGtk), - AdjRestore: !common.IsZero(extGtk), + AdjPos: !common.AllZero(extNet) && (mhints.Decoration > 1 || nhints.WinGravity > 1) || !common.AllZero(extGtk), + AdjSize: !common.AllZero(extNet) || !common.AllZero(extGtk), + AdjRestore: !common.AllZero(extGtk), } return &Info{ @@ -550,14 +533,3 @@ func GetInfo(w xproto.Window) *Info { Dimensions: dimensions, } } - -func GetScreenNum(geom xrect.Rect) uint { - - // Window center position - center := render.Pointfix{ - X: render.Fixed(geom.X() + geom.Width()/2), - Y: render.Fixed(geom.Y() + geom.Height()/2), - } - - return ScreenNumGet(center) -} diff --git a/store/corner.go b/store/corner.go index f628306..63e5a4a 100644 --- a/store/corner.go +++ b/store/corner.go @@ -1,47 +1,52 @@ package store import ( - "github.com/jezek/xgbutil/xrect" - "github.com/leukipp/cortile/v2/common" + + log "github.com/sirupsen/logrus" ) type Corner struct { - Name string // Corner name used in config - ScreenNum uint // Screen number the corner is located - Area xrect.Rect // Rectangle area of the corner section - Active bool // Mouse pointer is in this corner + Name string // Corner name used in config + Active bool // Mouse pointer is in this corner + ScreenNum uint // Screen number the corner is located + Geometry common.Geometry // Geometry of the corner section } func CreateCorner(name string, screenNum uint, x int, y int, w int, h int) *Corner { return &Corner{ Name: name, - ScreenNum: screenNum, - Area: xrect.New(x, y, w, h), Active: false, + ScreenNum: screenNum, + Geometry: common.Geometry{ + X: x, + Y: y, + Width: w, + Height: h, + }, } } -func CreateCorners(screens []*XHead) []*Corner { +func CreateCorners(screens []XHead) []*Corner { corners := []*Corner{} for i, screen := range screens { screenNum := uint(i) - xw, yw, ww, hw := screen.Pieces() + x, y, w, h := screen.Geometry.Pieces() // Corner dimensions - wcs, hcs := common.Config.EdgeCornerSize, common.Config.EdgeCornerSize - wcl, hcl := common.Config.EdgeCenterSize, common.Config.EdgeCenterSize + ws, hs := common.Config.EdgeCornerSize, common.Config.EdgeCornerSize + wl, hl := common.Config.EdgeCenterSize, common.Config.EdgeCenterSize // Define corners and positions - tl := CreateCorner("top_left", screenNum, xw, yw, wcs, hcs) - tc := CreateCorner("top_center", screenNum, (xw+ww)/2-wcl/2, yw, wcl, hcs) - tr := CreateCorner("top_right", screenNum, xw+ww-wcs, yw, wcs, hcs) - cr := CreateCorner("center_right", screenNum, xw+ww-wcs, (yw+hw)/2-hcl/2, wcs, hcl) - br := CreateCorner("bottom_right", screenNum, xw+ww-wcs, yw+hw-hcs, wcs, hcs) - bc := CreateCorner("bottom_center", screenNum, (xw+ww)/2-wcl/2, yw+hw-hcs, wcl, hcl) - bl := CreateCorner("bottom_left", screenNum, xw, yw+hw-hcs, wcs, hcs) - cl := CreateCorner("center_left", screenNum, xw, (yw+hw)/2-hcl/2, wcs, hcl) + tl := CreateCorner("top_left", screenNum, x, y, ws, hs) + tc := CreateCorner("top_center", screenNum, x+w/2-wl/2, y, wl, hs) + tr := CreateCorner("top_right", screenNum, x+w-ws, y, ws, hs) + cr := CreateCorner("center_right", screenNum, x+w-ws, y+h/2-hl/2, ws, hl) + br := CreateCorner("bottom_right", screenNum, x+w-ws, y+h-hs, ws, hs) + bc := CreateCorner("bottom_center", screenNum, x+w/2-wl/2, y+h-hs, wl, hl) + bl := CreateCorner("bottom_left", screenNum, x, y+h-hs, ws, hs) + cl := CreateCorner("center_left", screenNum, x, y+h/2-hl/2, ws, hl) corners = append(corners, []*Corner{tl, tc, tr, cr, br, bc, bl, cl}...) } @@ -52,7 +57,31 @@ func CreateCorners(screens []*XHead) []*Corner { func (c *Corner) IsActive(p *XPointer) bool { // Check if pointer is inside rectangle - c.Active = common.IsInsideRect(p.Position, c.Area) + c.Active = common.IsInsideRect(p.Position, c.Geometry) return c.Active } + +func HotCorner() *Corner { + + // Update active states + for i := range Workplace.Displays.Corners { + hc := Workplace.Displays.Corners[i] + + wasActive := hc.Active + isActive := hc.IsActive(Pointer) + + // Corner is hot + if !wasActive && isActive { + log.Debug("Corner at position ", hc.Geometry, " is hot [", hc.Name, "]") + return hc + } + + // Corner was hot + if wasActive && !isActive { + log.Debug("Corner at position ", hc.Geometry, " is cold [", hc.Name, "]") + } + } + + return nil +} diff --git a/store/manager.go b/store/manager.go index 3d8e2f6..3c32017 100644 --- a/store/manager.go +++ b/store/manager.go @@ -17,11 +17,9 @@ type Manager struct { Slaves *Clients // List of slave window clients } -type Directions struct { - Top bool // Indicates proportion changes on the top - Right bool // Indicates proportion changes on the right - Bottom bool // Indicates proportion changes on the bottom - Left bool // Indicates proportion changes on the left +type Location struct { + DeskNum uint // Location desktop number + ScreenNum uint // Location screen number } type Proportions struct { @@ -31,13 +29,21 @@ type Proportions struct { } type Clients struct { - Stacked []*Client `json:"-"` // List of stored window clients - MaxAllowed int // Currently maximum allowed clients + Maximum int // Currently maximum allowed clients + Stacked []*Client `json:"-"` // List of stored window clients +} + +type Directions struct { + Top bool // Indicates proportion changes on the top + Right bool // Indicates proportion changes on the right + Bottom bool // Indicates proportion changes on the bottom + Left bool // Indicates proportion changes on the left } const ( - Stacked uint8 = 1 // Flag for stacked (all) clients - Visible uint8 = 2 // Flag for visible (top) clients + Stacked uint8 = 1 // Flag for stacked (internal index order) clients + Ordered uint8 = 2 // Flag for ordered (bottom to top order) clients + Visible uint8 = 3 // Flag for visible (top only) clients ) func CreateManager(loc Location) *Manager { @@ -50,12 +56,12 @@ func CreateManager(loc Location) *Manager { SlaveSlave: calcProportions(common.Config.WindowSlavesMax), }, Masters: &Clients{ - Stacked: make([]*Client, 0), - MaxAllowed: 1, + Maximum: 1, + Stacked: make([]*Client, 0), }, Slaves: &Clients{ - Stacked: make([]*Client, 0), - MaxAllowed: common.Config.WindowSlavesMax, + Maximum: common.Config.WindowSlavesMax, + Stacked: make([]*Client, 0), }, } } @@ -68,7 +74,7 @@ func (mg *Manager) AddClient(c *Client) { log.Debug("Add client for manager [", c.Latest.Class, ", ", mg.Name, "]") // Fill up master area then slave area - if len(mg.Masters.Stacked) < mg.Masters.MaxAllowed { + if len(mg.Masters.Stacked) < mg.Masters.Maximum { mg.Masters.Stacked = addClient(mg.Masters.Stacked, c) } else { mg.Slaves.Stacked = addClient(mg.Slaves.Stacked, c) @@ -146,7 +152,7 @@ func (mg *Manager) NextClient() *Client { // Get next window next := -1 for i, c := range clients { - if c.Win.Id == Windows.Active { + if c.Window.Id == Windows.Active.Id { next = i + 1 if next > last { next = 0 @@ -170,7 +176,7 @@ func (mg *Manager) PreviousClient() *Client { // Get previous window prev := -1 for i, c := range clients { - if c.Win.Id == Windows.Active { + if c.Window.Id == Windows.Active.Id { prev = i - 1 if prev < 0 { prev = last @@ -190,45 +196,45 @@ func (mg *Manager) PreviousClient() *Client { func (mg *Manager) IncreaseMaster() { // Increase master area - if len(mg.Slaves.Stacked) > 1 && mg.Masters.MaxAllowed < common.Config.WindowMastersMax { - mg.Masters.MaxAllowed += 1 + if len(mg.Slaves.Stacked) > 1 && mg.Masters.Maximum < common.Config.WindowMastersMax { + mg.Masters.Maximum += 1 mg.Masters.Stacked = append(mg.Masters.Stacked, mg.Slaves.Stacked[0]) mg.Slaves.Stacked = mg.Slaves.Stacked[1:] } - log.Info("Increase masters to ", mg.Masters.MaxAllowed) + log.Info("Increase masters to ", mg.Masters.Maximum) } func (mg *Manager) DecreaseMaster() { // Decrease master area if len(mg.Masters.Stacked) > 0 { - mg.Masters.MaxAllowed -= 1 + mg.Masters.Maximum -= 1 mg.Slaves.Stacked = append([]*Client{mg.Masters.Stacked[len(mg.Masters.Stacked)-1]}, mg.Slaves.Stacked...) mg.Masters.Stacked = mg.Masters.Stacked[:len(mg.Masters.Stacked)-1] } - log.Info("Decrease masters to ", mg.Masters.MaxAllowed) + log.Info("Decrease masters to ", mg.Masters.Maximum) } func (mg *Manager) IncreaseSlave() { // Increase slave area - if mg.Slaves.MaxAllowed < common.Config.WindowSlavesMax { - mg.Slaves.MaxAllowed += 1 + if mg.Slaves.Maximum < common.Config.WindowSlavesMax { + mg.Slaves.Maximum += 1 } - log.Info("Increase slaves to ", mg.Slaves.MaxAllowed) + log.Info("Increase slaves to ", mg.Slaves.Maximum) } func (mg *Manager) DecreaseSlave() { // Decrease slave area - if mg.Slaves.MaxAllowed > 1 { - mg.Slaves.MaxAllowed -= 1 + if mg.Slaves.Maximum > 1 { + mg.Slaves.Maximum -= 1 } - log.Info("Decrease slaves to ", mg.Slaves.MaxAllowed) + log.Info("Decrease slaves to ", mg.Slaves.Maximum) } func (mg *Manager) IncreaseProportion() { @@ -290,7 +296,7 @@ func (mg *Manager) Index(windows *Clients, c *Client) int { // Traverse client list for i, m := range windows.Stacked { - if m.Win.Id == c.Win.Id { + if m.Window.Id == c.Window.Id { return i } } @@ -304,7 +310,7 @@ func (mg *Manager) Ordered(windows *Clients) []*Client { // Create ordered client list for _, w := range Windows.Stacked { for _, c := range windows.Stacked { - if w == c.Win.Id { + if w.Id == c.Window.Id { ordered = append(ordered, c) break } @@ -315,11 +321,11 @@ func (mg *Manager) Ordered(windows *Clients) []*Client { } func (mg *Manager) Visible(windows *Clients) []*Client { - visible := make([]*Client, int(math.Min(float64(len(windows.Stacked)), float64(windows.MaxAllowed)))) + visible := make([]*Client, int(math.Min(float64(len(windows.Stacked)), float64(windows.Maximum)))) // Create visible client list for _, c := range mg.Ordered(windows) { - visible[mg.Index(windows, c)%windows.MaxAllowed] = c + visible[mg.Index(windows, c)%windows.Maximum] = c } return visible @@ -329,6 +335,8 @@ func (mg *Manager) Clients(flag uint8) []*Client { switch flag { case Stacked: return append(mg.Masters.Stacked, mg.Slaves.Stacked...) + case Ordered: + return append(mg.Ordered(mg.Masters), mg.Ordered(mg.Slaves)...) case Visible: return append(mg.Visible(mg.Masters), mg.Visible(mg.Slaves)...) } diff --git a/store/root.go b/store/root.go index b1b3f4d..51796be 100644 --- a/store/root.go +++ b/store/root.go @@ -7,7 +7,6 @@ import ( "time" "github.com/jezek/xgb/randr" - "github.com/jezek/xgb/render" "github.com/jezek/xgb/xproto" "github.com/jezek/xgbutil" @@ -24,48 +23,99 @@ import ( var ( X *xgbutil.XUtil // X connection + Workplace *XWorkplace // X workplace Pointer *XPointer // X pointer Windows *XWindows // X windows - Workplace *XWorkplace // X workplace ) -var ( - pointerCallbacksFun []func(uint16, uint, uint) // Pointer events callback functions - stateCallbacksFun []func(string, uint, uint) // State events callback functions -) +type XWorkplace struct { + DeskCount uint // Number of desktops + ScreenCount uint // Number of screens + CurrentDesk uint // Current desktop number + CurrentScreen uint // Current screen number + Displays XDisplays // Physical connected displays +} + +type XDisplays struct { + Name string // Unique heads name (display summary) + Screens []XHead // Screen dimensions (full display size) + Desktops []XHead // Desktop dimensions (desktop without panels) + Corners []*Corner // Display corners (for pointer events) +} + +type XHead struct { + Id uint32 // Head output id (display id) + Name string // Head output name (display name) + Primary bool // Head primary flag (primary display) + Geometry common.Geometry // Head dimensions (x/y/width/height) +} type XPointer struct { - Button uint16 // Pointer device button states - Position render.Pointfix // Pointer position coordinates + Drag XDrag // Pointer device drag states + Button XButton // Pointer device button states + Position common.Point // Pointer position coordinates } -type XWindows struct { - Active xproto.Window // Current active window - Stacked []xproto.Window // List of stacked windows +func (p *XPointer) Dragging(dt time.Duration) bool { + return p.Drag.Left(dt) || p.Drag.Middle(dt) || p.Drag.Right(dt) } -type XWorkplace struct { - Displays XHeads // Physical connected displays - DeskCount uint // Number of desktops - ScreenCount uint // Number of screens - CurrentDesk uint // Current desktop number - CurrentScreen uint // Current screen number +func (p *XPointer) Pressed() bool { + return p.Button.Left || p.Button.Middle || p.Button.Right } -type XHeads struct { - Name string // Unique heads name (display summary) - Screens []*XHead // Screen dimensions (full display size) - Desktops []*XHead // Desktop dimensions (desktop without panels) - Corners []*Corner // Display corners (for pointer events) +func (p *XPointer) Press() { + p.Button = XButton{true, true, true} } -type XHead struct { - Id uint32 // Head output id (display id) - Name string // Head output name (display name) - Primary bool // Head primary flag (primary display) - xrect.Rect // Head dimensions (x/y/width/height) +type XDrag struct { + LeftTime int64 // Pointer left last drag time + MiddleTime int64 // Pointer middle last drag time + RightTime int64 // Pointer right last drag time +} + +func (d *XDrag) Left(dt time.Duration) bool { + return time.Since(time.UnixMilli(d.LeftTime)) < dt*time.Millisecond +} + +func (d *XDrag) Middle(dt time.Duration) bool { + return time.Since(time.UnixMilli(d.MiddleTime)) < dt*time.Millisecond } +func (d *XDrag) Right(dt time.Duration) bool { + return time.Since(time.UnixMilli(d.RightTime)) < dt*time.Millisecond +} + +type XButton struct { + Left bool // Pointer left click + Middle bool // Pointer middle click + Right bool // Pointer right click +} + +type XWindows struct { + Active XWindow // Current active window + Stacked []XWindow // List of stacked windows +} + +type XWindow struct { + Id xproto.Window // Window object id + Created int64 // Internal creation timestamp + Instance *xwindow.Window `json:"-"` // Window object instance +} + +func CreateXWindow(w xproto.Window) *XWindow { + return &XWindow{ + Id: w, + Created: time.Now().UnixMilli(), + Instance: xwindow.New(X, w), + } +} + +var ( + stateCallbacksFun []func(string, uint, uint) // State events callback functions + pointerCallbacksFun []func(XPointer, uint, uint) // Pointer events callback functions +) + func InitRoot() { // Connect to X server @@ -90,8 +140,8 @@ func InitRoot() { Workplace.CurrentScreen = ScreenNumGet(Pointer.Position) // Attach root events - root := xwindow.New(X, X.RootWin()) - root.Listen(xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange) + root := CreateXWindow(X.RootWin()) + root.Instance.Listen(xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange) xevent.PropertyNotifyFun(StateUpdate).Connect(X, root.Id) } @@ -129,7 +179,7 @@ func Connected() bool { } // Connection to X established - log.Info("Connected to X server [", wm, "]") + log.Info("Connected to X server on ", common.Process.Host, " [", common.Process.System, ", ", wm, "]") randr.Init(X.Conn()) connected = true } @@ -161,8 +211,14 @@ func CurrentDesktopGet(X *xgbutil.XUtil) uint { return currentDesk } -func ActiveWindowGet(X *xgbutil.XUtil) xproto.Window { - activeWindow, err := ewmh.ActiveWindowGet(X) +func CurrentDesktopSet(X *xgbutil.XUtil, deskNum uint) { + ewmh.CurrentDesktopSet(X, deskNum) + ewmh.ClientEvent(X, X.RootWin(), "_NET_CURRENT_DESKTOP", int(deskNum), int(0)) + Workplace.CurrentDesk = deskNum +} + +func ActiveWindowGet(X *xgbutil.XUtil) XWindow { + active, err := ewmh.ActiveWindowGet(X) // Validate active window if err != nil { @@ -170,11 +226,17 @@ func ActiveWindowGet(X *xgbutil.XUtil) xproto.Window { return Windows.Active } - return activeWindow + return *CreateXWindow(active) +} + +func ActiveWindowSet(X *xgbutil.XUtil, w *XWindow) { + ewmh.ActiveWindowSet(X, w.Id) + ewmh.ClientEvent(X, w.Id, "_NET_ACTIVE_WINDOW", int(2), int(0), int(0)) + Windows.Active = *CreateXWindow(w.Id) } -func ClientListStackingGet(X *xgbutil.XUtil) []xproto.Window { - windows, err := ewmh.ClientListStackingGet(X) +func ClientListStackingGet(X *xgbutil.XUtil) []XWindow { + clients, err := ewmh.ClientListStackingGet(X) // Validate client list if err != nil { @@ -182,14 +244,21 @@ func ClientListStackingGet(X *xgbutil.XUtil) []xproto.Window { return Windows.Stacked } + // Create windows + windows := []XWindow{} + for _, w := range clients { + windows = append(windows, *CreateXWindow(w)) + } + return windows } -func DisplaysGet(X *xgbutil.XUtil) XHeads { +func DisplaysGet(X *xgbutil.XUtil) XDisplays { var name string // Get geometry of root window - rGeom, err := xwindow.New(X, X.RootWin()).Geometry() + root := CreateXWindow(X.RootWin()) + geom, err := root.Instance.Geometry() if err != nil { log.Fatal("Error retrieving root geometry: ", err) } @@ -200,35 +269,39 @@ func DisplaysGet(X *xgbutil.XUtil) XHeads { // Get heads name for _, screen := range screens { - x, y, w, h := screen.Rect.Pieces() + x, y, w, h := screen.Geometry.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{} + rects := []xrect.Rect{} for _, desktop := range desktops { - dRects = append(dRects, desktop.Rect) + rects = append(rects, desktop.Geometry.Rect()) } - // Account for desktop panels - for _, win := range Windows.Stacked { - strut, err := ewmh.WmStrutPartialGet(X, win) + // Get margins of desktop panels + for _, w := range Windows.Stacked { + strut, err := ewmh.WmStrutPartialGet(X, w.Id) if err != nil { continue } - // Apply in place struts to desktop - _, _, w, h := rGeom.Pieces() - xrect.ApplyStrut(dRects, uint(w), uint(h), + // Apply struts to rectangles in place + xrect.ApplyStrut(rects, uint(geom.Width()), uint(geom.Height()), strut.Left, strut.Right, strut.Top, strut.Bottom, strut.LeftStartY, strut.LeftEndY, strut.RightStartY, strut.RightEndY, strut.TopStartX, strut.TopEndX, strut.BottomStartX, strut.BottomEndX, ) } + // Update desktop geometry + for i := range desktops { + desktops[i].Geometry = *common.CreateGeometry(rects[i]) + } + // Create display heads - heads := XHeads{Name: name} + heads := XDisplays{Name: name} heads.Screens = screens heads.Desktops = desktops heads.Corners = CreateCorners(screens) @@ -238,12 +311,11 @@ func DisplaysGet(X *xgbutil.XUtil) XHeads { log.Info("Screens ", heads.Screens) log.Info("Desktops ", heads.Desktops) - log.Info("Corners ", heads.Corners) return heads } -func PhysicalHeadsGet(X *xgbutil.XUtil) []*XHead { +func PhysicalHeadsGet(X *xgbutil.XUtil) []XHead { // Get screen resources resources, err := randr.GetScreenResources(X.Conn(), X.RootWin()).Reply() @@ -259,8 +331,8 @@ func PhysicalHeadsGet(X *xgbutil.XUtil) []*XHead { hasPrimary := false // Get output heads - heads := []*XHead{} - biggest := XHead{Rect: xrect.New(0, 0, 0, 0)} + heads := []XHead{} + biggest := XHead{} for _, output := range resources.Outputs { oinfo, err := randr.GetOutputInfo(X.Conn(), output, 0).Reply() if err != nil { @@ -283,18 +355,18 @@ func PhysicalHeadsGet(X *xgbutil.XUtil) []*XHead { Id: uint32(output), Name: string(oinfo.Name), Primary: primary != nil && output == primary.Output, - Rect: xrect.New( - int(cinfo.X), - int(cinfo.Y), - int(cinfo.Width), - int(cinfo.Height), - ), + Geometry: common.Geometry{ + X: int(cinfo.X), + Y: int(cinfo.Y), + Width: int(cinfo.Width), + Height: int(cinfo.Height), + }, } - heads = append(heads, &head) + heads = append(heads, head) // Set helper variables hasPrimary = head.Primary || hasPrimary - if head.Width()*head.Height() > biggest.Rect.Width()*biggest.Rect.Height() { + if head.Geometry.Width*head.Geometry.Height > biggest.Geometry.Width*biggest.Geometry.Height { biggest = head } } @@ -310,7 +382,7 @@ func PhysicalHeadsGet(X *xgbutil.XUtil) []*XHead { // Sort output heads sort.Slice(heads, func(i, j int) bool { - return heads[i].X() < heads[j].X() + return heads[i].Geometry.X < heads[j].Geometry.X }) return heads @@ -326,19 +398,24 @@ func PointerGet(X *xgbutil.XUtil) *XPointer { } return &XPointer{ - Button: p.Mask&xproto.ButtonMask1 | p.Mask&xproto.ButtonMask2 | p.Mask&xproto.ButtonMask3, - Position: render.Pointfix{ - X: render.Fixed(p.RootX), - Y: render.Fixed(p.RootY), + Drag: XDrag{}, + Button: XButton{ + Left: p.Mask&xproto.ButtonMask1 == xproto.ButtonMask1, + Middle: p.Mask&xproto.ButtonMask2 == xproto.ButtonMask2, + Right: p.Mask&xproto.ButtonMask3 == xproto.ButtonMask3, + }, + Position: common.Point{ + X: int(p.RootX), + Y: int(p.RootY), }, } } -func ScreenNumGet(p render.Pointfix) uint { +func ScreenNumGet(p common.Point) uint { // Check if point is inside screen rectangle - for screenNum, rect := range Workplace.Displays.Screens { - if common.IsInsideRect(p, rect) { + for screenNum, screen := range Workplace.Displays.Screens { + if common.IsInsideRect(p, screen.Geometry) { return uint(screenNum) } } @@ -346,21 +423,20 @@ func ScreenNumGet(p render.Pointfix) uint { return 0 } -func DesktopDimensions(screenNum uint) (x, y, w, h int) { +func DesktopGeometry(screenNum uint) *common.Geometry { if int(screenNum) >= len(Workplace.Displays.Desktops) { - return + return &common.Geometry{} } desktop := Workplace.Displays.Desktops[screenNum] - // Get desktop dimensions - x, y, w, h = desktop.Pieces() + // Get desktop geometry + x, y, w, h := desktop.Geometry.Pieces() // Add desktop margin margin := common.Config.EdgeMargin if desktop.Primary && len(common.Config.EdgeMarginPrimary) > 0 { margin = common.Config.EdgeMarginPrimary } - if len(margin) == 4 { x += margin[3] y += margin[0] @@ -368,24 +444,47 @@ func DesktopDimensions(screenNum uint) (x, y, w, h int) { h -= margin[2] + margin[0] } - return + return &common.Geometry{ + X: x, + Y: y, + Width: w, + Height: h, + } } func PointerUpdate(X *xgbutil.XUtil) *XPointer { - - // Update current pointer - previousButton := uint16(0) + previous := XPointer{XDrag{}, XButton{}, common.Point{}} if Pointer != nil { - previousButton = Pointer.Button + previous = *Pointer } + + // Update current pointer Pointer = PointerGet(X) // Update current screen Workplace.CurrentScreen = ScreenNumGet(Pointer.Position) + // Update pointer left button drag + Pointer.Drag.LeftTime = previous.Drag.LeftTime + if Pointer.Button.Left { + Pointer.Drag.LeftTime = time.Now().UnixMilli() + } + + // Update pointer middle button drag + Pointer.Drag.MiddleTime = previous.Drag.MiddleTime + if Pointer.Button.Middle { + Pointer.Drag.MiddleTime = time.Now().UnixMilli() + } + + // Update pointer right button drag + Pointer.Drag.RightTime = previous.Drag.RightTime + if Pointer.Button.Right { + Pointer.Drag.RightTime = time.Now().UnixMilli() + } + // Pointer callbacks - if previousButton != Pointer.Button { - pointerCallbacks(Pointer.Button, Workplace.CurrentDesk, Workplace.CurrentScreen) + if previous.Button != Pointer.Button { + pointerCallbacks(*Pointer, Workplace.CurrentDesk, Workplace.CurrentScreen) } return Pointer @@ -415,7 +514,7 @@ func StateUpdate(X *xgbutil.XUtil, e xevent.PropertyNotifyEvent) { stateCallbacks(aname, Workplace.CurrentDesk, Workplace.CurrentScreen) } -func OnPointerUpdate(fun func(uint16, uint, uint)) { +func OnPointerUpdate(fun func(XPointer, uint, uint)) { pointerCallbacksFun = append(pointerCallbacksFun, fun) } @@ -423,11 +522,11 @@ func OnStateUpdate(fun func(string, uint, uint)) { stateCallbacksFun = append(stateCallbacksFun, fun) } -func pointerCallbacks(button uint16, desk uint, screen uint) { - log.Info("Pointer event ", button) +func pointerCallbacks(pointer XPointer, desk uint, screen uint) { + log.Info("Pointer event ", pointer.Button) for _, fun := range pointerCallbacksFun { - fun(button, desk, screen) + fun(pointer, desk, screen) } } diff --git a/ui/gui.go b/ui/gui.go index 836b0e1..056dfec 100644 --- a/ui/gui.go +++ b/ui/gui.go @@ -16,7 +16,6 @@ import ( "github.com/jezek/xgbutil/motif" "github.com/jezek/xgbutil/xevent" "github.com/jezek/xgbutil/xgraphics" - "github.com/jezek/xgbutil/xrect" "github.com/jezek/xgbutil/xwindow" "github.com/leukipp/cortile/v2/common" @@ -52,8 +51,8 @@ func ShowLayout(ws *desktop.Workspace) { } // Calculate scaled desktop dimensions - dim := xrect.New(store.DesktopDimensions(ws.Location.ScreenNum)) - _, _, width, height := scale(dim.X(), dim.Y(), dim.Width(), dim.Height()) + dim := store.DesktopGeometry(ws.Location.ScreenNum) + _, _, width, height := scale(dim.X, dim.Y, dim.Width, dim.Height) // Create an empty canvas image bg := bgra("gui_background") @@ -85,18 +84,18 @@ func drawClients(cv *xgraphics.Image, ws *desktop.Workspace, layout string) { // Obtain fullscreen client for _, state := range c.Latest.States { if state == "_NET_WM_STATE_FULLSCREEN" || layout == "fullscreen" { - clients = mg.Visible(&store.Clients{Stacked: mg.Clients(store.Stacked), MaxAllowed: 1}) + clients = mg.Visible(&store.Clients{Stacked: mg.Clients(store.Stacked), Maximum: 1}) break } } } // Draw default rectangle - dim := xrect.New(store.DesktopDimensions(ws.Location.ScreenNum)) + dim := store.DesktopGeometry(ws.Location.ScreenNum) if len(clients) == 0 || layout == "disabled" { // Calculate scaled desktop dimensions - x, y, width, height := scale(0, 0, dim.Width(), dim.Height()) + x, y, width, height := scale(0, 0, dim.Width, dim.Height) // Draw client rectangle onto canvas color := bgra("gui_client_slave") @@ -113,7 +112,7 @@ func drawClients(cv *xgraphics.Image, ws *desktop.Workspace, layout string) { // Calculate scaled client dimensions cx, cy, cw, ch := c.OuterGeometry() - x, y, width, height := scale(cx-dim.X(), cy-dim.Y(), cw, ch) + x, y, width, height := scale(cx-dim.X, cy-dim.Y, cw, ch) // Calculate icon size iconSize := math.MaxInt @@ -136,7 +135,7 @@ func drawClients(cv *xgraphics.Image, ws *desktop.Workspace, layout string) { drawImage(cv, rect, color, x+rectMargin, y+rectMargin, x+width, y+height) // Draw client icon onto canvas - ico, err := xgraphics.FindIcon(store.X, c.Win.Id, iconSize, iconSize) + ico, err := xgraphics.FindIcon(store.X, c.Window.Id, iconSize, iconSize) if err == nil { drawImage(cv, ico, color, x+rectMargin/2+width/2-iconSize/2, y+rectMargin/2+height/2-iconSize/2, x+width, y+height) } @@ -178,9 +177,9 @@ func showGraphics(img *xgraphics.Image, ws *desktop.Workspace, duration time.Dur } // Calculate window dimensions - dim := xrect.New(store.DesktopDimensions(ws.Location.ScreenNum)) + dim := store.DesktopGeometry(ws.Location.ScreenNum) w, h := img.Rect.Dx(), img.Rect.Dy() - x, y := dim.X()+dim.Width()/2-w/2, dim.Y()+dim.Height()/2-h/2 + x, y := dim.X+dim.Width/2-w/2, dim.Y+dim.Height/2-h/2 // Create the graphics window win.Create(img.X.RootWin(), x, y, w, h, 0)