9 Commits

Author SHA1 Message Date
25d5c571ec update swayipc, go, add linter cfg, satisfy linter 2026-08-22 20:51:17 +02:00
60081f9dce update to go 1.26, update to SwayIPC v2 2026-08-22 20:26:53 +02: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
11 changed files with 376 additions and 134 deletions

View File

@@ -1,87 +0,0 @@
name: build-release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
name: Build Release Assets
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.23.5
- name: Build the executables
run: ./mkrel.sh swaycycle ${{ github.ref_name}}
- name: List the executables
run: ls -l ./releases
- name: Upload the binaries
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
file: ./releases/*
file_glob: true
- name: Build Changelog
id: github_release
uses: mikepenz/release-changelog-builder-action@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
mode: "PR"
configurationJson: |
{
"template": "#{{CHANGELOG}}\n\n**Full Changelog**: #{{RELEASE_DIFF}}",
"pr_template": "- #{{TITLE}} (##{{NUMBER}}) by #{{AUTHOR}}\n#{{BODY}}",
"empty_template": "- no changes",
"categories": [
{
"title": "## New Features",
"labels": ["add", "feature"]
},
{
"title": "## Bug Fixes",
"labels": ["fix", "bug", "revert"]
},
{
"title": "## Documentation Enhancements",
"labels": ["doc"]
},
{
"title": "## Refactoring Efforts",
"labels": ["refactor"]
},
{
"title": "## Miscellaneus Changes",
"labels": []
}
],
"ignore_labels": [
"duplicate", "good first issue", "help wanted", "invalid", "question", "wontfix"
],
"label_extractor": [
{
"pattern": "(.) (.+)",
"target": "$1"
},
{
"pattern": "(.) (.+)",
"target": "$1",
"on_property": "title"
}
]
}
- name: Create Release
uses: softprops/action-gh-release@v2
with:
body: ${{steps.github_release.outputs.changelog}}

104
.golangci.yml Normal file
View File

@@ -0,0 +1,104 @@
version: "2"
linters:
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- clickhouselint
- containedctx
- copyloopvar
- cyclop
- decorder
- dogsled
- dupword
- durationcheck
- embeddedstructfieldcheck
- errchkjson
- errname
- exhaustive
- exptostd
- fatcontext
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoinits
- gochecksumtype
- gocritic
- gocyclo
- godoclint
- goheader
- gomoddirectives
- gomodguard_v2
- goprintffuncname
- gosmopolitan
- grouper
- iface
- importas
- inamedparam
- interfacebloat
- intrange
- iotamixing
- lll
- loggercheck
- makezero
- misspell
- modernize
- nakedret
- nestif
- nilerr
- nilnesserr
- nlreturn
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- promlinter
- protogetter
- reassign
- recvcheck
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- tagalign
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wsl_v5
- zerologlint
settings:
varnamelen:
ignore-names:
- err
- wg
- mu
- ts
- to
- es
- op
- fd
- id
- fn

65
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,65 @@
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
- go mod tidy
gitea_urls:
api: https://codeberg.org/api/v1
download: https://codeberg.org
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- freebsd
archives:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}_{{ .Tag }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: [zip]
- goos: linux
formats: [tar.gz,binary]
files:
- src: "*.md"
strip_parent: true
- src: Makefile.dist
dst: Makefile
wrap_in_directory: true
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
groups:
- title: Improved
regexp: '^.*?(feat|add|new)(\([[:word:]]+\))??!?:.+$'
order: 0
- title: Fixed
regexp: '^.*?(bug|fix)(\([[:word:]]+\))??!?:.+$'
order: 1
- title: Changed
order: 999
release:
header: "# Release Notes"
footer: >-
---
Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/swaycycle/compare/{{ .PreviousTag }}...{{ .Tag }})

38
.woodpecker/build.yaml Normal file
View File

@@ -0,0 +1,38 @@
matrix:
platform:
- linux/amd64
goversion:
- 1.26
lintversion:
- v2.12.2
labels:
platform: ${platform}
steps:
build:
when:
event: [push]
image: golang:${goversion}
commands:
- go get
- go build
linter:
when:
event: [push]
image: golang:${goversion}
commands:
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin ${lintversion}
- golangci-lint --version
- golangci-lint run ./...
depends_on: [build]
test:
when:
event: [push]
image: golang:${goversion}
commands:
- go get
- go test -v -cover
depends_on: [build,linter]

15
.woodpecker/release.yaml Normal file
View File

@@ -0,0 +1,15 @@
# build release
labels:
platform: linux/amd64
steps:
goreleaser:
image: goreleaser/goreleaser
when:
event: [tag]
environment:
GITEA_TOKEN:
from_secret: DEPLOY_TOKEN
commands:
- goreleaser release --clean --verbose

View File

@@ -46,10 +46,7 @@ test: clean
testlint: test lint testlint: test lint
lint: lint:
golangci-lint run golangci-lint run --show-stats=false
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 testfuzzy: clean
go test -fuzz ./... $(ARGS) go test -fuzz ./... $(ARGS)

18
Makefile.dist Normal file
View File

@@ -0,0 +1,18 @@
# -*-make-*-
.PHONY: install all
tool = rpn
PREFIX = /usr/local
UID = root
GID = 0
all:
@echo "Type 'sudo make install' to install the tool."
@echo "To change prefix, type 'sudo make install PREFIX=/opt'"
install:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/share/doc
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 *.md $(PREFIX)/share/doc/

View File

@@ -1,3 +1,7 @@
[![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/swaycycle)
# swaycycle # swaycycle
Cycle through all visible windows on a sway[fx] workspace including Cycle through all visible windows on a sway[fx] workspace including
@@ -9,7 +13,7 @@ the tool to `ALT-tab` and there you go.
## Installation ## Installation
Download the binary for your architecture from the [release Download the binary for your architecture from the [release
page](https://github.com/TLINDEN/swaycycle/releases) and copy to page](https://codeberg.org/scip/swaycycle/releases) and copy to
some location within your `$PATH`. some location within your `$PATH`.
To build the tool from source, checkout the repo and execute To build the tool from source, checkout the repo and execute
@@ -24,6 +28,13 @@ Add such a line to your sway config file (e.g. in `$HOME/.config/sway/config`):
bindsym $mod+Tab exec ~/bin/swaycycle 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 ## Debugging
You may call `swaycycle` in a terminal window on a workspace with at You may call `swaycycle` in a terminal window on a workspace with at
@@ -69,7 +80,7 @@ best way for me to forget to do something.
In order to report a bug, unexpected behavior, feature requests or to In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github: submit a patch, please open an issue on github:
https://github.com/tlinden/swaycycle/issues. https://codeberg.org/scip/swaycycle/issues.
## See also ## See also
@@ -87,7 +98,7 @@ T.v.Dein <tom AT vondein DOT org>
## Project homepage ## Project homepage
https://github.com/tlinden/swaycycle https://codeberg.org/scip/swaycycle
## Copyright and License ## Copyright and License

14
go.mod
View File

@@ -1,18 +1,18 @@
module swaycycle module swaycycle
go 1.23 go 1.26
require ( require (
github.com/lmittmann/tint v1.1.2 codeberg.org/scip/swayipc/v2 v2.1.0
github.com/mattn/go-isatty v0.0.20 github.com/lmittmann/tint v1.1.3
github.com/spf13/pflag v1.0.7 github.com/mattn/go-isatty v0.0.24
github.com/tlinden/swayipc v0.3.0 github.com/spf13/pflag v1.0.10
github.com/tlinden/yadu v0.1.3 github.com/tlinden/yadu v0.1.3
) )
require ( require (
github.com/fatih/color v1.16.0 // indirect github.com/fatih/color v1.16.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.15 // indirect
golang.org/x/sys v0.14.0 // indirect golang.org/x/sys v0.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

14
go.sum
View File

@@ -1,16 +1,24 @@
codeberg.org/scip/swayipc/v2 v2.1.0 h1:tBiOBOUFDOA38+acZwx4BO5Yo/VVwxbqkL+R2/zmW6c=
codeberg.org/scip/swayipc/v2 v2.1.0/go.mod h1:cqicjgMgPS7tGTJgjqjSMvnLeMA+J3to/YnufcysCns=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tlinden/swayipc v0.0.0-20250816175030-177eecd4757f h1:SP/fEurr6crxQI+j85L61rMppsHmOlJwxVzXnCvYJ40= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/tlinden/swayipc v0.0.0-20250816175030-177eecd4757f/go.mod h1:JwlMIC7eBwV8soCt2UDqlAyBudobLo07ZvepIA0irY8= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tlinden/swayipc v0.3.0 h1:hGNWeEZUZIHfeP+MxpAKsUzPf3YSJ0FYX2XEu/yqNXA= github.com/tlinden/swayipc v0.3.0 h1:hGNWeEZUZIHfeP+MxpAKsUzPf3YSJ0FYX2XEu/yqNXA=
github.com/tlinden/swayipc v0.3.0/go.mod h1:JwlMIC7eBwV8soCt2UDqlAyBudobLo07ZvepIA0irY8= github.com/tlinden/swayipc v0.3.0/go.mod h1:JwlMIC7eBwV8soCt2UDqlAyBudobLo07ZvepIA0irY8=
github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY= github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY=
@@ -19,6 +27,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

129
main.go
View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2025 Thomas von Dein Copyright © 2025-2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU General Public License as published by
@@ -18,16 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package main
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
"log/slog" "log/slog"
"os" "os"
"runtime/debug" "runtime/debug"
"sort"
"codeberg.org/scip/swayipc/v2"
"github.com/lmittmann/tint" "github.com/lmittmann/tint"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/tlinden/swayipc"
"github.com/tlinden/yadu" "github.com/tlinden/yadu"
flag "github.com/spf13/pflag" flag "github.com/spf13/pflag"
@@ -42,14 +44,16 @@ const (
LevelNotice = slog.Level(2) LevelNotice = slog.Level(2)
VERSION = "v0.4.0" VERSION = "v0.3.2"
) )
var ( var (
Visibles = []*swayipc.Node{} Visibles = []*swayipc.Node{}
CurrentWorkspace = "" CurrentWorkspace = ""
Previous = false
Debug = false Debug = false
Dumptree = false Dumptree = false
Dumpvisibles = false
Version = false Version = false
Verbose = false Verbose = false
Notswitch = false Notswitch = false
@@ -62,19 +66,22 @@ const Usage string = `This is swaycycle - cycle focus through all visible window
Usage: swaycycle [-vdDn] [-l <log>] Usage: swaycycle [-vdDn] [-l <log>]
Options: Options:
-p, --prev cycle backward
-n, --no-switch do not switch windows -n, --no-switch do not switch windows
-d, --debug enable debugging -d, --debug enable debugging
-D, --dump dump the sway tree (needs -d as well) -D, --dump dump the sway tree (needs -d as well)
--dump-visibles dump a list of visible windows on current workspace (needs -d)
-l, --logfile string write output to logfile -l, --logfile string write output to logfile
-v, --version show program version -v, --version show program version
Copyleft (L) 2025 Thomas von Dein. Copyleft (L) 2025 Thomas von Dein.
Licensed under the terms of the GNU GPL version 3. Licensed under the terms of the GNU GPL version 3.`
`
func main() { func main() {
flag.BoolVarP(&Previous, "prev", "p", false, "cycle backward")
flag.BoolVarP(&Debug, "debug", "d", false, "enable debugging") flag.BoolVarP(&Debug, "debug", "d", false, "enable debugging")
flag.BoolVarP(&Dumptree, "dump", "D", false, "dump the sway tree (needs -d as well)") flag.BoolVarP(&Dumptree, "dump", "D", false, "dump the sway tree (needs -d as well)")
flag.BoolVarP(&Dumpvisibles, "dump-visibles", "", false, "dump the sway tree (needs -d as well)")
flag.BoolVarP(&Notswitch, "no-switch", "n", false, "do not switch windows") flag.BoolVarP(&Notswitch, "no-switch", "n", false, "do not switch windows")
flag.BoolVarP(&Version, "version", "v", false, "show program version") flag.BoolVarP(&Version, "version", "v", false, "show program version")
flag.BoolVarP(&Showhelp, "help", "h", Showhelp, "show help") flag.BoolVarP(&Showhelp, "help", "h", Showhelp, "show help")
@@ -98,48 +105,73 @@ func main() {
if err != nil { if err != nil {
log.Fatalf("Failed to open logfile %s: %s", Logfile, err) log.Fatalf("Failed to open logfile %s: %s", Logfile, err)
} }
defer file.Close() defer func() {
if err := file.Close(); err != nil {
log.Fatalf("failed to close log file: %s", err)
}
}()
setupLogging(file) setupLogging(file)
} else { } else {
setupLogging(os.Stdout) setupLogging(os.Stdout)
} }
if err := cycle(); err != nil {
log.Fatal(err) // nolint:gocritic
}
}
func cycle() error {
// connect to sway unix socket // connect to sway unix socket
ipc := swayipc.NewSwayIPC() ipc := swayipc.NewSwayIPC()
err := ipc.Connect() err := ipc.Connect()
if err != nil { if err != nil {
return err
}
defer func() {
if err := ipc.Close(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer ipc.Close() }()
sway, err := ipc.GetTree() sway, err := ipc.GetTree()
if err != nil { if err != nil {
log.Fatal(err) return err
} }
// traverse the tree and find visible windows // traverse the tree and find visible windows
if err := processJSON(sway); err != nil { if err := processJSON(sway); err != nil {
log.Fatalf("%s", err) return err
} }
if len(Visibles) == 0 { if len(Visibles) == 0 {
os.Exit(0) return nil
} }
id := findNextWindow() var id int
if Previous {
id = findPrevWindow()
slog.Debug("findPrevWindow", "nextid", id)
} else {
id = findNextWindow()
slog.Debug("findNextWindow", "nextid", id) slog.Debug("findNextWindow", "nextid", id)
}
if id > 0 && !Notswitch { if id > 0 && !Notswitch {
switchFocus(id, ipc) if err := switchFocus(id, ipc); err != nil {
return err
} }
} }
return nil
}
// get into the sway tree, determine current workspace and extract all // get into the sway tree, determine current workspace and extract all
// its visible windows, store them in the global var Visibles // its visible windows, store them in the global var Visibles
func processJSON(sway *swayipc.Node) error { func processJSON(sway *swayipc.Node) error {
if !istype(sway, root) && len(sway.Nodes) == 0 { if !istype(sway, root) && len(sway.Nodes) == 0 {
return fmt.Errorf("Invalid or empty JSON structure") return errors.New("invalid or empty JSON structure")
} }
if Dumptree { if Dumptree {
@@ -147,15 +179,18 @@ func processJSON(sway *swayipc.Node) error {
} }
for _, node := range sway.Nodes { for _, node := range sway.Nodes {
if node.Current_workspace != "" { if node.CurrentWorkspace != "" {
// this is an output node containing the current workspace // this is an output node containing the current workspace
CurrentWorkspace = node.Current_workspace CurrentWorkspace = node.CurrentWorkspace
recurseNodes(node.Nodes) recurseNodes(node.Nodes)
break break
} }
} }
slog.Debug("processed visible windows", "visibles", Visibles) if Dumpvisibles {
dumpVisibles()
}
return nil return nil
} }
@@ -172,16 +207,36 @@ func findNextWindow() int {
for _, node := range Visibles { for _, node := range Visibles {
if node.Focused { if node.Focused {
seenfocused = true seenfocused = true
continue continue
} }
if seenfocused { if seenfocused {
return node.Id return node.ID
} }
} }
if seenfocused { if seenfocused {
return Visibles[0].Id return Visibles[0].ID
}
return 0
}
func findPrevWindow() int {
vislen := len(Visibles)
if vislen == 0 {
return 0
}
prevnode := Visibles[vislen-1].ID
for _, node := range Visibles {
if node.Focused {
return prevnode
}
prevnode = node.ID
} }
return 0 return 0
@@ -191,7 +246,7 @@ func findNextWindow() int {
func switchFocus(id int, ipc *swayipc.SwayIPC) error { func switchFocus(id int, ipc *swayipc.SwayIPC) error {
responses, err := ipc.RunContainerCommand(id, "focus") responses, err := ipc.RunContainerCommand(id, "focus")
if err != nil { if err != nil {
log.Fatalf("failed to send focus command to container %d: %w (%s)", return fmt.Errorf("failed to send focus command to container %d: %s (%s)",
id, responses[0].Error, err) id, responses[0].Error, err)
} }
@@ -203,12 +258,18 @@ func switchFocus(id int, ipc *swayipc.SwayIPC) error {
// iterate recursively over given node list extracting visible windows // iterate recursively over given node list extracting visible windows
func recurseNodes(nodes []*swayipc.Node) { func recurseNodes(nodes []*swayipc.Node) {
for _, node := range nodes { for _, node := range nodes {
// we handle nodes and floating_nodes identical
node.Nodes = append(node.Nodes, node.FloatingNodes...)
if istype(node, workspace) { if istype(node, workspace) {
if node.Name == CurrentWorkspace { if node.Name == CurrentWorkspace {
// floating_nodes need to be sorted because
// order changes each time they are focused.
FloatVis := node.FloatingNodes
sort.Slice(FloatVis, func(i, j int) bool {
return FloatVis[i].ID < FloatVis[j].ID
})
// now we can handle nodes and floating_nodes identical
node.Nodes = append(node.Nodes, FloatVis...)
recurseNodes(node.Nodes) recurseNodes(node.Nodes)
return return
} }
@@ -227,6 +288,16 @@ func recurseNodes(nodes []*swayipc.Node) {
} }
} }
func dumpVisibles() {
windows := make([]string, len(Visibles))
for idx, node := range Visibles {
windows[idx] = fmt.Sprintf("id: %02d, focus: %5t, name: %s", node.ID, node.Focused, node.Name)
}
slog.Debug("visible windows on current workspace", "visibles", windows)
}
// we use line wise logging, unless debugging is enabled // we use line wise logging, unless debugging is enabled
func setupLogging(output io.Writer) { func setupLogging(output io.Writer) {
logLevel := &slog.LevelVar{} logLevel := &slog.LevelVar{}
@@ -267,25 +338,25 @@ func setupLogging(output io.Writer) {
} }
} }
// little helper to distinguish sway tree node types // istype is a little helper to distinguish sway tree node types
func istype(nd *swayipc.Node, which int) bool { func istype(nd *swayipc.Node, which int) bool {
switch nd.Type { switch nd.Type {
case "root": case "root":
return which == root return which == swayipc.NodeTypeRoot
case "output": case "output":
return which == output return which == swayipc.NodeTypeOutput
case "workspace": case "workspace":
return which == workspace return which == swayipc.NodeTypeWorkspace
case "con": case "con":
return which == con return which == swayipc.NodeTypeCon
case "floating_con": case "floating_con":
return which == floating return which == swayipc.NodeTypeFloating
} }
return false return false
} }
// returns TRUE if stdout is NOT a tty or windows // IsNoTty returns TRUE if stdout is NOT a tty or windows
func IsNoTty() bool { func IsNoTty() bool {
if !isatty.IsTerminal(os.Stdout.Fd()) { if !isatty.IsTerminal(os.Stdout.Fd()) {
return true return true