diff --git a/.gitignore b/.gitignore
index d699187..3a1e481 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,8 +44,5 @@ go.work
# local dev
/.dev
-/sway-yast
-/test.sh
-/data.txt
-
+/sway-yasm
/dist
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 3d00e79..82e71df 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -1,7 +1,7 @@
builds:
- - id: "sway-yast"
- main: "./cmd/sway-yast/main.go"
- binary: "sway-yast"
+ - id: "sway-yasm"
+ main: "./cmd/sway-yasm/main.go"
+ binary: "sway-yasm"
goos:
- linux
goarch:
@@ -14,6 +14,6 @@ builds:
release:
github:
owner: pancsta
- name: sway-yast
+ name: sway-yasm
draft: true
replace_existing_draft: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ec80f6f..1fcbcb7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,16 @@
-## dev
+## v0.2.0
+- refac: rearrange under pressure
+- feat: add clipboard manager
- feat: add support for user command files
-- fix: correct app ID on startup
-- feat(cmd): add win-to-space
+- fix: correct app IDs on startup
+- feat(usr-cmd): add titlebar-toggle
- feat(usr-cmd): add resize-toggle
- feat(usr-cmd): add arrange
+- feat(cmd): add win-to-space
## v0.1.0
+
- mouse follows focus
- pick space
- pick win
@@ -14,4 +18,5 @@
- live config changes
## v0.0.1
-- initial release
\ No newline at end of file
+
+- initial release
diff --git a/README.md b/README.md
index b8ed9ce..dfdb17d 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,10 @@
-# sway-yast
+# sway-yasm
-Sway **Y**et **A**nother **S**way **T**ab is a text-based window switcher which mimics alt+tab behavior (Most Recently Used order)
-for [Sway WM](https://github.com/swaywm/sway).
+Sway **Y**et **A**nother **S**way **M**anager is a daemon for managing [Sway WM](https://github.com/swaywm/sway) windows, workspaces, outputs, clipboard and PATH using [FZF](https://github.com/junegunn/fzf), both as a floating window and in the terminal.
+
+It tries to deliver all these features in one command, without any configuration, and with a single binary, so it can be deployed easily:
+
+- `sway-yasm daemon --autoconfig --default-keybindings`
| Dark Mode | Light Mode |
@@ -9,39 +12,41 @@ for [Sway WM](https://github.com/swaywm/sway).
| ![Dark mode](./assets/dark.png) | ![Light mode](./assets/light.png) |
```text
-$ sway-yast --help
+$ sway-yasm --help
+
Usage:
- sway-yast [flags]
- sway-yast [command]
+ sway-yasm [flags]
+ sway-yasm [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
config Change the config of a running daemon process
daemon Start tracking focus in sway
- fzf Run fzf with a list of windows
- fzf-path Run fzf with a list of executable files from PATH
- fzf-pick-space Run fzf with a list of workspaces to pick
- fzf-pick-win Run fzf with a list of windows to pick
+ fzf Pure FZF versions of the switcher and pickers
help Help about any command
mru-list Print a list of MRU window IDs
path Show the +x files from PATH using foot
+ pick-clipboard Set the clipboard contents from the history
pick-space Show the workspace picker using foot
pick-win Show the window picker using foot
switcher Show the switcher window using foot
+ usr-cmd Run a user command with a specific name and optional args
+ win-to-space Move the current window to a specific workspace
Flags:
- -h, --help help for sway-yast
+ -h, --help help for sway-yasm
--version Print version and exit
-Use "sway-yast [command] --help" for more information about a command.
+Use "sway-yasm [command] --help" for more information about a command.
```
```text
-$ sway-yast daemon --help
+$ sway-yasm daemon --help
+
Start tracking focus in sway
Usage:
- sway-yast daemon [flags]
+ sway-yasm daemon [flags]
Flags:
--autoconfig Automatic configuration of layout (default true)
@@ -53,29 +58,36 @@ Flags:
## features
+- window / workspace management
+ - alt+tab / MRU order for windows
+ - move a workspace to the current output
+ - move a window to the current workspace
+- miscellaneous management
+ - run anything in your `PATH`
+ - copy from clipboard history using `clipman` and `wl-clipboard`
+- [user command files](#user-command-files) (scripts)
+ - resize-toggle
+ - arrange
+ - titlebar-toggle
- daemon (IPC & RPC) architecture, filesystem-free
-- uses `fzf`, so it works in the terminal
-- renders a floating popup using `foot` (optional)
-- dark mode support (optional)
+- uses `fzf`, so renders in the terminal
+- shows a floating window using `foot`
+- dark mode support
checks `gsettings get org.gnome.desktop.interface color-scheme`
-- 1-hand keystrokes
+- 1-hand keystrokes for window switching
- [mouse follows focus](#mouse-follows-focus) mode (optional)
-- additional features (popups)
- - move a workspace to the current output
- - move a window to the current workspace
- - run anything in your `PATH`
-- general MRU watcher via `mru-list`
+- plain MRU list via `mru-list` for integrations
## usage
1. Install using either
- - binary from [the releases page](https://github.com/pancsta/sway-yast/releases/latest)
- - `go install github.com/pancsta/sway-yast@latest`
+ - binary from [the releases page](https://github.com/pancsta/sway-yasm/releases/latest)
+ - `go install github.com/pancsta/sway-yasm@latest`
- `git clone && go mod tidy && go build`
2. Start the daemon
- `sway-yast daemon --default-keystrokes`
+ `sway-yasm daemon --default-keystrokes`
3. Use directly in the terminal (optional)
- `sway-yast fzf`
+ `sway-yasm fzf`
4. Press `alt+tab`
## keystrokes
@@ -96,7 +108,7 @@ Switcher mode:
- `ctrl+c` close the switcher
- `a-z`, `0-9` fuzzy search
-Example - switch to the 3nd MRU window:
+Example - switch to the 3rd MRU window:
- `alt+tab`
- `tab`
@@ -113,32 +125,58 @@ Example - switch to Krusader by name:
Various ways to get the default keybindings.
```bash
-$ sway-yast daemon --default-keybindings
+$ sway-yasm daemon --default-keybindings
```
```bash
# shell
-swaymsg bindsym alt+tab exec sway-yast switcher
-swaymsg bindsym mod4+o exec sway-yast pick-space
-swaymsg bindsym mod4+p exec sway-yast pick-win
-swaymsg bindsym mod4+d exec sway-yast path
+swaymsg bindsym alt+tab exec sway-yasm switcher
+swaymsg bindsym mod4+o exec sway-yasm pick-space
+swaymsg bindsym mod4+p exec sway-yasm pick-win
+swaymsg bindsym mod4+d exec sway-yasm path
+swaymsg bbindsym mod4+alt+c exec sway-yasm clipboard
```
```text
# config
-bindsym alt+tab exec sway-yast switcher
-bindsym $mod+o exec sway-yast pick-space
-bindsym $mod+p exec sway-yast pick-win
-bindsym $mod+d exec sway-yast path
+bindsym alt+tab exec sway-yasm switcher
+bindsym $mod+o exec sway-yasm pick-space
+bindsym $mod+p exec sway-yasm pick-win
+bindsym $mod+d exec sway-yasm path
+bindsym $mod+alt+c exec sway-yasm clipboard
+```
+
+### simulate blur events
+
+```text
+# passes `container move to workspace number` via sway-yasm
+
+bindsym $mod+Control+1 exec sway-yasm win-to-space 1
+bindsym $mod+Control+2 exec sway-yasm win-to-space 2
+bindsym $mod+Control+3 exec sway-yasm win-to-space 3
+bindsym $mod+Control+4 exec sway-yasm win-to-space 4
+bindsym $mod+Control+5 exec sway-yasm win-to-space 5
+bindsym $mod+Control+6 exec sway-yasm win-to-space 6
+bindsym $mod+Control+7 exec sway-yasm win-to-space 7
+bindsym $mod+Control+8 exec sway-yasm win-to-space 8
+bindsym $mod+Control+9 exec sway-yasm win-to-space 9
+bindsym $mod+Control+0 exec sway-yasm win-to-space 10
```
## mouse follows focus
```bash
-$ sway-yast daemon --mouse-follows-focus
+$ sway-yasm daemon --mouse-follows-focus
```
-Using `input map_to_output`, traps the relative cursor inside the currently focused output. Changing focus moves the cursor between outputs (thus the name). Useful for VNC screens on separate machines. When combined with [waycorner](https://github.com/AndreasBackx/waycorner), it creates a synergy-like effect.
+Using `input map_to_output`, the daemon traps the relative cursor inside the currently focused output. Changing focus moves the cursor between outputs (thus the name). Useful for VNC screens on separate machines. When combined with [waycorner](https://github.com/AndreasBackx/waycorner), it creates a synergy-like effect.
+
+Turning on/off:
+
+```bash
+$ sway-yasm config --mouse-follows-focus=false
+$ sway-yasm config --mouse-follows-focus=true
+```
### waycorner config example
@@ -160,7 +198,7 @@ description = ".*output 2.*"
## configuration
-See the [top config section in internal/pkg/daemon.go](internal/pkg/daemon.go), modify and `go build cmd/sway-yast/*.go`.
+There is a [top config section in daemon.go](internal/daemon/daemon.go) - modify and `./scripts/build.sh`. YAML config [is planned](#todo)...
## additional features (popups)
@@ -178,13 +216,64 @@ See the [top config section in internal/pkg/daemon.go](internal/pkg/daemon.go),
## troubleshooting
-`env YAST_LOG=1 sway-yast`
+`env YASM_LOG=1 sway-yasm`
## development
-- `go build cmd/sway-yast/main.go`
-- `env YAST_LOG=1 YAST_DEBUG=1 ./main deamon`
-- `env YAST_LOG=1 YAST_DEBUG=1 ./main switcher`
+- `./scripts/build.sh`
+- `env YASM_LOG=1 YASM_DEBUG=1 ./sway-yasm deamon`
+- `env YASM_LOG=1 YASM_DEBUG=1 ./sway-yasm switcher`
+
+## user command files
+
+User command files provide a simple way to **script sway using Go within the daemon**, and can be fairly easily exchanged with others.
+
+- [resize-toggle](pkg/usr-cmds/resize-toggle.go)
+ - `sway-yasm usr-cmd resize-toggle`
+ - resizes a split to 10/50/90%
+- [arrange](pkg/usr-cmds/arrange.go)
+ - `sway-yasm usr-cmd arrange`
+ - arranges windows to workspaces
+- [titlebar-toggle](pkg/usr-cmds/titlebar-toggle.go)
+ - `sway-yasm usr-cmd titlebar-toggle`
+ - shows/hides window's titlebar
+
+Installing a user command file:
+
+```shell
+cp my-cmd.go pkg/usr-cmds
+./scripts/build.sh
+# run
+./sway-yasm deamon
+./sway-yasm usr-cmd my-cmd 123 -- -a --b=c
+```
+
+Modifying a user command file:
+
+See [pkg/usr-cmds/api.go](pkg/usr-cmds/api.go) for the API and [pkg/usr-cmds/template.go](pkg/usr-cmds/template.go) for a sample usage.
+
+```shell
+nano pkg/usr-cmds/arrange.go
+./scripts/build.sh
+# run
+./sway-yasm deamon
+./sway-yasm usr-cmd arrange
+```
+
+## todo
+
+- config file
+- user scripts in wasm
+- underscore windows from the current workspace
+- show on all screens (via wayland)
+- group fzf cmds under `sway-yasm fzf`
+- pick grouping containers with `pick-container`
+- `switcher --current-output-only`
+- `switcher --current-space-only`
+- `switcher --group-by-output`
+- tests (wink wink)
+- themes
+- reconnect logic
## changelog
@@ -195,3 +284,5 @@ See [CHANGELOG.md](CHANGELOG.md).
- [applist.py](https://github.com/davxy/dotfiles/blob/main/_old/sway/applist.py)
- [sway-fzfify](https://github.com/ldelossa/sway-fzfify)
- [Difrex/gosway](https://github.com/Difrex/gosway)
+- [fzf](https://github.com/junegunn/fzf)
+- [contexts](https://contexts.co/)
diff --git a/assets/grafana-dashboard-dark.png b/assets/grafana-dashboard-dark.png
deleted file mode 100644
index 4b3056c..0000000
Binary files a/assets/grafana-dashboard-dark.png and /dev/null differ
diff --git a/assets/grafana-dashboard-light.png b/assets/grafana-dashboard-light.png
deleted file mode 100644
index a1d2e4f..0000000
Binary files a/assets/grafana-dashboard-light.png and /dev/null differ
diff --git a/cmd/sway-yasm/main.go b/cmd/sway-yasm/main.go
new file mode 100644
index 0000000..6c15c2e
--- /dev/null
+++ b/cmd/sway-yasm/main.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+ "io"
+ "log"
+ "os"
+
+ "github.com/pancsta/sway-yasm/internal/cmds"
+)
+
+func main() {
+ // TODO --status (PID, config, windows count)
+ if os.Getenv("YASM_LOG") == "" {
+ log.SetOutput(io.Discard)
+ }
+ // TODO slog
+ logger := log.New(os.Stdout, "", 0)
+
+ err := cmds.GetRootCmd(logger).Execute()
+ if err != nil {
+ logger.Fatal("cobra error:", err)
+ }
+}
diff --git a/cmd/sway-yast/main.go b/cmd/sway-yast/main.go
deleted file mode 100644
index ce5ebe7..0000000
--- a/cmd/sway-yast/main.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package main
-
-import (
- "io"
- "log"
- "os"
-
- "github.com/pancsta/sway-yast/internal/cmds"
- "github.com/spf13/cobra"
- "os/signal"
- "syscall"
-)
-
-func main() {
- // TODO --status (PID, config, windows count)
- // TODO readme, screenshots
- // TODO auto bind default shortcuts via --bind-default-keys
- // alt+tab, cmds+o, cmds+p
- // TODO include desktop shortcuts in "path" (Name, Exe)
- // - ~/.local/share/applications
- // - /usr/share/applications
- if os.Getenv("YAST_LOG") == "" {
- log.SetOutput(io.Discard)
- }
- out := log.New(os.Stdout, "", 0)
-
- cmdList := cmds.GetCmds(out)
-
- var rootCmd = &cobra.Command{
- Use: "sway-yast",
- Run: cmds.CmdRoot,
- }
- rootCmd.AddCommand(cmdList...)
- rootCmd.Flags().Bool("version", false,
- "Print version and exit")
-
- err := rootCmd.Execute()
- if err != nil {
- out.Fatal("cobra error:", err)
- }
-}
-
-// TODO
-func waitForExit() {
- // Create a channel to receive OS signals.
- sigs := make(chan os.Signal, 1)
-
- // `signal.Notify` makes `os.Signal` send OS signals to the channel.
- // If no signals are provided, all incoming signals will be relayed to the channel.
- // Otherwise, just the provided signals will.
- signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
-
- // The program will wait here until it gets an OS signal, such as SIGINT or SIGTERM,
- // and then it will exit.
- <-sigs
-}
diff --git a/go.mod b/go.mod
index d29f549..a22e0c9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/pancsta/sway-yast
+module github.com/pancsta/sway-yasm
go 1.22.3
@@ -8,6 +8,7 @@ replace github.com/Difrex/gosway/ipc => github.com/pancsta/gosway/ipc v0.0.0-202
require (
github.com/Difrex/gosway/ipc v0.0.0-20240312143858-20214f4c38d6
github.com/fsnotify/fsnotify v1.7.0
+ github.com/lithammer/dedent v1.1.0
github.com/pancsta/asyncmachine-go v0.6.1
github.com/samber/lo v1.39.0
github.com/spf13/cobra v1.8.0
diff --git a/internal/cmds/cmds.go b/internal/cmds/cmds.go
index 519aafe..0b9f15e 100644
--- a/internal/cmds/cmds.go
+++ b/internal/cmds/cmds.go
@@ -10,94 +10,38 @@ import (
"strconv"
"strings"
- "github.com/pancsta/sway-yast/internal/daemon"
+ "github.com/lithammer/dedent"
+ "github.com/pancsta/sway-yasm/internal/daemon"
"github.com/spf13/cobra"
"runtime/debug"
)
-// TODO gen fzf.yml for user overrides
-
-const (
- shellFzf = `
- fzf \
- --prompt 'Switcher: ' \
- --bind "load:pos(2)" \
- --bind "change:pos(1)" \
- --layout=reverse --info=hidden \
- --bind=space:accept,tab:offset-down,btab:offset-up
-`
- shellFzfPickWin = `
- fzf \
- --prompt 'Move which window to this workspace?: ' \
- --layout=reverse --info=hidden \
- --bind=space:accept,tab:offset-down,btab:offset-up
-`
- shellFzfPickSpace = `
- fzf \
- --prompt 'Move which workspace to this output?: ' \
- --layout=reverse --info=hidden \
- --bind=space:accept,tab:offset-down,btab:offset-up
-`
- shellFzfPath = `
- fzf \
- --prompt 'Run: ' \
- --layout=reverse --info=hidden \
- --bind=space:accept,tab:offset-down,btab:offset-up
-`
- // junegunn/seoul256.vim (light)
- shellFzfLight = ` \
- --color=bg+:#D9D9D9,bg:#E1E1E1,border:#C8C8C8,spinner:#719899,hl:#719872,fg:#616161,header:#719872,info:#727100,pointer:#E12672,marker:#E17899,fg+:#616161,preview-bg:#D9D9D9,prompt:#0099BD,hl+:#719899
-`
- shellSwitcher = `
- foot --title "sway-yast" sway-yast fzf
-`
- shellPickWin = `
- foot --title "sway-yast" sway-yast fzf-pick-win
-`
- shellPickSpace = `
- foot --title "sway-yast" sway-yast fzf-pick-space
-`
- shellPath = `
- foot --title "sway-yast" sway-yast fzf-path
-`
-)
+var clipboardSanitize = regexp.MustCompile(`\s+`)
// ///// ///// /////
// ///// COBRAS
// ///// ///// /////
-func GetCmds(out *log.Logger) []*cobra.Command {
- var list []*cobra.Command
+func mouseFollowsFocusFlag(cmd *cobra.Command) {
+ cmd.Flags().Bool("mouse-follows-focus", false,
+ "Calls 'input ... map_to_output OUTPUT' on each focus")
+}
+
+func GetRootCmd(logger *log.Logger) *cobra.Command {
cmdDaemon := &cobra.Command{
Use: "daemon",
Short: "Start tracking focus in sway",
- Run: func(cmd *cobra.Command, args []string) {
- mouseFollow, _ := cmd.Flags().GetBool("mouse-follows-focus")
- autoconfig, _ := cmd.Flags().GetBool("autoconfig")
- defaultKeybindings, _ := cmd.Flags().GetBool("default-keybindings")
- d := &daemon.Daemon{
- MouseFollowsFocus: mouseFollow,
- Autoconfig: autoconfig,
- DefaultKeybindings: defaultKeybindings,
- Out: out,
- }
- if mouseFollow {
- d.Out.Println("Mouse follows focus enabled")
- }
- d.Start()
- },
+ Run: cmdDaemon(logger),
}
- // TODO extract
- cmdDaemon.Flags().Bool("mouse-follows-focus", false,
- "Calls 'input ... map_to_output OUTPUT' on each focus")
+ mouseFollowsFocusFlag(cmdDaemon)
cmdDaemon.Flags().Bool("autoconfig", true,
- "Automatic configuration of layout")
+ "Automatically configure the layout and start clipman")
cmdDaemon.Flags().Bool("default-keybindings", false,
"Add default keybindings")
- cmdList := &cobra.Command{
+ cmdMRUList := &cobra.Command{
Use: "mru-list",
Short: "Print a list of MRU window IDs",
Run: func(cmd *cobra.Command, args []string) {
@@ -105,42 +49,61 @@ func GetCmds(out *log.Logger) []*cobra.Command {
},
}
- cmdFzf := &cobra.Command{
- Use: "fzf",
+ cmdFzfSwitcher := &cobra.Command{
+ Use: "switcher",
Short: "Run fzf with a list of windows",
- Run: CmdFzf,
+ Run: CmdFzfSwitcher,
}
cmdFzfPickWin := &cobra.Command{
- Use: "fzf-pick-win",
+ Use: "pick-win",
Short: "Run fzf with a list of windows to pick",
Run: CmdFzfPickWin,
}
cmdFzfPickSpace := &cobra.Command{
- Use: "fzf-pick-space",
+ Use: "pick-space",
Short: "Run fzf with a list of workspaces to pick",
Run: CmdFzfPickSpace,
}
cmdFzfPath := &cobra.Command{
- Use: "fzf-path",
+ Use: "path",
Short: "Run fzf with a list of executable files from PATH",
- Run: CmdFzfPath,
+ Long: "Run fzf with a list of executable files from PATH, with all the " +
+ "dirs being watched for changes.",
+ Run: CmdFzfPath,
+ }
+
+ cmdFzfPickClip := &cobra.Command{
+ Use: "clipboard",
+ Short: "Run fzf with your clipboard history and copy the selection",
+ Run: CmdFzfClipboard,
+ }
+
+ cmdFzf := &cobra.Command{
+ Use: "fzf",
+ Short: "Pure FZF versions of the switcher and pickers",
+ Long: "Pure FZF versions of the switcher and pickers, which allows them " +
+ "to be rendered directly in the terminal.",
}
+ cmdFzf.AddCommand(cmdFzfSwitcher, cmdFzfPickWin, cmdFzfPickSpace, cmdFzfPath, cmdFzfPickClip)
+
cmdUserCmd := &cobra.Command{
Use: "usr-cmd",
Short: "Run a user command with a specific name and optional args",
- Example: "sway-yast usr-cmd resize-toggle -- -f=1",
+ Example: "sway-yasm usr-cmd resize-toggle -- -f=1",
Run: CmdUsrCmd,
Args: cobra.ExactArgs(1),
}
cmdSwitcher := &cobra.Command{
Use: "switcher",
- Short: "Show the switcher window using foot",
- Run: CmdSwitcher,
+ Short: "Show the window switcher window using foot",
+ Long: "Show the window switcher window using foot in the Most Recently " +
+ "Used order. The list can be traversed by pressing Tab or arrows.",
+ Run: CmdSwitcher,
}
cmdPickWin := &cobra.Command{
@@ -158,7 +121,9 @@ func GetCmds(out *log.Logger) []*cobra.Command {
cmdPath := &cobra.Command{
Use: "path",
Short: "Show the +x files from PATH using foot",
- Run: CmdPath,
+ Long: "Show the +x files from PATH using foot, with all the dirs being " +
+ "watched for changes.",
+ Run: CmdPath,
}
cmdWinToSpace := &cobra.Command{
@@ -173,15 +138,42 @@ func GetCmds(out *log.Logger) []*cobra.Command {
Short: "Change the config of a running daemon process",
Run: CmdConfig,
}
- // TODO extract
- cmdConfig.Flags().Bool("mouse-follows-focus", false,
- "Calls 'input ... map_to_output OUTPUT' on each focus")
+ mouseFollowsFocusFlag(cmdConfig)
- list = append(list, cmdDaemon, cmdList, cmdFzf, cmdSwitcher, cmdFzfPickWin,
- cmdPickWin, cmdConfig, cmdFzfPickSpace, cmdPickSpace, cmdPath, cmdFzfPath,
- cmdUserCmd, cmdWinToSpace)
+ cmdClipboard := &cobra.Command{
+ Use: "clipboard",
+ Short: "Set the clipboard contents from the history",
+ Run: CmdClipboard,
+ }
+
+ var rootCmd = &cobra.Command{
+ Use: "sway-yasm",
+ Run: CmdRoot,
+ }
+ rootCmd.AddCommand(cmdDaemon, cmdMRUList, cmdSwitcher, cmdPickWin, cmdConfig,
+ cmdPickSpace, cmdPath, cmdUserCmd, cmdWinToSpace, cmdClipboard, cmdFzf)
+ rootCmd.Flags().Bool("version", false,
+ "Print version and exit")
- return list
+ return rootCmd
+}
+
+func cmdDaemon(logger *log.Logger) func(cmd *cobra.Command, args []string) {
+ return func(cmd *cobra.Command, args []string) {
+ mouseFollow, _ := cmd.Flags().GetBool("mouse-follows-focus")
+ autoconfig, _ := cmd.Flags().GetBool("autoconfig")
+ defaultKeybindings, _ := cmd.Flags().GetBool("default-keybindings")
+ d := &daemon.Daemon{
+ MouseFollowsFocus: mouseFollow,
+ Autoconfig: autoconfig,
+ DefaultKeybindings: defaultKeybindings,
+ Logger: logger,
+ }
+ if mouseFollow {
+ d.Logger.Println("Mouse follows focus enabled")
+ }
+ d.Start()
+ }
}
// ///// ///// /////
@@ -231,103 +223,14 @@ func CmdPath(_ *cobra.Command, _ []string) {
}
}
-// ///// ///// /////
-// ///// FZF COMMANDS
-// ///// ///// /////
-
-func CmdFzf(_ *cobra.Command, _ []string) {
- // req the daemon
- input, err := daemon.RemoteCall("Daemon.RemoteFZFList", daemon.RPCArgs{})
- if err != nil {
- log.Fatalf("rpc error: %s", err)
- }
-
- // run fzf
- result, err := fzf(shellFzf, &input)
- if err != nil {
- log.Fatalf("fzf error: %s", err)
- }
-
- // match the window's ID at the end of the line
- winID, err := matchWinID(result)
- if err != nil {
- log.Fatalf("error: %s", err)
- }
-
- // focus the window
- _, err = daemon.RemoteCall("Daemon.RemoteFocusWinID", daemon.RPCArgs{WinID: winID})
- if err != nil {
- log.Fatalf("rpc error: %s", err)
- }
-}
-
-func CmdFzfPickWin(_ *cobra.Command, _ []string) {
- // req the daemon
- input, err := daemon.RemoteCall("Daemon.RemoteFZFListPickWin", daemon.RPCArgs{})
- if err != nil {
- log.Fatalf("rpc error: %s", err)
- }
- // run fzf
- result, err := fzf(shellFzfPickWin, &input)
- if err != nil {
- log.Fatalf("fzf error: %s", err)
- }
-
- // match the window's ID at the end of the line
- winID, err := matchWinID(result)
- if err != nil {
- log.Fatalf("error: %s", err)
- }
-
- // move the window to the current workspace
- _, err = daemon.RemoteCall("Daemon.RemoteMoveWinToSpace", daemon.RPCArgs{WinID: winID})
- if err != nil {
- log.Fatalf("rpc error: %s", err)
- }
-}
-
-func CmdFzfPickSpace(_ *cobra.Command, _ []string) {
- // req the daemon
- list, err := daemon.RemoteCall("Daemon.RemoteFZFListPickSpace", daemon.RPCArgs{})
- if err != nil {
- log.Fatalf("rpc error: %s", err)
- }
-
- // run fzf to pick the workspace
- result, err := fzf(shellFzfPickSpace, &list)
- if err != nil {
- log.Fatalf("fzf error: %s", err)
- }
-
- // move the workspace to the current output
- _, err = daemon.RemoteCall("Daemon.RemoteMoveSpaceToOutput", daemon.RPCArgs{
- Workspace: strings.Trim(result, " \n"),
- })
- if err != nil {
- log.Fatalf("rpc error: %s", err)
- }
-}
-
-func CmdFzfPath(_ *cobra.Command, _ []string) {
- // req the daemon
- list, err := daemon.RemoteCall("Daemon.RemoteGetPathFiles", daemon.RPCArgs{})
- if err != nil {
- log.Fatalf("rpc error: %s", err)
+func CmdClipboard(_ *cobra.Command, _ []string) {
+ if !shouldOpen() {
+ log.Fatal("fzf error: already open")
}
- // run fzf
- result, err := fzf(shellFzfPath, &list)
+ _, err := run(shellClipboard)
if err != nil {
- log.Fatalf("fzf error: %s", err)
- }
-
- // return the picked exe
- log.Printf("path: %s", result)
- result, err = daemon.RemoteCall("Daemon.RemoteExec", daemon.RPCArgs{
- ExePath: result,
- })
- if err != nil {
- log.Fatalf("error: cant run %s", result)
+ log.Fatalf("foot error: %s", err)
}
}
@@ -346,8 +249,17 @@ func CmdRoot(cmd *cobra.Command, _ []string) {
fmt.Println(build.Main.Version)
os.Exit(0)
} else {
-
- fmt.Println("Yet Another Sway Tab\n\nUsage:\n$ sway-yast daemon\n$ sway-yast --help")
+ fmt.Println(fmt.Sprintf(dedent.Dedent(strings.Trim(`
+ sway-yasm: SWAY Yet Another Sway Manager
+
+ Daemon for managing Sway WM windows, workspaces, outputs, clipboard and PATH
+ using FZF, both as a floating window and in the terminal.
+
+ Usage:
+
+ $ sway-yasm daemon --autoconfig --default-keybindings
+ $ sway-yasm switcher
+ $ sway-yasm help`, " \n"))))
}
}
@@ -395,7 +307,8 @@ func CmdConfig(cmd *cobra.Command, _ []string) {
if result != "" {
log.Fatal("config error")
}
- fmt.Println("Config updated")
+ fmt.Println("Config updated:")
+ fmt.Printf("- mouse follows focus: %t\n", mouseFollow)
// TODO print out the current config (as yaml)
}
@@ -403,12 +316,21 @@ func CmdConfig(cmd *cobra.Command, _ []string) {
// ///// HELPERS
// ///// ///// /////
-// TODO docs
-func matchWinID(result string) (int, error) {
+func matchSuffixID(result string) (int, error) {
re := regexp.MustCompile(`\((\d+)\)\s*$`)
match := re.FindStringSubmatch(result)
if len(match) == 0 {
- return 0, fmt.Errorf("no winID match")
+ return 0, fmt.Errorf("no (ID) match")
+ }
+
+ return strconv.Atoi(match[1])
+}
+
+func matchPrefixID(result string) (int, error) {
+ re := regexp.MustCompile(`^\s*\((\d+)\)`)
+ match := re.FindStringSubmatch(result)
+ if len(match) == 0 {
+ return 0, fmt.Errorf("no (ID) match")
}
return strconv.Atoi(match[1])
@@ -425,7 +347,7 @@ func shouldOpen() bool {
return shouldOpen == "true"
}
-func fzf(cmd string, input *string) (string, error) {
+func runFZF(cmd string, input *string) (string, error) {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
@@ -454,5 +376,6 @@ func run(cmd string) (string, error) {
shell = "sh"
}
out, err := exec.Command(shell, "-c", cmd).Output()
+
return string(out), err
}
diff --git a/internal/cmds/fzf.go b/internal/cmds/fzf.go
new file mode 100644
index 0000000..fd20370
--- /dev/null
+++ b/internal/cmds/fzf.go
@@ -0,0 +1,224 @@
+package cmds
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/pancsta/sway-yasm/internal/daemon"
+ "github.com/spf13/cobra"
+ "log"
+ "os"
+ "os/exec"
+ "slices"
+ "strings"
+)
+
+// TODO gen fzf.yml for user overrides
+
+const (
+ shellFzf = `
+ fzf \
+ --prompt 'Switcher: ' \
+ --bind "load:pos(2)" \
+ --bind "change:pos(1)" \
+ --layout=reverse --info=hidden \
+ --bind=space:accept,tab:offset-down,btab:offset-up
+`
+ shellFzfPickWin = `
+ fzf \
+ --prompt 'Move which window to this workspace?: ' \
+ --layout=reverse --info=hidden \
+ --bind=space:accept,tab:offset-down,btab:offset-up
+`
+ shellFzfClipboard = `
+ fzf \
+ --prompt 'Copy which one to the clipboard?: ' \
+ --layout=reverse --info=hidden \
+ --bind=space:accept,tab:offset-down,btab:offset-up
+`
+ shellFzfPickSpace = `
+ fzf \
+ --prompt 'Move which workspace to this output?: ' \
+ --layout=reverse --info=hidden \
+ --bind=space:accept,tab:offset-down,btab:offset-up
+`
+ shellFzfPath = `
+ fzf \
+ --prompt 'Run: ' \
+ --layout=reverse --info=hidden \
+ --bind=space:accept,tab:offset-down,btab:offset-up
+`
+ // junegunn/seoul256.vim (light)
+ shellFzfLight = ` \
+ --color=bg+:#D9D9D9,bg:#E1E1E1,border:#C8C8C8,spinner:#719899,hl:#719872,fg:#616161,header:#719872,info:#727100,pointer:#E12672,marker:#E17899,fg+:#616161,preview-bg:#D9D9D9,prompt:#0099BD,hl+:#719899
+`
+ shellSwitcher = `
+ foot --title "sway-yasm" sway-yasm fzf switcher
+`
+ shellPickWin = `
+ foot --title "sway-yasm" sway-yasm fzf pick-win
+`
+ shellPickSpace = `
+ foot --title "sway-yasm" sway-yasm fzf pick-space
+`
+ shellPath = `
+ foot --title "sway-yasm" sway-yasm fzf path
+`
+ shellClipboard = `
+ foot --title "sway-yasm" sway-yasm fzf clipboard
+`
+)
+
+// ///// ///// /////
+// ///// FZF COMMANDS
+// ///// ///// /////
+
+func CmdFzfSwitcher(_ *cobra.Command, _ []string) {
+ // req the daemon
+ input, err := daemon.RemoteCall("Daemon.RemoteFZFList", daemon.RPCArgs{})
+ if err != nil {
+ log.Fatalf("rpc error: %s", err)
+ }
+
+ // run fzf
+ result, err := runFZF(shellFzf, &input)
+ if err != nil {
+ log.Fatalf("fzf error: %s", err)
+ }
+
+ // match the window's ID at the end of the line
+ winID, err := matchSuffixID(result)
+ if err != nil {
+ log.Fatalf("error: %s", err)
+ }
+
+ // focus the window
+ _, err = daemon.RemoteCall("Daemon.RemoteFocusWinID", daemon.RPCArgs{WinID: winID})
+ if err != nil {
+ log.Fatalf("rpc error: %s", err)
+ }
+}
+
+func CmdFzfPickWin(_ *cobra.Command, _ []string) {
+ // req the daemon
+ input, err := daemon.RemoteCall("Daemon.RemoteFZFListPickWin", daemon.RPCArgs{})
+ if err != nil {
+ log.Fatalf("rpc error: %s", err)
+ }
+ // run fzf
+ result, err := runFZF(shellFzfPickWin, &input)
+ if err != nil {
+ log.Fatalf("fzf error: %s", err)
+ }
+
+ // match the window's ID at the end of the line
+ winID, err := matchSuffixID(result)
+ if err != nil {
+ log.Fatalf("error: %s", err)
+ }
+
+ // move the window to the current workspace
+ _, err = daemon.RemoteCall("Daemon.RemoteMoveWinToSpace", daemon.RPCArgs{WinID: winID})
+ if err != nil {
+ log.Fatalf("rpc error: %s", err)
+ }
+}
+
+func CmdFzfPickSpace(_ *cobra.Command, _ []string) {
+ // req the daemon
+ list, err := daemon.RemoteCall("Daemon.RemoteFZFListPickSpace", daemon.RPCArgs{})
+ if err != nil {
+ log.Fatalf("rpc error: %s", err)
+ }
+
+ // run fzf to pick the workspace
+ result, err := runFZF(shellFzfPickSpace, &list)
+ if err != nil {
+ log.Fatalf("fzf error: %s", err)
+ }
+
+ // move the workspace to the current output
+ _, err = daemon.RemoteCall("Daemon.RemoteMoveSpaceToOutput", daemon.RPCArgs{
+ Workspace: strings.Trim(result, " \n"),
+ })
+ if err != nil {
+ log.Fatalf("rpc error: %s", err)
+ }
+}
+
+func CmdFzfClipboard(_ *cobra.Command, _ []string) {
+ shell := os.Getenv("SHELL")
+ if len(shell) == 0 {
+ shell = "sh"
+ }
+ cmdClipman := "clipman show-history"
+ if daemon.IsLightMode() {
+ cmdClipman = strings.TrimRight(cmdClipman, " \n") + shellFzfLight
+ }
+
+ // get json
+ histJSON, err := exec.Command(shell, "-c", cmdClipman).Output()
+ if err != nil {
+ log.Fatalf("clipman error: %s", err)
+ }
+
+ // parse json
+ var hist []string
+ err = json.Unmarshal(histJSON, &hist)
+ slices.Reverse(hist)
+ if err != nil {
+ log.Fatalf("json error: %s", err)
+ }
+
+ // prep fzf input
+ fzfInput := ""
+ for i, h := range hist {
+ clean := strings.Trim(clipboardSanitize.ReplaceAllString(h, " "), " ")
+ if clean == "" {
+ continue
+ }
+
+ fzfInput += fmt.Sprintf("(%d) %s\n", i, clean)
+ }
+
+ // run fzf
+ result, err := runFZF(shellFzfClipboard, &fzfInput)
+ if err != nil {
+ log.Fatalf("fzf error: %s", err)
+ }
+ // match the entry's ID at the end of the line
+ id, err := matchPrefixID(result)
+ if err != nil {
+ log.Fatalf("error: %s", err)
+ }
+
+ // set the clipboard
+ _, err = daemon.RemoteCall("Daemon.RemoteCopy", daemon.RPCArgs{
+ Clipboard: hist[id],
+ })
+ if err != nil {
+ log.Fatalf("rpc error: %s", err)
+ }
+}
+
+func CmdFzfPath(_ *cobra.Command, _ []string) {
+ // req the daemon
+ list, err := daemon.RemoteCall("Daemon.RemoteGetPathFiles", daemon.RPCArgs{})
+ if err != nil {
+ log.Fatalf("rpc error: %s", err)
+ }
+
+ // run fzf
+ result, err := runFZF(shellFzfPath, &list)
+ if err != nil {
+ log.Fatalf("fzf error: %s", err)
+ }
+
+ // return the picked exe
+ log.Printf("path: %s", result)
+ result, err = daemon.RemoteCall("Daemon.RemoteExec", daemon.RPCArgs{
+ ExePath: result,
+ })
+ if err != nil {
+ log.Fatalf("error: cant run %s", result)
+ }
+}
diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go
index c02e90a..c3230d3 100644
--- a/internal/daemon/daemon.go
+++ b/internal/daemon/daemon.go
@@ -2,20 +2,21 @@ package daemon
import (
"context"
+ "errors"
"fmt"
"log"
+ "os"
"os/exec"
+ "slices"
"strconv"
"strings"
"time"
- "errors"
- "os"
- "slices"
-
- "github.com/pancsta/sway-yast/internal/types"
- "github.com/pancsta/sway-yast/internal/watcher"
+
+ "github.com/pancsta/sway-yasm/internal/types"
+ "github.com/pancsta/sway-yasm/internal/watcher"
"github.com/Difrex/gosway/ipc"
+ usrCmds "github.com/pancsta/sway-yasm/pkg/usr-cmds"
"github.com/samber/lo"
)
@@ -47,32 +48,48 @@ type Daemon struct {
openedAt time.Time
Autoconfig bool
DefaultKeybindings bool
- Out *log.Logger
+ Logger *log.Logger
+ // current mouse output
+ mouseInOutput string
}
+// API compat check
+var _ usrCmds.DaemonAPI = &Daemon{}
+
// ///// ///// /////
// ///// DAEMON
// ///// ///// /////
+func isClipmanRunning() bool {
+ out, err := exec.Command("pgrep", "-a", "wl-paste").Output()
+ if err != nil {
+ return false
+ }
+
+ return strings.Contains(string(out), "clipman")
+}
+
func (d *Daemon) Start() {
var err error
d.ctx = context.Background()
+
d.winData = make(map[string]types.WindowData)
- d.watcher, err = watcher.New(d.ctx)
+ d.watcher, err = watcher.New(d.ctx, d.Logger)
if err != nil {
- d.Out.Fatalf("error: %s", err)
+ d.Logger.Fatalf("error: %s", err)
}
- // TODO reconnect backoff?
+
+ // connect
conn, err := ipc.NewSwayConnection()
if err != nil {
- d.Out.Fatal(err)
+ d.Logger.Fatal(err)
}
d.conn = conn
// read the existing tree to fill out the MRU list
tree, err := conn.GetTree()
if err != nil {
- d.Out.Fatal("error:", err)
+ d.Logger.Fatal("error:", err)
}
for _, output := range tree.Nodes {
for _, workspace := range output.Nodes {
@@ -82,78 +99,47 @@ func (d *Daemon) Start() {
}
}
- // set up the window layout
if d.Autoconfig {
- msgs := []string{
- `for_window [title="sway-yast"] floating enable`,
- `for_window [title="sway-yast"] border none`,
- `for_window [title="sway-yast"] sticky enable`,
- }
- err = d.SwayMsgs(msgs)
- if err != nil {
- d.Out.Fatal("error:", err)
- }
+ err = d.autoconfig(err)
}
if d.DefaultKeybindings {
-
- var msgs []string
- if isDev() {
- msgs = []string{
- `bindsym alt+tab exec env YAST_DEBUG=1 sway-yast switcher`,
- `bindsym mod4+o exec env YAST_DEBUG=1 sway-yast pick-space`,
- `bindsym mod4+p exec env YAST_DEBUG=1 sway-yast pick-win`,
- `bindsym mod4+d exec env YAST_DEBUG=1 sway-yast path`,
- }
- } else {
- // TODO support $mod
- msgs = []string{
- `bindsym alt+tab exec sway-yast switcher`,
- `bindsym mod4+o exec sway-yast pick-space`,
- `bindsym mod4+p exec sway-yast pick-win`,
- `bindsym mod4+d exec sway-yast path`,
- }
- }
-
- err = d.SwayMsgs(msgs)
- if err != nil {
- d.Out.Fatal("error:", err)
- }
+ err = d.defaultKeybinding(err)
}
- // TODO reconnect backoff?
+ // subscribe to events
subCon, err := ipc.NewSwayConnection()
if err != nil {
- d.Out.Fatal(err)
+ d.Logger.Fatal(err)
}
// Subscribe only to the window related events
_, err = subCon.SendCommand(ipc.IPC_SUBSCRIBE, `["window"]`)
if err != nil {
- d.Out.Fatal(err)
+ d.Logger.Fatal(err)
}
// Listen for the events
s := subCon.Subscribe()
defer s.Close()
- go rpcServer(d.Out, d)
+ go rpcServer(d.Logger, d)
d.watcher.Start()
- log.Println("Listening for sway events...")
+ d.Logger.Printf("Listening for sway events...")
for {
select {
case event := <-s.Events:
if isLog() {
- log.Printf("Event: %s #%d", event.Change, event.Container.ID)
+ d.Logger.Printf("Event: %s #%d", event.Change, event.Container.ID)
}
if event.Change == "focus" {
- d.onFocus(&event.Container)
+ d.onFocus("focus", &event.Container)
}
if event.Change == "new" {
- d.onFocus(&event.Container)
+ d.onFocus("new", &event.Container)
}
if event.Change == "close" {
d.onClose(&event.Container)
@@ -167,6 +153,60 @@ func (d *Daemon) Start() {
}
}
+func (d *Daemon) defaultKeybinding(err error) error {
+ var msgs []string
+ if isDev() {
+ msgs = []string{
+ `bindsym alt+tab exec env YASM_DEBUG=1 sway-yasm switcher`,
+ `bindsym mod4+o exec env YASM_DEBUG=1 sway-yasm pick-space`,
+ `bindsym mod4+p exec env YASM_DEBUG=1 sway-yasm pick-win`,
+ `bindsym mod4+d exec env YASM_DEBUG=1 sway-yasm path`,
+ `bindsym alt+mod4+c exec env YASM_DEBUG=1 sway-yasm clipboard`,
+ }
+ } else {
+
+ // TODO support $mod, read config
+ msgs = []string{
+ `bindsym alt+tab exec sway-yasm switcher`,
+ `bindsym mod4+o exec sway-yasm pick-space`,
+ `bindsym mod4+p exec sway-yasm pick-win`,
+ `bindsym mod4+d exec sway-yasm path`,
+ `bindsym alt+mod4+c exec env sway-yasm clipboard`,
+ }
+ }
+
+ err = d.SwayMsgs(msgs)
+ if err != nil {
+ d.Logger.Fatal("error:", err)
+ }
+
+ return err
+}
+
+func (d *Daemon) autoconfig(err error) error {
+ msgs := []string{
+ `for_window [title="sway-yasm"] floating enable`,
+ `for_window [title="sway-yasm"] border none`,
+ `for_window [title="sway-yasm"] sticky enable`,
+ }
+ err = d.SwayMsgs(msgs)
+ if err != nil {
+ d.Logger.Fatal("error:", err)
+ }
+
+ if !isClipmanRunning() {
+ d.Logger.Printf("clipman not running, starting...")
+
+ err = d.SwayMsg("exec wl-paste -t text --watch clipman store " +
+ "--no-persist --max-items=200")
+ if err != nil {
+ d.Logger.Fatal("error:", err)
+ }
+ }
+
+ return err
+}
+
// ListSpaces returns names of the current workspaces.
func (d *Daemon) ListSpaces(skipOutputs []string) ([]string, error) {
tree, err := d.conn.GetTree()
@@ -271,18 +311,24 @@ func (d *Daemon) onClose(c *ipc.Container) {
d.winFocus = lo.Without(d.winFocus, id)
// remove from winData
+ data := d.winData[id]
delete(d.winData, id)
+
+ // run user scripts
+ for _, l := range usrCmds.Listeners["close"] {
+ l(d, data)
+ }
}
-func (d *Daemon) onFocus(con *ipc.Container) {
+func (d *Daemon) onFocus(event string, con *ipc.Container) {
// skip self
- if con.Name == "sway-yast" {
+ if con.Name == "sway-yasm" {
return
}
space, err := d.conn.GetFocusedWorkspace()
if err != nil {
- log.Printf("error: %s", err)
+ d.Logger.Printf("error: %s", err)
}
// update win data
@@ -312,7 +358,12 @@ func (d *Daemon) onFocus(con *ipc.Container) {
}
err = d.MouseToOutput(data.Output)
if err != nil {
- log.Printf("error: %s", err)
+ d.Logger.Printf("error: %s", err)
+ }
+
+ // run user scripts
+ for _, l := range usrCmds.Listeners[event] {
+ l(d, data)
}
}
@@ -358,7 +409,7 @@ func (d *Daemon) SwayMsg(msg string, args ...any) error {
cmd := fmt.Sprintf(msg, args...)
if isLog() {
- log.Printf("swaymsg %s", cmd)
+ d.Logger.Printf("swaymsg %s", cmd)
}
_, err := d.conn.RunSwayCommand(cmd)
if err != nil {
@@ -369,11 +420,17 @@ func (d *Daemon) SwayMsg(msg string, args ...any) error {
}
func (d *Daemon) MouseToOutput(output string) error {
+ if d.mouseInOutput == output {
+ return nil
+ }
+
_, err := d.conn.RunSwayCommand(fmt.Sprintf(
`input 0:0:wlr_virtual_pointer_v1 map_to_output "%s"`, output))
if err != nil {
return err
}
+ d.mouseInOutput = output
+
return nil
}
@@ -429,14 +486,14 @@ func (d *Daemon) spaceNameFromID(spaceID int) (string, error) {
}
func (d *Daemon) MoveSpaceToOutput(space, output string, focusedWinData types.WindowData) error {
- log.Printf("moving space %s to %s", space, output)
+ d.Logger.Printf("moving space %s to %s", space, output)
msgs := []string{
fmt.Sprintf("workspace %s", space),
fmt.Sprintf("move workspace to output %s", output),
}
err := d.SwayMsgs(msgs)
if err != nil {
- log.Printf("error: %s", err)
+ d.Logger.Printf("error: %s", err)
return err
}
@@ -445,13 +502,13 @@ func (d *Daemon) MoveSpaceToOutput(space, output string, focusedWinData types.Wi
// focus the original window back
err = d.FocusWinID(focusedWinData.ID)
if err != nil {
- log.Printf("error: %s", err)
+ d.Logger.Printf("error: %s", err)
return err
}
if d.MouseFollowsFocus {
err = d.MouseToOutput(focusedWinData.Output)
if err != nil {
- log.Printf("error: %s", err)
+ d.Logger.Printf("error: %s", err)
return err
}
}
@@ -459,16 +516,24 @@ func (d *Daemon) MoveSpaceToOutput(space, output string, focusedWinData types.Wi
return nil
}
-func (d *Daemon) WinMatch(win types.WindowData, match string, matchApp, matchTitle bool) bool {
- match = strings.ToLower(match)
- if matchApp && strings.Contains(strings.ToLower(win.App), match) {
- return true
- }
- if matchTitle && strings.Contains(strings.ToLower(win.Title), match) {
- return true
- }
+func (d *Daemon) WinMatchApp(win types.WindowData, match string) bool {
+ return strings.Contains(strings.ToLower(win.App), strings.ToLower(match))
+}
+
+func (d *Daemon) WinMatchTitle(win types.WindowData, match string) bool {
+ return strings.Contains(strings.ToLower(win.Title), strings.ToLower(match))
+}
+
+func (d *Daemon) HandlerOnFocus(func(types.WindowData)) {
+ // TODO
+}
+
+func (d *Daemon) HandlerOnClose(func(types.WindowData)) {
+ // TODO
+}
- return false
+func (d *Daemon) HandlerOnNew(func(types.WindowData)) {
+ // TODO
}
// ///// ///// /////
@@ -513,11 +578,11 @@ func IsLightMode() bool {
}
func isLog() bool {
- return os.Getenv("YAST_LOG") != ""
+ return os.Getenv("YASM_LOG") != ""
}
func isDev() bool {
- return os.Getenv("YAST_DEBUG") != ""
+ return os.Getenv("YASM_DEBUG") != ""
}
// parseFlags parses a string of flags into a map
diff --git a/internal/daemon/rpc.go b/internal/daemon/rpc.go
index 7f59033..17e798c 100644
--- a/internal/daemon/rpc.go
+++ b/internal/daemon/rpc.go
@@ -11,8 +11,8 @@ import (
"syscall"
"time"
- ss "github.com/pancsta/sway-yast/internal/watcher/states"
- usrCmds "github.com/pancsta/sway-yast/pkg/usr-cmds"
+ ss "github.com/pancsta/sway-yasm/internal/watcher/states"
+ usrCmds "github.com/pancsta/sway-yasm/pkg/usr-cmds"
)
// RPC
@@ -28,6 +28,7 @@ type RPCArgs struct {
ExePath string
UsrCmd string
UsrArgs string
+ Clipboard string
}
// RemoteWinList is an RPC method
@@ -221,13 +222,44 @@ func (d *Daemon) RemoteWinToSpace(args RPCArgs, ret *string) error {
return nil
}
+// RemoteCopy is an RPC method
+func (d *Daemon) RemoteCopy(args RPCArgs, ret *string) error {
+ log.Printf("RemoteCopy...")
+
+ // create a temp file
+ tmpFile, err := os.CreateTemp("", "sway-yasm-clip.txt")
+ if err != nil {
+ return err
+ }
+ // clean up with a delay
+ go func() {
+ time.Sleep(time.Second)
+ os.Remove(tmpFile.Name())
+ }()
+
+ // save clipboard as a file
+ if _, err := tmpFile.WriteString(args.Clipboard); err != nil {
+ tmpFile.Close()
+ return err
+ }
+ tmpFile.Close()
+
+ // copy from file
+ err = d.SwayMsg(`exec "cat %s | wl-copy"`, tmpFile.Name())
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
func RemoteCall(method string, args RPCArgs) (string, error) {
// TODO timeout
log.Printf("rpcCall %s...", method)
var err error
url := rpcHost
- if os.Getenv("YAST_DEBUG") != "" {
+ if os.Getenv("YASM_DEBUG") != "" {
url = rpcHostDbg
}
@@ -288,7 +320,7 @@ func rpcServer(out *log.Logger, server any) {
out.Fatal("register error:", err)
}
url := rpcHost
- if os.Getenv("YAST_DEBUG") != "" {
+ if os.Getenv("YASM_DEBUG") != "" {
url = rpcHostDbg
}
diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go
index e1a200a..8fd0a81 100644
--- a/internal/watcher/watcher.go
+++ b/internal/watcher/watcher.go
@@ -12,7 +12,7 @@ import (
"github.com/fsnotify/fsnotify"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
"github.com/pancsta/asyncmachine-go/pkg/telemetry"
- ss "github.com/pancsta/sway-yast/internal/watcher/states"
+ ss "github.com/pancsta/sway-yasm/internal/watcher/states"
)
// PathWatcher watches all dirs in PATH for changes and returns a list
@@ -32,7 +32,7 @@ type PathWatcher struct {
lastRefresh map[string]time.Time
}
-func New(ctx context.Context) (*PathWatcher, error) {
+func New(ctx context.Context, logger *log.Logger) (*PathWatcher, error) {
w := &PathWatcher{
EnvPath: os.Getenv("PATH"),
dirCache: make(map[string][]string),
@@ -60,7 +60,7 @@ func New(ctx context.Context) (*PathWatcher, error) {
return nil, err
}
- w.Mach.SetTestLogger(log.Printf, am.LogChanges)
+ w.Mach.SetTestLogger(logger.Printf, am.LogChanges)
w.Mach.SetLogArgs(am.NewArgsMapper([]string{"dir"}, 0))
if isAMDebug() {
err = telemetry.TransitionsToDBG(w.Mach, "")
@@ -330,7 +330,7 @@ func (w *PathWatcher) Stop() {
// ///// ///// /////
func isAMDebug() bool {
- return os.Getenv("YAST_DEBUG") == "2"
+ return os.Getenv("YASM_DEBUG") == "2"
}
func isExecutable(path string) (bool, error) {
diff --git a/pkg/usr-cmds/api.go b/pkg/usr-cmds/api.go
new file mode 100644
index 0000000..eb3d36e
--- /dev/null
+++ b/pkg/usr-cmds/api.go
@@ -0,0 +1,77 @@
+package usrCmds
+
+import (
+ "github.com/Difrex/gosway/ipc"
+ "github.com/pancsta/sway-yasm/internal/types"
+ "log"
+)
+
+type DaemonAPI interface {
+ FocusedWindow() types.WindowData
+ ListSpaces(skipOutputs []string) ([]string, error)
+ GetWinTreePath(id int) ([]*ipc.Node, error)
+ PrevWindow() types.WindowData
+ SwayMsgs(msgs []string) error
+ SwayMsg(msg string, args ...any) error
+ MoveWinToSpaceNum(winID, spaceNum int) error
+ MoveWinToSpace(winID int, space string) error
+ MoveSpaceToOutput(space, output string, focusedWinData types.WindowData) error
+ ListWindows() map[string]types.WindowData
+ MouseToOutput(output string) error
+ FocusWinID(id int) error
+ WinMatchApp(win types.WindowData, match string) bool
+ WinMatchTitle(win types.WindowData, match string) bool
+}
+
+type UserFunc func(DaemonAPI, map[string]string) (string, error)
+type ListenerFunc func(DaemonAPI, types.WindowData)
+
+var Registered map[string]UserFunc
+var Listeners map[string][]ListenerFunc
+
+func init() {
+ if Listeners == nil {
+ Listeners = make(map[string][]ListenerFunc)
+ }
+}
+
+// register registers a new user command function.
+func register(name string, fn UserFunc) {
+ if Registered == nil {
+ Registered = make(map[string]UserFunc)
+ }
+ Registered[name] = fn
+}
+
+// listener registers a new event listener.
+func listener(event string, fn ListenerFunc) {
+ if Listeners == nil {
+ Listeners = make(map[string][]ListenerFunc)
+ }
+ Listeners[event] = append(Listeners[event], fn)
+}
+
+// register registers a new user command function.
+func onClose(fn ListenerFunc) {
+ listener("close", fn)
+}
+
+// register registers a new user command function.
+func onFocus(fn ListenerFunc) {
+ listener("focus", fn)
+}
+
+// register registers a new user command function.
+func onNew(fn ListenerFunc) {
+ listener("new", fn)
+}
+
+// inspect prints the value to the daemon's log.
+func inspect(val any) {
+ log.Printf("Inspect: %+v\n", val)
+}
+
+// inspect prints the value to the daemon's log.
+func p(msg string, vals ...any) {
+ log.Printf(msg, vals...)
+}
diff --git a/pkg/usr-cmds/arrange.go b/pkg/usr-cmds/arrange.go
index b6f6fdf..358420d 100644
--- a/pkg/usr-cmds/arrange.go
+++ b/pkg/usr-cmds/arrange.go
@@ -22,7 +22,7 @@ func ArrangeWindows(d DaemonAPI, _ map[string]string) (string, error) {
var err error
// multi space apps
- if d.WinMatch(win, "firefox", true, false) {
+ if d.WinMatchApp(win, "firefox") {
// skip if already there
if win.Workspace == spaces.dev || win.Workspace == spaces.blogic {
continue
@@ -36,7 +36,7 @@ func ArrangeWindows(d DaemonAPI, _ map[string]string) (string, error) {
firefox++
}
- if d.WinMatch(win, "krusader", true, false) {
+ if d.WinMatchApp(win, "krusader") {
// skip if already there
if win.Workspace == spaces.dev || win.Workspace == spaces.blogic {
continue
@@ -51,29 +51,32 @@ func ArrangeWindows(d DaemonAPI, _ map[string]string) (string, error) {
}
// 1:dev
- if d.WinMatch(win, "jetbrains", true, false) {
+ if d.WinMatchApp(win, "jetbrains") {
err = d.MoveWinToSpace(win.ID, spaces.dev)
}
- if d.WinMatch(win, "jaeger", false, true) {
+ if d.WinMatchApp(win, "jaeger") {
err = d.MoveWinToSpace(win.ID, spaces.dev)
}
// 2:blogic
- if d.WinMatch(win, "obsidian", true, false) {
+ if d.WinMatchApp(win, "obsidian") {
+ err = d.MoveWinToSpace(win.ID, spaces.blogic)
+ }
+ if d.WinMatchApp(win, "gmail") {
err = d.MoveWinToSpace(win.ID, spaces.blogic)
}
// 3:read
- if d.WinMatch(win, "pocket", false, true) {
+ if d.WinMatchApp(win, "pocket") {
err = d.MoveWinToSpace(win.ID, spaces.read)
}
- if d.WinMatch(win, "inoreader", false, true) {
+ if d.WinMatchApp(win, "inoreader") {
err = d.MoveWinToSpace(win.ID, spaces.read)
}
- if d.WinMatch(win, "thunderbird", false, true) {
+ if d.WinMatchApp(win, "thunderbird") {
err = d.MoveWinToSpace(win.ID, spaces.read)
}
- if d.WinMatch(win, "discord", false, true) {
+ if d.WinMatchApp(win, "discord") {
err = d.MoveWinToSpace(win.ID, spaces.read)
}
diff --git a/pkg/usr-cmds/base.go b/pkg/usr-cmds/base.go
deleted file mode 100644
index d2cc930..0000000
--- a/pkg/usr-cmds/base.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package usrCmds
-
-import (
- "github.com/Difrex/gosway/ipc"
- "github.com/pancsta/sway-yast/internal/types"
-)
-
-type DaemonAPI interface {
- FocusedWindow() types.WindowData
- ListSpaces(skipOutputs []string) ([]string, error)
- GetWinTreePath(id int) ([]*ipc.Node, error)
- PrevWindow() types.WindowData
- SwayMsgs(msgs []string) error
- SwayMsg(msg string, args ...any) error
- MoveWinToSpaceNum(winID, spaceNum int) error
- MoveWinToSpace(winID int, space string) error
- MoveSpaceToOutput(space, output string, focusedWinData types.WindowData) error
- ListWindows() map[string]types.WindowData
- WinMatch(win types.WindowData, match string, matchApp, matchTitle bool) bool
- MouseToOutput(output string) error
- FocusWinID(id int) error
-}
-
-type UserFunc func(DaemonAPI, map[string]string) (string, error)
-
-var (
- Registered map[string]UserFunc
-)
-
-// register registers a user command
-func register(name string, fn UserFunc) {
- if Registered == nil {
- Registered = make(map[string]UserFunc)
- }
- Registered[name] = fn
-}
diff --git a/pkg/usr-cmds/template.go b/pkg/usr-cmds/template.go
new file mode 100644
index 0000000..33d7991
--- /dev/null
+++ b/pkg/usr-cmds/template.go
@@ -0,0 +1,29 @@
+package usrCmds
+
+func init() {
+ register("template", Template)
+ // onFocus(func(d DaemonAPI, win types.WindowData) {
+ // fmt.Println("template.focus")
+ // })
+ // onClose(func(d DaemonAPI, win types.WindowData) {
+ // fmt.Println("template.close")
+ // })
+ // onNew(func(d DaemonAPI, win types.WindowData) {
+ // fmt.Println("template.new")
+ // })
+}
+
+// Template is a template for creating new user commands, with some API examples.
+func Template(d DaemonAPI, args map[string]string) (string, error) {
+ win := d.FocusedWindow()
+ path, err := d.GetWinTreePath(win.ID)
+ if err != nil {
+ return "", err
+ }
+
+ p("Focused window: %d", win.Title)
+ p("Focused workspace: %d", path[0].Name)
+ inspect(args)
+
+ return "cli output", d.SwayMsg(`exec echo %d`, win.ID)
+}
diff --git a/pkg/usr-cmds/titlebar-toggle.go b/pkg/usr-cmds/titlebar-toggle.go
new file mode 100644
index 0000000..9039870
--- /dev/null
+++ b/pkg/usr-cmds/titlebar-toggle.go
@@ -0,0 +1,25 @@
+package usrCmds
+
+func init() {
+ register("titlebar-toggle", TitlebarToggle)
+}
+
+// TitlebarToggle is a template for creating new user commands, with some API examples.
+func TitlebarToggle(d DaemonAPI, _ map[string]string) (string, error) {
+ cw := d.FocusedWindow()
+ path, err := d.GetWinTreePath(cw.ID)
+ if err != nil {
+ return "", err
+ }
+
+ win := path[len(path)-1]
+
+ var border string
+ if win.Border == "normal" {
+ border = "none"
+ } else {
+ border = "normal"
+ }
+
+ return "", d.SwayMsg(`border %s`, border)
+}
diff --git a/scripts/build.sh b/scripts/build.sh
index 525e5f5..039f452 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -1,3 +1,3 @@
#!/usr/bin/env sh
-go build -o sway-yast cmd/sway-yast/main.go
+go build -o sway-yasm cmd/sway-yasm/main.go