Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions actions/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package actions

import (
"encoding/json"
"errors"
"log/slog"
"reflect"

Expand Down Expand Up @@ -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
Expand Down
9 changes: 2 additions & 7 deletions cmd/move.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cmd

import (
"encoding/json"
"errors"
"strconv"

Expand Down Expand Up @@ -89,19 +88,15 @@ var moveCmd = &cobra.Command{
}

data := map[string]any{
"ID": window.ID,
"x": map[string]float64{
"SetFixed": newX,
},
"y": map[string]float64{
"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
},
Expand Down
52 changes: 52 additions & 0 deletions cmd/switchWindow.go
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 3 additions & 0 deletions examples/niri-config.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

19 changes: 19 additions & 0 deletions internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
package common

import (
"bytes"
"fmt"
"log/slog"
"os"
"os/exec"
"reflect"

"github.com/soderluk/nirimgr/config"
Expand Down Expand Up @@ -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
}
Loading