mirror of
https://codeberg.org/scip/swaycycle.git
synced 2025-12-16 20:11:02 +01:00
first version
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
swaycycle
|
||||||
87
Makefile
Normal file
87
Makefile
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Copyright © 2025 Thomas von Dein
|
||||||
|
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# no need to modify anything below
|
||||||
|
tool = swaycycle
|
||||||
|
VERSION = $(shell grep VERSION main.go | head -1 | cut -d '"' -f2)
|
||||||
|
archs = darwin freebsd linux
|
||||||
|
PREFIX = /usr/local
|
||||||
|
UID = root
|
||||||
|
GID = 0
|
||||||
|
HAVE_POD := $(shell pod2text -h 2>/dev/null)
|
||||||
|
|
||||||
|
all: buildlocal
|
||||||
|
|
||||||
|
|
||||||
|
buildlocal:
|
||||||
|
CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags "-extldflags=-static" -o $(tool)
|
||||||
|
|
||||||
|
install: buildlocal
|
||||||
|
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
|
||||||
|
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
|
||||||
|
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
|
||||||
|
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(tool) coverage.out testdata t/out
|
||||||
|
|
||||||
|
test: clean
|
||||||
|
mkdir -p t/out
|
||||||
|
go test ./... $(ARGS)
|
||||||
|
|
||||||
|
testlint: test lint
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
lint-full:
|
||||||
|
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest
|
||||||
|
|
||||||
|
testfuzzy: clean
|
||||||
|
go test -fuzz ./... $(ARGS)
|
||||||
|
|
||||||
|
singletest:
|
||||||
|
@echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v"
|
||||||
|
go test -run $(TEST) $(ARGS)
|
||||||
|
|
||||||
|
cover-report:
|
||||||
|
go test ./... -cover -coverprofile=coverage.out
|
||||||
|
go tool cover -html=coverage.out
|
||||||
|
|
||||||
|
goupdate:
|
||||||
|
go get -t -u=patch ./...
|
||||||
|
|
||||||
|
buildall:
|
||||||
|
./mkrel.sh $(tool) $(VERSION)
|
||||||
|
|
||||||
|
release:
|
||||||
|
gh release create v$(VERSION) --generate-notes
|
||||||
|
|
||||||
|
show-versions: buildlocal
|
||||||
|
@echo "### swaycycle version:"
|
||||||
|
@./swaycycle -V
|
||||||
|
|
||||||
|
@echo
|
||||||
|
@echo "### go module versions:"
|
||||||
|
@go list -m all
|
||||||
|
|
||||||
|
@echo
|
||||||
|
@echo "### go version used for building:"
|
||||||
|
@grep -m 1 go go.mod
|
||||||
|
|
||||||
|
# lint:
|
||||||
|
# golangci-lint run -p bugs -p unused
|
||||||
8
go.mod
Normal file
8
go.mod
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
module swaycycle
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/repr v0.5.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.7 // indirect
|
||||||
|
)
|
||||||
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
|
||||||
|
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
|
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||||
|
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
233
main.go
Normal file
233
main.go
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2025 Thomas von Dein
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/alecthomas/repr"
|
||||||
|
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
root = iota + 1
|
||||||
|
output
|
||||||
|
workspace
|
||||||
|
con
|
||||||
|
floating
|
||||||
|
|
||||||
|
VERSION = "v0.1.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Nodetype string `json:"type"` // output, workspace or container
|
||||||
|
Name string `json:"name"` // workspace number or app name
|
||||||
|
Nodes []Node `json:"nodes"`
|
||||||
|
FloatingNodes []Node `json:"floating_nodes"`
|
||||||
|
Focused bool `json:"focused"`
|
||||||
|
Window int `json:"window"` // wayland native
|
||||||
|
X11Window string `json:"app_id"` // x11 compat
|
||||||
|
Current_workspace string `json:"current_workspace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var Visibles = []Node{}
|
||||||
|
var CurrentWorkspace = ""
|
||||||
|
var Debug = false
|
||||||
|
var Version = false
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.BoolVarP(&Debug, "debug", "d", false, "enable debugging")
|
||||||
|
flag.BoolVarP(&Version, "version", "v", false, "show program version")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if Version {
|
||||||
|
fmt.Printf("This is swaycycle version %s\n", VERSION)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fills Visibles node list
|
||||||
|
fetchSwayTree()
|
||||||
|
|
||||||
|
if len(Visibles) == 0 {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
id := findNextWindow()
|
||||||
|
|
||||||
|
if id > 0 {
|
||||||
|
switchFocus(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the next window after the one with current focus. if the last
|
||||||
|
// one has focus, return the first
|
||||||
|
func findNextWindow() int {
|
||||||
|
if len(Visibles) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
seenfocused := false
|
||||||
|
|
||||||
|
for _, node := range Visibles {
|
||||||
|
if node.Focused {
|
||||||
|
seenfocused = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if seenfocused {
|
||||||
|
return node.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if seenfocused {
|
||||||
|
return Visibles[0].Id
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// actually switch focus using a swaymsg command
|
||||||
|
func switchFocus(id int) {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
arg := fmt.Sprintf("[con_id=%d]", id)
|
||||||
|
|
||||||
|
if Debug {
|
||||||
|
fmt.Printf("executing: swaymsg %s focus\n", arg)
|
||||||
|
}
|
||||||
|
cmd = exec.Command("swaymsg", arg, "focus")
|
||||||
|
|
||||||
|
errbuf := &bytes.Buffer{}
|
||||||
|
cmd.Stderr = errbuf
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to execute swaymsg to switch focus: %s", err)
|
||||||
|
if Debug {
|
||||||
|
fmt.Println(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errbuf.String() != "" {
|
||||||
|
log.Fatalf("swaymsg error: %s", errbuf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute swaymsg to get its internal tree
|
||||||
|
func fetchSwayTree() {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
|
||||||
|
if Debug {
|
||||||
|
fmt.Println("executing: swaymsg -t get_tree -r")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("swaymsg", "-t", "get_tree", "-r")
|
||||||
|
|
||||||
|
errbuf := &bytes.Buffer{}
|
||||||
|
cmd.Stderr = errbuf
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to execute swaymsg to get json tree: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errbuf.String() != "" {
|
||||||
|
log.Fatalf("swaymsg error: %s", errbuf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := processJSON(out); err != nil {
|
||||||
|
log.Fatalf("%s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func istype(nd Node, which int) bool {
|
||||||
|
switch nd.Nodetype {
|
||||||
|
case "root":
|
||||||
|
return which == root
|
||||||
|
case "output":
|
||||||
|
return which == output
|
||||||
|
case "workspace":
|
||||||
|
return which == workspace
|
||||||
|
case "con":
|
||||||
|
return which == con
|
||||||
|
case "floating_con":
|
||||||
|
return which == floating
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// get into the sway tree, determine current workspace and extract all
|
||||||
|
// its visible windows, store them in the global var Visibles
|
||||||
|
func processJSON(jsoncode []byte) error {
|
||||||
|
sway := Node{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(jsoncode, &sway); err != nil {
|
||||||
|
return fmt.Errorf("Failed to unmarshal json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !istype(sway, root) && len(sway.Nodes) == 0 {
|
||||||
|
return fmt.Errorf("Invalid or empty JSON structure")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range sway.Nodes {
|
||||||
|
if node.Current_workspace != "" {
|
||||||
|
// this is an output node containing the current workspace
|
||||||
|
CurrentWorkspace = node.Current_workspace
|
||||||
|
recurseNodes(node.Nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if Debug {
|
||||||
|
repr.Println(Visibles)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate recursively over given node list extracting visible windows
|
||||||
|
func recurseNodes(nodes []Node) {
|
||||||
|
for _, node := range nodes {
|
||||||
|
// we handle nodes and floating_nodes identical
|
||||||
|
node.Nodes = append(node.Nodes, node.FloatingNodes...)
|
||||||
|
|
||||||
|
if istype(node, workspace) {
|
||||||
|
if node.Name == CurrentWorkspace {
|
||||||
|
recurseNodes(node.Nodes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the first nodes seen are workspaces, so if we see a con
|
||||||
|
// node, we are already inside the current workspace
|
||||||
|
if (istype(node, con) || istype(node, floating)) && (node.Window > 0 || node.X11Window != "") {
|
||||||
|
Visibles = append(Visibles, node)
|
||||||
|
} else {
|
||||||
|
recurseNodes(node.Nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user