From 1fe1cf56564d8ca31c968ff9e7028129a39e069c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Sat, 8 Nov 2025 21:16:39 +0200 Subject: [PATCH 1/5] feat: add new function for easier handling of creating actions Add FromName function that takes the action name and it's JSON data as parameter, and handles the instantiation of the action. --- actions/actions.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/actions/actions.go b/actions/actions.go index e655baa..2c80b17 100644 --- a/actions/actions.go +++ b/actions/actions.go @@ -18,6 +18,7 @@ package actions import ( "encoding/json" + "errors" "log/slog" "reflect" @@ -98,6 +99,17 @@ func ParseRawActions(rawActions map[string]json.RawMessage) []Action { return actionList } +// FromName returns the named action and if data is passed, uses that to populate the model. +func FromName(name string, data map[string]any) (Action, error) { + jsonData, err := json.Marshal(data) + if err != nil { + return nil, errors.New("error parsing action json data") + } + action := FromRegistry(name, jsonData) + + return action, nil +} + // ActionRegistry contains all the actions Niri currently sends. // // The key needs to be the action name, and it should return the correct action model, and set From 2d2e2d31f4644ba8ef8d8bbc5b9b79c1b8f629a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Sat, 8 Nov 2025 21:16:39 +0200 Subject: [PATCH 2/5] fix: use the new action function Use the FromName action function when creating the action for moving a floating window. --- cmd/move.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/cmd/move.go b/cmd/move.go index 0e57219..4eace7c 100644 --- a/cmd/move.go +++ b/cmd/move.go @@ -1,7 +1,6 @@ package cmd import ( - "encoding/json" "errors" "strconv" @@ -89,6 +88,7 @@ var moveCmd = &cobra.Command{ } data := map[string]any{ + "ID": window.ID, "x": map[string]float64{ "SetFixed": newX, }, @@ -96,12 +96,7 @@ var moveCmd = &cobra.Command{ "SetFixed": newY, }, } - jsonData, err := json.Marshal(data) - if err != nil { - return errors.New("error parsing action json data") - } - action := actions.FromRegistry("MoveFloatingWindow", jsonData) - action = actions.HandleDynamicIDs(action, models.PossibleKeys{ID: window.ID}) + action, _ := actions.FromName("MoveFloatingWindow", data) connection.PerformAction(action) return nil }, From 377cc08c537653ec0dc14e43f79ba283ef248dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Sat, 8 Nov 2025 21:16:39 +0200 Subject: [PATCH 3/5] feat: add new function to run shell commands Add RunCommand function that takes a shell command as parameter, and runs it with sh. --- internal/common/common.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/common/common.go b/internal/common/common.go index 32c317c..be33b78 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -2,8 +2,11 @@ package common import ( + "bytes" + "fmt" "log/slog" "os" + "os/exec" "reflect" "github.com/soderluk/nirimgr/config" @@ -141,3 +144,19 @@ func FilterOutputs(outputs []*models.Output, f func(*models.Output) bool) []*mod func FilterOutputsChain(outputs []*models.Output, f func(*models.Output) bool) models.OutputSlice { return models.OutputSlice{Outputs: FilterOutputs(outputs, f)} } + +// RunCommand runs the given command with sh and returns the result in bytes. +func RunCommand(command string) ([]byte, error) { + cmd := exec.Command("sh", "-c", command) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("%w: %s", err, stderr.String()) + } + + return stdout.Bytes(), nil +} From 03feff7b6ccfefe2dd38408fba07b27c12cd7b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Sat, 8 Nov 2025 21:16:39 +0200 Subject: [PATCH 4/5] feat: add new command to switch windows The command gets a list of windows, and spawns a fuzzel dmenu to let the user choose an open window to switch to. --- cmd/switchWindow.go | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 cmd/switchWindow.go diff --git a/cmd/switchWindow.go b/cmd/switchWindow.go new file mode 100644 index 0000000..d9bde06 --- /dev/null +++ b/cmd/switchWindow.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "errors" + "fmt" + "log/slog" + "strconv" + "strings" + + "github.com/soderluk/nirimgr/actions" + "github.com/soderluk/nirimgr/internal/common" + "github.com/soderluk/nirimgr/internal/connection" + "github.com/spf13/cobra" +) + +// switchWindowCmd is used to open fuzzel with a list of windows, and you can choose which window to focus on. +var switchWindowCmd = &cobra.Command{ + Use: "switch-window", + Short: "Switch window focus with fuzzel.", + Long: `Opens fuzzel with a list of currently open windows. Choose one and you focus that window.`, + SilenceUsage: true, // If there's an error during running the command, don't show usage. + RunE: func(cmd *cobra.Command, args []string) error { + windows, err := connection.ListWindows() + if err != nil { + return errors.New("could not get windows") + } + var command string + for _, window := range windows { + command += fmt.Sprintf("[WS: %d] [%s] (%s) -> id: %d\n", window.WorkspaceID, window.AppID, window.Title, window.ID) + } + // TODO: Support any dmenu launcher here. Make the launcher configurable. + fullCommand := fmt.Sprintf("echo \"%s\" | sort | fuzzel -d -w 75 | awk '{print $NF}'", command) + result, err := common.RunCommand(fullCommand) + if err != nil { + slog.Error("Error running command", slog.String("command", fullCommand), slog.Any("error", err.Error())) + return errors.New("could not run command") + } + windowID, err := strconv.ParseUint(strings.Replace(string(result), "\n", "", 1), 10, 64) + if err != nil { + slog.Error("Could not convert result to uint64", slog.String("result", string(result)), slog.Any("error", err.Error())) + return errors.New("could not run command") + } + action, _ := actions.FromName("FocusWindow", map[string]any{"ID": windowID}) + connection.PerformAction(action) + + return nil + }, +} + +func init() { + rootCmd.AddCommand(switchWindowCmd) +} From 8a838b6f00ba2d31f3af21c376fda16292f8787b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kenneth=20S=C3=B6derlund?= Date: Sat, 8 Nov 2025 23:05:58 +0200 Subject: [PATCH 5/5] docs: update readme and example config Add the new command mention in the README.md and update the niri config example. --- README.md | 4 ++++ examples/niri-config.kdl | 3 +++ 2 files changed, 7 insertions(+) diff --git a/README.md b/README.md index b5e7bc6..6b8323e 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,8 @@ Example configuration (see: [config.json](./examples/config.json)): } ``` +## Changelog + - Added in v0.2.0: Configuration: Rule type - The new rule type defines if the rule should apply to a window or a workspace. - Added in v0.3.0: Scratch command: spawn-or-focus - The new scratch command supports spawning defined apps, or focusing them if they're already running. - Added in v0.4.0: Configuration: Add new configuration showScratchpadActions, i.e. define actions to be performed on the shown scratchpad window. @@ -284,6 +286,8 @@ Example configuration (see: [config.json](./examples/config.json)): where the `model` refers to the `Window` model when matching `"type": "window"`, `Workspace` model when matching `"type": "workspace"`, or the event model when listening on custom events. - Added in v0.7.0: Command to move a floating window to the edges of the screen `nirimgr floating move top 10` +- Added in v0.8.0: New action and event models +- Added in v0.9.0: Command to switch windows using fuzzel `nirimgr switch-window`. The rules are the same as the `window-rule` in Niri configuration. Currently we only match the window on a given title or app-id. Then specify which action you want to do with the matched window. In the example above, the gnome calculator diff --git a/examples/niri-config.kdl b/examples/niri-config.kdl index bcb06cf..1a1c4e9 100644 --- a/examples/niri-config.kdl +++ b/examples/niri-config.kdl @@ -35,5 +35,8 @@ binds { Mod+Ctrl+L { spawn-sh "nirimgr floating move right 4 || niri msg action move-column-right-or-to-monitor-right" } + Alt+Tab { + spawn nirimgr switch-window + } }