21 Commits

Author SHA1 Message Date
0a396edb10 moved to codeberg 2025-11-10 20:15:36 +01:00
T. von Dein
9703fd4ad0 migrate to codeberg (#1) 2025-11-10 20:14:32 +01:00
dependabot[bot]
85a1e6530c Bump actions/checkout from 4 to 5 (#5) 2025-09-19 07:56:48 +02:00
feba0f3580 bump version 2025-08-25 09:53:53 +02:00
2fbb3ebc59 add doc about prev flag 2025-08-25 09:53:40 +02:00
T.v.Dein
8e48b42bad enhance window switch debugging (#4) 2025-08-25 09:02:14 +02:00
kkvark
7a5657b778 add --prev option and sort floating_nodes (#3)
* add --prev option
* sort floating_nodes to avoid skipping floating windows
2025-08-25 08:59:21 +02:00
0bdef90f97 swich to i3ipc library 2025-08-15 12:32:33 +02:00
3402df69b4 Merge branch 'main' of github.com:TLINDEN/swaycycle 2025-08-14 20:51:39 +02:00
b21d8ebed9 duplicate error check 2025-08-14 20:51:14 +02:00
Thomas von Dein
a481cc7172 get rid screenshot, doesnt help anyone 2025-08-12 21:39:17 +02:00
Thomas von Dein
cb8421e6f6 cleanup doc 2025-08-12 21:38:24 +02:00
051b68c266 little doc tweaks 2025-08-12 18:34:26 +02:00
bde1301e2c added links 2025-08-12 18:20:18 +02:00
977c374197 get rid of os/exec and talk with sway directly via IPC 2025-08-12 18:20:18 +02:00
4741481527 updated docs 2025-08-12 18:20:18 +02:00
dacdc5c214 add doc about inner workings 2025-08-11 11:38:56 +02:00
1bc1b2a963 added release hint 2025-08-11 09:41:17 +02:00
c567e64772 bump version 2025-08-11 08:49:59 +02:00
cbc061c082 added logging, switched to slog, added screenshot, added release builder 2025-08-11 08:47:41 +02:00
cb9b63f568 enhanced intro 2025-08-10 23:34:43 +02:00
6 changed files with 82 additions and 344 deletions

10
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "monthly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"

View File

@@ -1,87 +0,0 @@
# 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

View File

@@ -1,11 +1,27 @@
[![status-badge](https://ci.codeberg.org/api/badges/15562/status.svg)](https://ci.codeberg.org/repos/15562)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://codeberg.org/scip/swaycycle/raw/branch/main/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/codeberg.org/scip/epuppy)](https://goreportcard.com/report/codeberg.org/scip/swaycycleg)
> [!CAUTION]
> This software is now being maintained on [Codeberg](https://codeberg.org/scip/swaycycle/).
# swaycycle
Cycle through all visible windows on a sway[fx] workspace
Cycle through all visible windows on a sway[fx] workspace including
floating ones or windows in sub-containers. So it simulates the
behavior of other window managers and desktop environments. Just bind
the tool to `ALT-tab` and there you go.
## Installation
Checkout the repo and execute `make`. You'll need the go toolkit. Then
copy the binary `swaycycle` to some location within your `$PATH`.
Download the binary for your architecture from the [release
page](https://codeberg.org/scip/swaycycle/releases) and copy to
some location within your `$PATH`.
To build the tool from source, checkout the repo and execute
`make`. You'll need the go toolkit. Then copy the binary `swaycycle`
to some location within your `$PATH`.
## Configuration
@@ -15,6 +31,51 @@ Add such a line to your sway config file (e.g. in `$HOME/.config/sway/config`):
bindsym $mod+Tab exec ~/bin/swaycycle
```
You may also add a second key binding to do the reverse, which is
sometimes very useful:
```default
bindsym $mod+Shift+Tab exec ~/bin/swaycycle --prev
```
## Debugging
You may call `swaycycle` in a terminal window on a workspace with at
least one another window to test it. Use the option `--debug (-d)` to
get comprehensive debugging output. Add the option `--dump (-D)` to
also get a dump of the sway data tree retrieved by swaycycle. You may
also try `--verbose (-v)` to get a oneliner about the switch.
It's also possible to debug an instance executed by sway using the
`--logfile (-l)` switch, e.g.:
```default
bindsym $mod+Tab exec ~/bin/swaycycle -d -l /tmp/cycle.log
```
## How does it work?
`swaycycle` is being executed by sway when the user presses a key
(e.g. `ALT-tab`). It then connects to the running sway instance via
the provided IPC unix domain socket as available in the environment
variable `SWAYSOCK`. Via that connection it sends the `GET_TREE`
command and processes the retrieved JSON response. This JSON tree
contains all information about the running instance such as outputs,
workspaces and containers.
Then it determines which workspace is the current active one and
builds a list of all windows visible on that workspace, whether
floating or not.
Next it determines which window is following the one in the list with
the current active focus. If the active one is at the end of the list,
it starts from the top.
Finally `swaycycle` sends the propper switch focus command via the IPC
connection to sway, e.g.:
`[con_id=14] focus`
## Getting help
Although I'm happy to hear from swaycycle users in private email, that's the
@@ -22,7 +83,13 @@ best way for me to forget to do something.
In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github:
https://github.com/TLINDEN/swaycycle/issues.
https://codeberg.org/scip/swaycycle/issues.
## See also
- [sway-ipc(7)](https://www.mankier.com/7/sway-ipc)
- [swaywm](https://github.com/swaywm/sway/)
- [swayfx](https://github.com/WillPower3309/swayfx)
## Copyright and license
@@ -34,7 +101,7 @@ T.v.Dein <tom AT vondein DOT org>
## Project homepage
https://github.com/TLINDEN/swaycycle
https://codeberg.org/scip/swaycycle
## Copyright and License

8
go.mod
View File

@@ -1,8 +0,0 @@
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
View File

@@ -1,4 +0,0 @@
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=

240
main.go
View File

@@ -1,240 +0,0 @@
/*
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.2"
)
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
var Notswitch = false
func main() {
flag.BoolVarP(&Debug, "debug", "d", false, "enable debugging")
flag.BoolVarP(&Notswitch, "no-switch", "n", false, "do not switch windows")
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 && !Notswitch {
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)
break
}
}
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
}
// ignore other workspaces
continue
}
// 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)
}
}
}