initial commit

This commit is contained in:
2025-07-25 13:41:23 +02:00
commit 840de62902
27 changed files with 1772 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*_test.go

52
do.go Normal file
View File

@ -0,0 +1,52 @@
package niri
import (
"errors"
"fmt"
)
func (c *Client) Action(action Action) error {
// print request and response to console:
//
// io.Copy(os.Stdout, asJsonReader(action))
// r, err := readSocketRaw(c.GetSocketPath(), asJsonReader(action))
// if err != nil {
// return err
// }
// io.Copy(os.Stdout, r)
// return nil
resp := readSocket[string](c.GetSocketPath(), asJsonReader(action))
if resp.Err != nil {
return errors.New(*resp.Err)
} else if resp.OK == nil {
return errors.New("unexpected response value: nil")
} else if *resp.OK != "Handled" {
return fmt.Errorf("unexpected response value: %s", *resp.OK)
}
return nil
}
func (c *Client) Output(outputAction OutputActionRequest) error {
body := map[string]OutputActionRequest{"Output": outputAction}
// print request and response to console:
//
// io.Copy(os.Stdout, asJsonReader(body))
// r, err := readSocketRaw(c.GetSocketPath(), asJsonReader(body))
// if err != nil {
// return err
// }
// io.Copy(os.Stdout, r)
// return nil
resp := readSocket[outputActionResponse](c.GetSocketPath(), asJsonReader(body))
if resp.Err != nil {
return errors.New(*resp.Err)
} else if resp.OK == nil {
return errors.New("unexpected response value: nil")
} else if resp.OK.OutputConfigChanged != "Applied" {
return fmt.Errorf("unexpected response value: %s", resp.OK.OutputConfigChanged)
}
return nil
}

15
event_stream.go Normal file
View File

@ -0,0 +1,15 @@
package niri
import (
"context"
"git.tordarus.net/tordarus/channel"
)
func (c *Client) SubscribeEvents(ctx context.Context) (<-chan Event, error) {
ch, err := subscribeSocket[*eventContainer](ctx, c.GetSocketPath(), asJsonReader("EventStream"))
if err != nil {
return nil, err
}
return channel.MapSuccessive(ch, (*eventContainer).event), nil
}

103
getters.go Normal file
View File

@ -0,0 +1,103 @@
package niri
import (
"fmt"
"image/color"
"git.tordarus.net/tordarus/slices"
)
func (c *Client) GetOutputs() ([]Output, error) {
resp := readSocket[map[string]map[string]Output](c.GetSocketPath(), asJsonReader("Outputs"))
if resp.Err != nil {
return nil, fmt.Errorf("retrieving outputs: %s", *resp.Err)
}
return slices.OfMap((*resp.OK)["Outputs"], slices.UnmapValue), nil
}
func (c *Client) GetWorkspaces() ([]Workspace, error) {
resp := readSocket[map[string][]Workspace](c.GetSocketPath(), asJsonReader("Workspaces"))
if resp.Err != nil {
return nil, fmt.Errorf("retrieving workspaces: %s", *resp.Err)
}
return (*resp.OK)["Workspaces"], nil
}
func (c *Client) GetWindows() ([]Window, error) {
resp := readSocket[map[string][]Window](c.GetSocketPath(), asJsonReader("Windows"))
if resp.Err != nil {
return nil, fmt.Errorf("retrieving windows: %s", *resp.Err)
}
return (*resp.OK)["Windows"], nil
}
func (c *Client) GetLayers() ([]LayerSurface, error) {
resp := readSocket[map[string][]LayerSurface](c.GetSocketPath(), asJsonReader("Layers"))
if resp.Err != nil {
return nil, fmt.Errorf("retrieving layers: %s", *resp.Err)
}
return (*resp.OK)["Layers"], nil
}
func (c *Client) GetKeyboardLayouts() (*KeyboardLayouts, error) {
resp := readSocket[map[string]KeyboardLayouts](c.GetSocketPath(), asJsonReader("KeyboardLayouts"))
if resp.Err != nil {
return nil, fmt.Errorf("retrieving layers: %s", *resp.Err)
}
value := (*resp.OK)["KeyboardLayouts"]
return &value, nil
}
func (c *Client) GetFocusedOutput() (*Output, error) {
resp := readSocket[map[string]Output](c.GetSocketPath(), asJsonReader("FocusedOutput"))
if resp.Err != nil {
return nil, fmt.Errorf("retrieving outputs: %s", *resp.Err)
}
value := (*resp.OK)["FocusedOutput"]
return &value, nil
}
func (c *Client) GetFocusedWindow() (*Window, error) {
resp := readSocket[map[string]Window](c.GetSocketPath(), asJsonReader("FocusedWindow"))
if resp.Err != nil {
return nil, fmt.Errorf("retrieving outputs: %s", *resp.Err)
}
value := (*resp.OK)["FocusedWindow"]
return &value, nil
}
func (c *Client) PickWindow() (*Window, error) {
resp := readSocket[map[string]Window](c.GetSocketPath(), asJsonReader("PickWindow"))
if resp.Err != nil {
return nil, fmt.Errorf("retrieving outputs: %s", *resp.Err)
}
value := (*resp.OK)["PickedWindow"]
return &value, nil
}
func (c *Client) PickColor() (color.Color, error) {
resp := readSocket[map[string]niriColor](c.GetSocketPath(), asJsonReader("PickColor"))
if resp.Err != nil {
return nil, fmt.Errorf("retrieving outputs: %s", *resp.Err)
}
value := (*resp.OK)["PickedColor"]
return value.AsColor(), nil
}
func (c *Client) GetVersion() (string, error) {
resp := readSocket[map[string]string](c.GetSocketPath(), asJsonReader("Version"))
if resp.Err != nil {
return "", fmt.Errorf("retrieving version: %s", *resp.Err)
}
value := (*resp.OK)["Version"]
return value, nil
}
func (c *Client) GetOverviewState() (bool, error) {
resp := readSocket[map[string]overviewState](c.GetSocketPath(), asJsonReader("OverviewState"))
if resp.Err != nil {
return false, fmt.Errorf("retrieving version: %s", *resp.Err)
}
value := (*resp.OK)["OverviewState"]
return value.IsOpen, nil
}

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module git.tordarus.net/niri-tools/niri
go 1.24.4
require (
git.tordarus.net/tordarus/channel v0.1.20
git.tordarus.net/tordarus/slices v0.0.15
github.com/adrg/xdg v0.5.3
)
require (
git.tordarus.net/tordarus/gmath v0.0.7 // indirect
golang.org/x/sys v0.33.0 // indirect
)

18
go.sum Normal file
View File

@ -0,0 +1,18 @@
git.tordarus.net/tordarus/channel v0.1.20 h1:kHAyp18IauDAyFtkbN67+qD2SB40Wiy0w1tC4JpHbOo=
git.tordarus.net/tordarus/channel v0.1.20/go.mod h1:8/dWFTdGO7g4AeSZ7cF6GerkGbe9c4dBVMVDBxOd9m4=
git.tordarus.net/tordarus/gmath v0.0.7 h1:tR48idt9AUL0r556ww3ZxByTKJEr6NWCTlhl2ihzYxQ=
git.tordarus.net/tordarus/gmath v0.0.7/go.mod h1:mO7aPlvNrGVE9UFXEuuACjZgMDsM63l3OcQy6xSQnoE=
git.tordarus.net/tordarus/slices v0.0.15 h1:qxKS+7BCZ/LzQbRCvdDFoLQXaJZ2C0GfVju/kPt1r3g=
git.tordarus.net/tordarus/slices v0.0.15/go.mod h1:eJBw6pSDNivPI0l4e0sGKUJzou/lbzHflXdAUzp1g4o=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

547
model_action.go Normal file
View File

@ -0,0 +1,547 @@
package niri
import "time"
type Action struct {
Action map[string]any `json:"Action"`
}
func ActionCustom[T any](actionName string, actionContent T) Action {
return Action{map[string]any{actionName: actionContent}}
}
func ActionQuit(skipConfirmation bool) Action {
return ActionCustom("Quit", map[string]any{"skip_confirmation": skipConfirmation})
}
func ActionPowerOffMonitors() Action {
return ActionCustom("PowerOffMonitors", map[string]any{})
}
func ActionPowerOnMonitors() Action {
return ActionCustom("PowerOnMonitors", map[string]any{})
}
func ActionSpawn(command []string) Action {
return ActionCustom("Spawn", map[string]any{"command": command})
}
func ActionDoScreenTransition(delay Option[time.Duration]) Action {
return ActionCustom("DoScreenTransition", map[string]any{"delay_ms": mapOption(delay, time.Duration.Milliseconds)})
}
func ActionScreenshot(showPointer bool) Action {
return ActionCustom("Screenshot", map[string]any{"show_pointer": showPointer})
}
func ActionScreenshotScreen(writeToDisk, showPointer bool) Action {
return ActionCustom("ScreenshotScreen", map[string]any{"write_to_disk": writeToDisk, "show_pointer": showPointer})
}
func ActionScreenshotWindow(id Option[WindowID], writeToDisk bool) Action {
return ActionCustom("ScreenshotWindow", map[string]any{"id": id, "write_to_disk": writeToDisk})
}
func ActionToggleKeyboardShortcutsInhibit() Action {
return ActionCustom("ToggleKeyboardShortcutsInhibit", map[string]any{})
}
func ActionCloseWindow(id Option[WindowID]) Action {
return ActionCustom("CloseWindow", map[string]any{"id": id})
}
func ActionFullscreenWindow(id Option[WindowID]) Action {
return ActionCustom("FullscreenWindow", map[string]any{"id": id})
}
func ActionToggleWindowedFullscreen(id Option[WindowID]) Action {
return ActionCustom("ToggleWindowedFullscreen", map[string]any{"id": id})
}
func ActionFocusWindow(id WindowID) Action {
return ActionCustom("FocusWindow", map[string]any{"id": id})
}
func ActionFocusWindowInColumn(index int) Action {
return ActionCustom("FocusWindowInColumn", map[string]any{"index": index})
}
func ActionFocusWindowPrevious() Action {
return ActionCustom("FocusWindowPrevious", map[string]any{})
}
func ActionFocusColumnLeft() Action {
return ActionCustom("FocusColumnLeft", map[string]any{})
}
func ActionFocusColumnRight() Action {
return ActionCustom("FocusColumnRight", map[string]any{})
}
func ActionFocusColumnFirst() Action {
return ActionCustom("FocusColumnFirst", map[string]any{})
}
func ActionFocusColumnLast() Action {
return ActionCustom("FocusColumnLast", map[string]any{})
}
func ActionFocusColumnRightOrFirst() Action {
return ActionCustom("FocusColumnRightOrFirst", map[string]any{})
}
func ActionFocusColumnLeftOrLast() Action {
return ActionCustom("FocusColumnLeftOrLast", map[string]any{})
}
func ActionFocusColumn(index int) Action {
return ActionCustom("FocusColumn", map[string]any{"index": index})
}
func ActionFocusWindowOrMonitorUp() Action {
return ActionCustom("FocusWindowOrMonitorUp", map[string]any{})
}
func ActionFocusWindowOrMonitorDown() Action {
return ActionCustom("FocusWindowOrMonitorDown", map[string]any{})
}
func ActionFocusColumnOrMonitorLeft() Action {
return ActionCustom("FocusColumnOrMonitorLeft", map[string]any{})
}
func ActionFocusColumnOrMonitorRight() Action {
return ActionCustom("FocusColumnOrMonitorRight", map[string]any{})
}
func ActionFocusWindowDown() Action {
return ActionCustom("FocusWindowDown", map[string]any{})
}
func ActionFocusWindowUp() Action {
return ActionCustom("FocusWindowUp", map[string]any{})
}
func ActionFocusWindowDownOrColumnLeft() Action {
return ActionCustom("FocusWindowDownOrColumnLeft", map[string]any{})
}
func ActionFocusWindowDownOrColumnRight() Action {
return ActionCustom("FocusWindowDownOrColumnRight", map[string]any{})
}
func ActionFocusWindowUpOrColumnLeft() Action {
return ActionCustom("FocusWindowUpOrColumnLeft", map[string]any{})
}
func ActionFocusWindowUpOrColumnRight() Action {
return ActionCustom("FocusWindowUpOrColumnRight", map[string]any{})
}
func ActionFocusWindowOrWorkspaceDown() Action {
return ActionCustom("FocusWindowOrWorkspaceDown", map[string]any{})
}
func ActionFocusWindowOrWorkspaceUp() Action {
return ActionCustom("FocusWindowOrWorkspaceUp", map[string]any{})
}
func ActionFocusWindowTop() Action {
return ActionCustom("FocusWindowTop", map[string]any{})
}
func ActionFocusWindowBottom() Action {
return ActionCustom("FocusWindowBottom", map[string]any{})
}
func ActionFocusWindowDownOrTop() Action {
return ActionCustom("FocusWindowDownOrTop", map[string]any{})
}
func ActionFocusWindowUpOrBottom() Action {
return ActionCustom("FocusWindowUpOrBottom", map[string]any{})
}
func ActionMoveColumnLeft() Action {
return ActionCustom("MoveColumnLeft", map[string]any{})
}
func ActionMoveColumnRight() Action {
return ActionCustom("MoveColumnRight", map[string]any{})
}
func ActionMoveColumnToFirst() Action {
return ActionCustom("MoveColumnToFirst", map[string]any{})
}
func ActionMoveColumnToLast() Action {
return ActionCustom("MoveColumnToLast", map[string]any{})
}
func ActionMoveColumnLeftOrToMonitorLeft() Action {
return ActionCustom("MoveColumnLeftOrToMonitorLeft", map[string]any{})
}
func ActionMoveColumnRightOrToMonitorRight() Action {
return ActionCustom("MoveColumnRightOrToMonitorRight", map[string]any{})
}
func ActionMoveColumnToIndex(index int) Action {
return ActionCustom("MoveColumnToIndex", map[string]any{"index": index})
}
func ActionMoveWindowDown() Action {
return ActionCustom("MoveWindowDown", map[string]any{})
}
func ActionMoveWindowUp() Action {
return ActionCustom("MoveWindowUp", map[string]any{})
}
func ActionMoveWindowDownOrToWorkspaceDown() Action {
return ActionCustom("MoveWindowDownOrToWorkspaceDown", map[string]any{})
}
func ActionMoveWindowUpOrToWorkspaceUp() Action {
return ActionCustom("MoveWindowUpOrToWorkspaceUp", map[string]any{})
}
func ActionConsumeOrExpelWindowLeft(id Option[WindowID]) Action {
return ActionCustom("ConsumeOrExpelWindowLeft", map[string]any{"id": id})
}
func ActionConsumeOrExpelWindowRight(id Option[WindowID]) Action {
return ActionCustom("ConsumeOrExpelWindowRight", map[string]any{"id": id})
}
func ActionConsumeWindowIntoColumn() Action {
return ActionCustom("ConsumeWindowIntoColumn", map[string]any{})
}
func ActionExpelWindowFromColumn() Action {
return ActionCustom("ExpelWindowFromColumn", map[string]any{})
}
func ActionSwapWindowRight() Action {
return ActionCustom("SwapWindowRight", map[string]any{})
}
func ActionSwapWindowLeft() Action {
return ActionCustom("SwapWindowLeft", map[string]any{})
}
func ActionToggleColumnTabbedDisplay() Action {
return ActionCustom("ToggleColumnTabbedDisplay", map[string]any{})
}
func ActionSetColumnDisplay(display ColumnDisplay) Action {
return ActionCustom("SetColumnDisplay", map[string]any{"display": display})
}
func ActionCenterColumn() Action {
return ActionCustom("CenterColumn", map[string]any{})
}
func ActionCenterWindow(id Option[WindowID]) Action {
return ActionCustom("CenterWindow", map[string]any{"id": id})
}
func ActionCenterVisibleColumns() Action {
return ActionCustom("CenterVisibleColumns", map[string]any{})
}
func ActionFocusWorkspaceDown() Action {
return ActionCustom("FocusWorkspaceDown", map[string]any{})
}
func ActionFocusWorkspaceUp() Action {
return ActionCustom("FocusWorkspaceUp", map[string]any{})
}
func ActionFocusWorkspace(reference WorkspaceReferenceArg) Action {
return ActionCustom("FocusWorkspace", map[string]any{"reference": reference})
}
func ActionFocusWorkspacePrevious() Action {
return ActionCustom("FocusWorkspacePrevious", map[string]any{})
}
func ActionMoveWindowToWorkspaceDown() Action {
return ActionCustom("MoveWindowToWorkspaceDown", map[string]any{})
}
func ActionMoveWindowToWorkspaceUp() Action {
return ActionCustom("MoveWindowToWorkspaceUp", map[string]any{})
}
func ActionMoveWindowToWorkspace(id Option[WindowID], reference WorkspaceReferenceArg, focus bool) Action {
return ActionCustom("MoveWindowToWorkspace", map[string]any{"id": id, "reference": reference, "focus": focus})
}
func ActionMoveColumnToWorkspaceDown(focus bool) Action {
return ActionCustom("MoveColumnToWorkspaceDown", map[string]any{"focus": focus})
}
func ActionMoveColumnToWorkspaceUp(focus bool) Action {
return ActionCustom("MoveColumnToWorkspaceUp", map[string]any{"focus": focus})
}
func ActionMoveColumnToWorkspace(reference WorkspaceReferenceArg, focus bool) Action {
return ActionCustom("MoveColumnToWorkspace", map[string]any{"reference": reference, "focus": focus})
}
func ActionMoveWorkspaceDown() Action {
return ActionCustom("MoveWorkspaceDown", map[string]any{})
}
func ActionMoveWorkspaceUp() Action {
return ActionCustom("MoveWorkspaceUp", map[string]any{})
}
func ActionMoveWorkspaceToIndex(index int, reference Option[WorkspaceReferenceArg]) Action {
return ActionCustom("MoveWorkspaceToIndex", map[string]any{"index": index, "reference": reference})
}
func ActionSetWorkspaceName(name string, workspace Option[WorkspaceReferenceArg]) Action {
return ActionCustom("SetWorkspaceName", map[string]any{"name": name, "workspace": workspace})
}
func ActionUnsetWorkspaceName(reference Option[WorkspaceReferenceArg]) Action {
return ActionCustom("UnsetWorkspaceName", map[string]any{"reference": reference})
}
func ActionFocusMonitorLeft() Action {
return ActionCustom("FocusMonitorLeft", map[string]any{})
}
func ActionFocusMonitorRight() Action {
return ActionCustom("FocusMonitorRight", map[string]any{})
}
func ActionFocusMonitorDown() Action {
return ActionCustom("FocusMonitorDown", map[string]any{})
}
func ActionFocusMonitorUp() Action {
return ActionCustom("FocusMonitorUp", map[string]any{})
}
func ActionFocusMonitorPrevious() Action {
return ActionCustom("FocusMonitorPrevious", map[string]any{})
}
func ActionFocusMonitorNext() Action {
return ActionCustom("FocusMonitorNext", map[string]any{})
}
func ActionFocusMonitor(output OutputName) Action {
return ActionCustom("FocusMonitorNext", map[string]any{"output": output})
}
func ActionMoveWindowToMonitorLeft() Action {
return ActionCustom("MoveWindowToMonitorLeft", map[string]any{})
}
func ActionMoveWindowToMonitorRight() Action {
return ActionCustom("MoveWindowToMonitorRight", map[string]any{})
}
func ActionMoveWindowToMonitorDown() Action {
return ActionCustom("MoveWindowToMonitorDown", map[string]any{})
}
func ActionMoveWindowToMonitorUp() Action {
return ActionCustom("MoveWindowToMonitorUp", map[string]any{})
}
func ActionMoveWindowToMonitorPrevious() Action {
return ActionCustom("MoveWindowToMonitorPrevious", map[string]any{})
}
func ActionMoveWindowToMonitorNext() Action {
return ActionCustom("MoveWindowToMonitorNext", map[string]any{})
}
func ActionMoveWindowToMonitor(id Option[WindowID], output OutputName) Action {
return ActionCustom("FocusMonitorNext", map[string]any{"id": id, "output": output})
}
func ActionMoveColumnToMonitorLeft() Action {
return ActionCustom("MoveColumnToMonitorLeft", map[string]any{})
}
func ActionMoveColumnToMonitorRight() Action {
return ActionCustom("MoveColumnToMonitorRight", map[string]any{})
}
func ActionMoveColumnToMonitorDown() Action {
return ActionCustom("MoveColumnToMonitorDown", map[string]any{})
}
func ActionMoveColumnToMonitorUp() Action {
return ActionCustom("MoveColumnToMonitorUp", map[string]any{})
}
func ActionMoveColumnToMonitorPrevious() Action {
return ActionCustom("MoveColumnToMonitorPrevious", map[string]any{})
}
func ActionMoveColumnToMonitorNext() Action {
return ActionCustom("MoveColumnToMonitorNext", map[string]any{})
}
func ActionMoveColumnToMonitor(output OutputName) Action {
return ActionCustom("FocusMonitorNext", map[string]any{"output": output})
}
func ActionSetWindowWidth(id Option[WindowID], change SizeChange) Action {
return ActionCustom("SetWindowWidth", map[string]any{"id": id, "change": change})
}
func ActionSetWindowHeight(id Option[WindowID], change SizeChange) Action {
return ActionCustom("SetWindowHeight", map[string]any{"id": id, "change": change})
}
func ActionResetWindowHeight(id Option[WindowID]) Action {
return ActionCustom("ResetWindowHeight", map[string]any{"id": id})
}
func ActionSwitchPresetColumnWidth() Action {
return ActionCustom("SwitchPresetColumnWidth", map[string]any{})
}
func ActionSwitchPresetWindowWidth(id Option[WindowID]) Action {
return ActionCustom("SwitchPresetWindowWidth", map[string]any{"id": id})
}
func ActionSwitchPresetWindowHeight(id Option[WindowID]) Action {
return ActionCustom("SwitchPresetWindowHeight", map[string]any{"id": id})
}
func ActionMaximizeColumn() Action {
return ActionCustom("MaximizeColumn", map[string]any{})
}
func ActionSetColumnWidth(change SizeChange) Action {
return ActionCustom("SetColumnWidth", map[string]any{"change": change})
}
func ActionExpandColumnToAvailableWidth() Action {
return ActionCustom("ExpandColumnToAvailableWidth", map[string]any{})
}
func ActionSwitchLayout(layout LayoutSwitchTarget) Action {
return ActionCustom("SwitchLayout", map[string]any{"layout": layout})
}
func ActionShowHotkeyOverlay() Action {
return ActionCustom("ShowHotkeyOverlay", map[string]any{})
}
func ActionMoveWorkspaceToMonitorLeft() Action {
return ActionCustom("MoveWorkspaceToMonitorLeft", map[string]any{})
}
func ActionMoveWorkspaceToMonitorRight() Action {
return ActionCustom("MoveWorkspaceToMonitorRight", map[string]any{})
}
func ActionMoveWorkspaceToMonitorDown() Action {
return ActionCustom("MoveWorkspaceToMonitorDown", map[string]any{})
}
func ActionMoveWorkspaceToMonitorUp() Action {
return ActionCustom("MoveWorkspaceToMonitorUp", map[string]any{})
}
func ActionMoveWorkspaceToMonitorPrevious() Action {
return ActionCustom("MoveWorkspaceToMonitorPrevious", map[string]any{})
}
func ActionMoveWorkspaceToMonitorNext() Action {
return ActionCustom("MoveWorkspaceToMonitorNext", map[string]any{})
}
func ActionMoveWorkspaceToMonitor(output OutputName, reference Option[WorkspaceReferenceArg]) Action {
return ActionCustom("MoveWorkspaceToMonitor", map[string]any{"output": output, "reference": reference})
}
func ActionToggleDebugTint() Action {
return ActionCustom("ToggleDebugTint", map[string]any{})
}
func ActionDebugToggleOpaqueRegions() Action {
return ActionCustom("DebugToggleOpaqueRegions", map[string]any{})
}
func ActionDebugToggleDamage() Action {
return ActionCustom("DebugToggleDamage", map[string]any{})
}
func ActionToggleWindowFloating(id Option[WindowID]) Action {
return ActionCustom("ToggleWindowFloating", map[string]any{"id": id})
}
func ActionMoveWindowToFloating(id Option[WindowID]) Action {
return ActionCustom("MoveWindowToFloating", map[string]any{"id": id})
}
func ActionMoveWindowToTiling(id Option[WindowID]) Action {
return ActionCustom("MoveWindowToTiling", map[string]any{"id": id})
}
func ActionFocusFloating() Action {
return ActionCustom("FocusFloating", map[string]any{})
}
func ActionFocusTiling() Action {
return ActionCustom("FocusTiling", map[string]any{})
}
func ActionSwitchFocusBetweenFloatingAndTiling() Action {
return ActionCustom("SwitchFocusBetweenFloatingAndTiling", map[string]any{})
}
func ActionMoveFloatingWindow(id Option[WindowID], x PositionChange, y PositionChange) Action {
return ActionCustom("MoveFloatingWindow", map[string]any{"id": id, "x": x, "y": y})
}
func ActionToggleWindowRuleOpacity(id Option[WindowID]) Action {
return ActionCustom("ToggleWindowRuleOpacity", map[string]any{"id": id})
}
func ActionSetDynamicCastWindow(id Option[WindowID]) Action {
return ActionCustom("SetDynamicCastWindow", map[string]any{"id": id})
}
func ActionSetDynamicCastMonitor(output Option[OutputName]) Action {
return ActionCustom("SetDynamicCastMonitor", map[string]any{"output": output})
}
func ActionClearDynamicCastTarget() Action {
return ActionCustom("ClearDynamicCastTarget", map[string]any{})
}
func ActionToggleOverview() Action {
return ActionCustom("ToggleOverview", map[string]any{})
}
func ActionOpenOverview() Action {
return ActionCustom("OpenOverview", map[string]any{})
}
func ActionCloseOverview() Action {
return ActionCustom("CloseOverview", map[string]any{})
}
func ActionToggleWindowUrgent(id Option[WindowID]) Action {
return ActionCustom("ToggleWindowUrgent", map[string]any{"id": id})
}
func ActionSetWindowUrgent(id Option[WindowID]) Action {
return ActionCustom("SetWindowUrgent", map[string]any{"id": id})
}
func ActionUnsetWindowUrgent(id Option[WindowID]) Action {
return ActionCustom("UnsetWindowUrgent", map[string]any{"id": id})
}

109
model_client.go Normal file
View File

@ -0,0 +1,109 @@
package niri
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"git.tordarus.net/tordarus/slices"
"github.com/adrg/xdg"
)
var niriSocketFilenameFormat = "niri.wayland-%d.%d.sock"
var niriSocketFilenamePattern = regexp.MustCompile(`^niri\.wayland-(\d+?)\.(\d+?)\.sock$`)
type Client struct {
display int
pid int
}
var ErrClientNotFound = errors.New("client not found")
func GetClientByPID(pid int) (*Client, error) {
clients, err := GetAllClients()
if err != nil {
return nil, err
}
for _, c := range clients {
if c.pid == pid {
return c, nil
}
}
return nil, ErrClientNotFound
}
func GetClientByDisplay(display int) (*Client, error) {
clients, err := GetAllClients()
if err != nil {
return nil, err
}
for _, c := range clients {
if c.display == display {
return c, nil
}
}
return nil, ErrClientNotFound
}
func GetDefaultClient() (*Client, error) {
if niriSocket, ok := os.LookupEnv("NIRI_SOCKET"); ok {
return GetClientBySocketPath(niriSocket)
}
if display, ok := os.LookupEnv("WAYLAND_DISPLAY"); ok {
displayInt, err := strconv.Atoi(display)
if err != nil {
return nil, err
}
return GetClientByDisplay(displayInt)
}
return nil, ErrClientNotFound
}
func GetAllClients() ([]*Client, error) {
runtimeDir := xdg.RuntimeDir
entries, err := os.ReadDir(runtimeDir)
if err != nil {
return nil, err
}
return slices.Filter(slices.Map(entries, parseEntryAsClient), notNull), nil
}
func GetClientBySocketPath(socketPath string) (*Client, error) {
baseName := filepath.Base(socketPath)
matches := niriSocketFilenamePattern.FindStringSubmatch(baseName)
if matches == nil {
return nil, errors.New("invalid niri socket path")
}
display, err := strconv.Atoi(matches[1])
if err != nil {
return nil, err
}
pid, err := strconv.Atoi(matches[2])
if err != nil {
return nil, err
}
return &Client{
display: display,
pid: pid,
}, nil
}
func (c Client) GetSocketPath() string {
return filepath.Join(xdg.RuntimeDir, fmt.Sprintf(niriSocketFilenameFormat, c.display, c.pid))
}

19
model_color.go Normal file
View File

@ -0,0 +1,19 @@
package niri
import (
"image/color"
"math"
)
type niriColor struct {
RGB [3]float64 `json:"rgb"`
}
func (c niriColor) AsColor() color.Color {
return color.NRGBA64{
R: uint16(c.RGB[0] * math.MaxUint16),
G: uint16(c.RGB[1] * math.MaxUint16),
B: uint16(c.RGB[2] * math.MaxUint16),
A: math.MaxUint16,
}
}

8
model_column_display.go Normal file
View File

@ -0,0 +1,8 @@
package niri
type ColumnDisplay string
const (
ColumnDisplayNormal ColumnDisplay = "Normal"
ColumnDisplayTabbed ColumnDisplay = "Tabbed"
)

198
model_event.go Normal file
View File

@ -0,0 +1,198 @@
package niri
import "encoding/json"
type Event interface {
private()
}
type eventContainer struct {
EventWorkspacesChanged *EventWorkspacesChanged `json:"WorkspacesChanged"`
EventWorkspaceUrgencyChanged *EventWorkspaceUrgencyChanged `json:"WorkspaceUrgencyChanged"`
EventWorkspaceActivated *EventWorkspaceActivated `json:"WorkspaceActivated"`
EventWorkspaceActiveWindowChanged *EventWorkspaceActiveWindowChanged `json:"WorkspaceActiveWindowChanged"`
EventWindowsChanged *EventWindowsChanged `json:"WindowsChanged"`
EventWindowOpenedOrChanged *EventWindowOpenedOrChanged `json:"WindowOpenedOrChanged"`
EventWindowClosed *EventWindowClosed `json:"WindowClosed"`
EventWindowFocusChanged *EventWindowFocusChanged `json:"WindowFocusChanged"`
EventWindowUrgencyChanged *EventWindowUrgencyChanged `json:"WindowUrgencyChanged"`
EventKeyboardLayoutsChanged *EventKeyboardLayoutsChanged `json:"KeyboardLayoutsChanged"`
EventKeyboardLayoutSwitched *EventKeyboardLayoutSwitched `json:"KeyboardLayoutSwitched"`
EventOverviewOpenedOrClosed *EventOverviewOpenedOrClosed `json:"OverviewOpenedOrClosed"`
}
func (c *eventContainer) event() Event {
if c.EventWorkspacesChanged != nil {
return c.EventWorkspacesChanged
} else if c.EventWorkspaceUrgencyChanged != nil {
return c.EventWorkspaceUrgencyChanged
} else if c.EventWorkspaceActivated != nil {
return c.EventWorkspaceActivated
} else if c.EventWorkspaceActiveWindowChanged != nil {
return c.EventWorkspaceActiveWindowChanged
} else if c.EventWindowsChanged != nil {
return c.EventWindowsChanged
} else if c.EventWindowOpenedOrChanged != nil {
return c.EventWindowOpenedOrChanged
} else if c.EventWindowClosed != nil {
return c.EventWindowClosed
} else if c.EventWindowFocusChanged != nil {
return c.EventWindowFocusChanged
} else if c.EventWindowUrgencyChanged != nil {
return c.EventWindowUrgencyChanged
} else if c.EventKeyboardLayoutsChanged != nil {
return c.EventKeyboardLayoutsChanged
} else if c.EventKeyboardLayoutSwitched != nil {
return c.EventKeyboardLayoutSwitched
} else if c.EventOverviewOpenedOrClosed != nil {
return c.EventOverviewOpenedOrClosed
}
return &EventInvalid{}
}
type EventInvalid struct {
}
func (m *EventInvalid) private() {}
func (m *EventInvalid) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventWorkspacesChanged struct {
Workspaces []Workspace `json:"workspaces"`
}
func (m *EventWorkspacesChanged) private() {}
func (m *EventWorkspacesChanged) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventWorkspaceUrgencyChanged struct {
ID WorkspaceID `json:"id"`
Urgent bool `json:"urgent"`
}
func (m *EventWorkspaceUrgencyChanged) private() {}
func (m *EventWorkspaceUrgencyChanged) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventWorkspaceActivated struct {
ID WorkspaceID `json:"id"`
Focused bool `json:"focused"`
}
func (m *EventWorkspaceActivated) private() {}
func (m *EventWorkspaceActivated) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventWorkspaceActiveWindowChanged struct {
WorkspaceID WorkspaceID `json:"workspace_id"`
ActiveWindowID Option[WindowID] `json:"active_window_id"`
}
func (m *EventWorkspaceActiveWindowChanged) private() {}
func (m *EventWorkspaceActiveWindowChanged) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventWindowsChanged struct {
Windows []Window `json:"windows"`
}
func (m *EventWindowsChanged) private() {}
func (m *EventWindowsChanged) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventWindowOpenedOrChanged struct {
Window Window `json:"window"`
}
func (m *EventWindowOpenedOrChanged) private() {}
func (m *EventWindowOpenedOrChanged) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventWindowClosed struct {
ID WindowID `json:"id"`
}
func (m *EventWindowClosed) private() {}
func (m *EventWindowClosed) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventWindowFocusChanged struct {
ID Option[WindowID] `json:"id"`
}
func (m *EventWindowFocusChanged) private() {}
func (m *EventWindowFocusChanged) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventWindowUrgencyChanged struct {
ID WindowID `json:"id"`
Urgent bool `json:"urgent"`
}
func (m *EventWindowUrgencyChanged) private() {}
func (m *EventWindowUrgencyChanged) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventKeyboardLayoutsChanged struct {
KeyboardLayouts KeyboardLayouts `json:"keyboard_layouts"`
}
func (m *EventKeyboardLayoutsChanged) private() {}
func (m *EventKeyboardLayoutsChanged) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventKeyboardLayoutSwitched struct {
Index int `json:"idx"`
}
func (m *EventKeyboardLayoutSwitched) private() {}
func (m *EventKeyboardLayoutSwitched) String() string {
data, _ := json.Marshal(m)
return string(data)
}
type EventOverviewOpenedOrClosed struct {
IsOpen bool `json:"is_open"`
}
func (m *EventOverviewOpenedOrClosed) private() {}
func (m *EventOverviewOpenedOrClosed) String() string {
data, _ := json.Marshal(m)
return string(data)
}

13
model_keyboard_layout.go Normal file
View File

@ -0,0 +1,13 @@
package niri
import "encoding/json"
type KeyboardLayouts struct {
Names []string `json:"names"`
CurrentIndex int `json:"current_idx"`
}
func (m KeyboardLayouts) String() string {
data, _ := json.MarshalIndent(m, "", "\t")
return string(data)
}

32
model_layersurface.go Normal file
View File

@ -0,0 +1,32 @@
package niri
import "encoding/json"
type LayerSurface struct {
Namespace string `json:"namespace"`
Output OutputName `json:"output"`
Layer Layer `json:"layer"`
KeyboardInteractivity LayerSurfaceKeyboardInteractivity `json:"keyboard_interactivity"`
}
func (m LayerSurface) String() string {
data, _ := json.MarshalIndent(m, "", "\t")
return string(data)
}
type Layer string
const (
LayerBackground Layer = "Background"
LayerBottom Layer = "Bottom"
LayerTop Layer = "Top"
LayerOverlay Layer = "Overlay"
)
type LayerSurfaceKeyboardInteractivity string
const (
KeyboardInteractivityNone LayerSurfaceKeyboardInteractivity = "None"
KeyboardInteractivityExclusive LayerSurfaceKeyboardInteractivity = "Exclusive"
KeyboardInteractivityOnDemand LayerSurfaceKeyboardInteractivity = "OnDemand"
)

View File

@ -0,0 +1,36 @@
package niri
import (
"encoding/json"
"errors"
)
type LayoutSwitchTarget struct {
prev Option[bool]
next Option[bool]
index Option[int]
}
func LayoutSwitchTargetPrev() LayoutSwitchTarget {
return LayoutSwitchTarget{prev: OptionOf(true)}
}
func LayoutSwitchTargetNext() LayoutSwitchTarget {
return LayoutSwitchTarget{next: OptionOf(true)}
}
func LayoutSwitchTargetIndex(index int) LayoutSwitchTarget {
return LayoutSwitchTarget{index: OptionOf(index)}
}
func (m LayoutSwitchTarget) MarshalJSON() ([]byte, error) {
if m.prev != nil {
return json.Marshal("Prev")
} else if m.next != nil {
return json.Marshal("Next")
} else if m.index != nil {
return json.Marshal(map[string]int{"Index": *m.index})
}
return nil, errors.New("LayoutSwitchTarget.MarshalJson()")
}

18
model_option.go Normal file
View File

@ -0,0 +1,18 @@
package niri
type Option[T any] = *T
func OptionOf[T any](v T) Option[T] {
return &v
}
func OptionEmpty[T any]() Option[T] {
return nil
}
func mapOption[T any, R any](o Option[T], mapper func(T) R) Option[R] {
if o == nil {
return nil
}
return OptionOf(mapper(*o))
}

70
model_output.go Normal file
View File

@ -0,0 +1,70 @@
package niri
import "encoding/json"
type OutputName string
type Output struct {
Name OutputName `json:"name"`
Make string `json:"make"`
Model string `json:"model"`
Serial string `json:"serial"`
PhysicalSize [2]int `json:"physical_size"`
Modes []OutputMode `json:"modes"`
CurrentMode int `json:"current_mode"`
VrrSupported bool `json:"vrr_supported"`
VrrEnabled bool `json:"vrr_enabled"`
Logical OutputLogical `json:"logical"`
}
func (m Output) String() string {
data, _ := json.MarshalIndent(m, "", "\t")
return string(data)
}
type OutputMode struct {
Width int `json:"width"`
Height int `json:"height"`
RefreshRate int `json:"refresh_rate"`
IsPreferred bool `json:"is_preferred"`
}
func (m OutputMode) Configured() ConfiguredMode {
return ConfiguredMode{
Width: m.Width,
Height: m.Height,
Refresh: OptionOf(float64(m.RefreshRate) / 100),
}
}
func (m OutputMode) String() string {
data, _ := json.MarshalIndent(m, "", "\t")
return string(data)
}
type OutputLogical struct {
X int `json:"x"`
Y int `json:"y"`
Width int `json:"width"`
Height int `json:"height"`
Scale float64 `json:"scale"`
Transform OutputTransform `json:"transform"`
}
func (m OutputLogical) String() string {
data, _ := json.MarshalIndent(m, "", "\t")
return string(data)
}
type OutputTransform string
const (
TransformNormal OutputTransform = "Normal"
Transform90 OutputTransform = "90"
Transform180 OutputTransform = "180"
Transform270 OutputTransform = "270"
TransformFlipped OutputTransform = "Flipped"
TransformFlipped90 OutputTransform = "Flipped90"
TransformFlipped180 OutputTransform = "Flipped180"
TransformFlipped270 OutputTransform = "Flipped270"
)

87
model_output_action.go Normal file
View File

@ -0,0 +1,87 @@
package niri
import (
"encoding/json"
"errors"
)
type OutputActionRequest struct {
Output OutputName `json:"output"`
Action OutputAction `json:"action"`
}
type outputActionResponse struct {
OutputConfigChanged string `json:"OutputConfigChanged"`
}
type OutputAction struct {
off Option[bool]
on Option[bool]
mode Option[ModeToSet]
scale Option[ScaleToSet]
transform Option[OutputTransform]
position Option[PositionToSet]
vrr Option[ConfiguredVrr]
}
func OutputActionOff(output string) OutputActionRequest {
return OutputActionRequest{
Output: OutputName(output),
Action: OutputAction{off: OptionOf(true)},
}
}
func OutputActionOn(output string) OutputActionRequest {
return OutputActionRequest{
Output: OutputName(output),
Action: OutputAction{on: OptionOf(true)},
}
}
func OutputActionMode(output string, mode ModeToSet) OutputActionRequest {
return OutputActionRequest{
Output: OutputName(output),
Action: OutputAction{mode: OptionOf(mode)},
}
}
func OutputActionScale(output string, scale ScaleToSet) OutputActionRequest {
return OutputActionRequest{
Output: OutputName(output),
Action: OutputAction{scale: OptionOf(scale)},
}
}
func OutputActionTransform(output string, transform OutputTransform) OutputActionRequest {
return OutputActionRequest{
Output: OutputName(output),
Action: OutputAction{transform: OptionOf(transform)},
}
}
func OutputActionVrr(output string, vrr ConfiguredVrr) OutputActionRequest {
return OutputActionRequest{
Output: OutputName(output),
Action: OutputAction{vrr: OptionOf(vrr)},
}
}
func (m OutputAction) MarshalJSON() ([]byte, error) {
if m.off != nil {
return json.Marshal("Off")
} else if m.on != nil {
return json.Marshal("On")
} else if m.mode != nil {
return json.Marshal(map[string]map[string]ModeToSet{"Mode": {"mode": *m.mode}})
} else if m.scale != nil {
return json.Marshal(map[string]map[string]ScaleToSet{"Scale": {"scale": *m.scale}})
} else if m.transform != nil {
return json.Marshal(map[string]map[string]OutputTransform{"Transform": {"transform": *m.transform}})
} else if m.position != nil {
return json.Marshal(map[string]map[string]PositionToSet{"Position": {"position": *m.position}})
} else if m.vrr != nil {
return json.Marshal(map[string]map[string]ConfiguredVrr{"Vrr": {"vrr": *m.vrr}})
}
return nil, errors.New("OutputAction.MarshalJson()")
}

View File

@ -0,0 +1,17 @@
package niri
type ConfiguredMode struct {
Width int `json:"width"`
Height int `json:"height"`
Refresh Option[float64] `json:"refresh,omitempty"`
}
type ConfiguredPosition struct {
X int `json:"x"`
Y int `json:"y"`
}
type ConfiguredVrr struct {
Vrr bool `json:"vrr"`
OnDemand bool `json:"on_demand"`
}

75
model_output_toset.go Normal file
View File

@ -0,0 +1,75 @@
package niri
import (
json "encoding/json"
"errors"
)
type ModeToSet struct {
automatic Option[bool]
specific Option[ConfiguredMode]
}
func OutputActionModeAutomatic() ModeToSet {
return ModeToSet{automatic: OptionOf(true)}
}
func OutputActionModeSpecific(mode ConfiguredMode) ModeToSet {
return ModeToSet{specific: OptionOf(mode)}
}
func (m ModeToSet) MarshalJSON() ([]byte, error) {
if m.automatic != nil {
return json.Marshal("Automatic")
} else if m.specific != nil {
return json.Marshal(map[string]any{"Specific": m.specific})
}
return nil, errors.New("ModeToSet.MarshalJson()")
}
type ScaleToSet struct {
automatic Option[bool]
specific Option[float64]
}
func OutputActionScaleAutomatic() ScaleToSet {
return ScaleToSet{automatic: OptionOf(true)}
}
func OutputActionScaleSpecific(scale float64) ScaleToSet {
return ScaleToSet{specific: OptionOf(scale)}
}
func (m ScaleToSet) MarshalJSON() ([]byte, error) {
if m.automatic != nil {
return json.Marshal("Automatic")
} else if m.specific != nil {
return json.Marshal(map[string]any{"Specific": m.specific})
}
return nil, errors.New("ScaleToSet.MarshalJson()")
}
type PositionToSet struct {
automatic Option[bool]
specific Option[ConfiguredPosition]
}
func OutputActionPositionAutomatic() *PositionToSet {
return &PositionToSet{automatic: OptionOf(true)}
}
func OutputActionPositionSpecific(position ConfiguredPosition) *PositionToSet {
return &PositionToSet{specific: OptionOf(position)}
}
func (m PositionToSet) MarshalJSON() ([]byte, error) {
if m.automatic != nil {
return json.Marshal("Automatic")
} else if m.specific != nil {
return json.Marshal(m.specific)
}
return nil, errors.New("PositionToSet.MarshalJson()")
}

12
model_overview_state.go Normal file
View File

@ -0,0 +1,12 @@
package niri
import "encoding/json"
type overviewState struct {
IsOpen bool `json:"is_open"`
}
func (m overviewState) String() string {
data, _ := json.MarshalIndent(m, "", "\t")
return string(data)
}

61
model_position_change.go Normal file
View File

@ -0,0 +1,61 @@
package niri
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
)
type PositionChange struct {
SetFixed Option[int] `json:"SetFixed,omitempty"`
AdjustFixed Option[int] `json:"AdjustFixed,omitempty"`
}
func (m PositionChange) String() string {
data, _ := json.MarshalIndent(m, "", "\t")
return string(data)
}
func PositionSetFixed(position int) PositionChange {
return PositionChange{SetFixed: &position}
}
func PositionAdjustFixed(position int) PositionChange {
return PositionChange{AdjustFixed: &position}
}
const positionPatternString = `^(\+|-)?(\d+?)$`
const positionPatternGroupAdjust = 1
const positionPatternGroupIntValue = 2
var positionPattern = regexp.MustCompile(positionPatternString)
func ParsePosition(str string) (PositionChange, error) {
matches := positionPattern.FindStringSubmatch(str)
if matches == nil {
return PositionChange{}, fmt.Errorf("invalid position spec '%s'. Expected pattern: %s", str, positionPatternString)
}
adjust := matches[positionPatternGroupAdjust] != ""
negative := matches[positionPatternGroupAdjust] == "-"
factor := 1
if negative {
factor = -1
}
intValue, err := strconv.ParseInt(matches[positionPatternGroupIntValue], 10, 64)
if err != nil {
return PositionChange{}, fmt.Errorf("invalid position spec '%s'. Expected pattern: %s", str, positionPatternString)
}
switch true {
case adjust:
return PositionAdjustFixed(int(intValue) * factor), nil
case !adjust:
return PositionSetFixed(int(intValue) * factor), nil
default:
return PositionChange{}, fmt.Errorf("invalid position spec '%s'. Expected pattern: %s", str, positionPatternString)
}
}

11
model_response.go Normal file
View File

@ -0,0 +1,11 @@
package niri
type Response[T any] struct {
OK *T `json:"Ok"`
Err *string `json:"Err"`
}
func errResponse[T any](err error) Response[T] {
errStr := err.Error()
return Response[T]{Err: &errStr}
}

83
model_size_change.go Normal file
View File

@ -0,0 +1,83 @@
package niri
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
)
type SizeChange struct {
SetFixed *int `json:"SetFixed,omitempty"`
SetProportion *float64 `json:"SetProportion,omitempty"`
AdjustFixed *int `json:"AdjustFixed,omitempty"`
AdjustProportion *float64 `json:"AdjustProportion,omitempty"`
}
func (m SizeChange) String() string {
data, _ := json.MarshalIndent(m, "", "\t")
return string(data)
}
func SizeSetFixed(size int) SizeChange {
return SizeChange{SetFixed: &size}
}
func SizeSetProportion(size float64) SizeChange {
return SizeChange{SetProportion: &size}
}
func SizeAdjustFixed(size int) SizeChange {
return SizeChange{AdjustFixed: &size}
}
func SizeAdjustProportion(size float64) SizeChange {
return SizeChange{AdjustProportion: &size}
}
const sizePatternString = `^(\+|-)?((\d+?)(?:\.\d+)?)(%)?$`
const sizePatternGroupAdjust = 1
const sizePatternGroupFloatValue = 2
const sizePatternGroupIntValue = 3
const sizePatternGroupPercentage = 4
var sizePattern = regexp.MustCompile(sizePatternString)
func ParseSize(str string) (SizeChange, error) {
matches := sizePattern.FindStringSubmatch(str)
if matches == nil {
return SizeChange{}, fmt.Errorf("invalid size spec '%s'. Expected pattern: %s", str, sizePatternString)
}
adjust := matches[sizePatternGroupAdjust] != ""
proportion := matches[sizePatternGroupPercentage] == "%"
negative := matches[sizePatternGroupAdjust] == "-"
factor := 1
if negative {
factor = -1
}
floatValue, err := strconv.ParseFloat(matches[sizePatternGroupFloatValue], 64)
if err != nil {
return SizeChange{}, fmt.Errorf("invalid size spec '%s'. Expected pattern: %s", str, sizePatternString)
}
intValue, err := strconv.ParseInt(matches[sizePatternGroupIntValue], 10, 64)
if err != nil {
return SizeChange{}, fmt.Errorf("invalid size spec '%s'. Expected pattern: %s", str, sizePatternString)
}
switch true {
case adjust && proportion:
return SizeAdjustProportion(floatValue * float64(factor)), nil
case adjust && !proportion:
return SizeAdjustFixed(int(intValue) * factor), nil
case !adjust && proportion:
return SizeSetProportion(floatValue * float64(factor)), nil
case !adjust && !proportion:
return SizeSetFixed(int(intValue) * factor), nil
default:
return SizeChange{}, fmt.Errorf("invalid size spec '%s'. Expected pattern: %s", str, sizePatternString)
}
}

21
model_window.go Normal file
View File

@ -0,0 +1,21 @@
package niri
import "encoding/json"
type WindowID int
type Window struct {
ID WindowID `json:"id"`
Title string `json:"title"`
AppID string `json:"app_id"`
PID int `json:"pid"`
WorkspaceID WorkspaceID `json:"workspace_id"`
Urgent bool `json:"is_urgent"`
Floating bool `json:"is_floating"`
Focused bool `json:"is_focused"`
}
func (m Window) String() string {
data, _ := json.MarshalIndent(m, "", "\t")
return string(data)
}

20
model_workspace.go Normal file
View File

@ -0,0 +1,20 @@
package niri
import "encoding/json"
type WorkspaceID int
type Workspace struct {
ID WorkspaceID `json:"id"`
Name string `json:"name"`
Output string `json:"output"`
Urgent bool `json:"is_urgent"`
Active bool `json:"is_active"`
Focused bool `json:"is_focused"`
ActiveWindowID WindowID `json:"active_window_id"`
}
func (m Workspace) String() string {
data, _ := json.MarshalIndent(m, "", "\t")
return string(data)
}

View File

@ -0,0 +1,19 @@
package niri
type WorkspaceReferenceArg struct {
ID *WorkspaceID `json:"Id,omitempty"`
Index *int `json:"Index,omitempty"`
Name *string `json:"Name,omitempty"`
}
func WorkspaceByID(id WorkspaceID) WorkspaceReferenceArg {
return WorkspaceReferenceArg{ID: &id}
}
func WorkspaceByIndex(index int) WorkspaceReferenceArg {
return WorkspaceReferenceArg{Index: &index}
}
func WorkspaceByName(name string) WorkspaceReferenceArg {
return WorkspaceReferenceArg{Name: &name}
}

113
utils.go Normal file
View File

@ -0,0 +1,113 @@
package niri
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net"
"os"
)
func notNull(c *Client) bool {
return c != nil
}
func parseEntryAsClient(entry os.DirEntry) *Client {
if entry.IsDir() {
return nil
}
client, err := GetClientBySocketPath(entry.Name())
if err != nil {
return nil
}
return client
}
func readSocket[T any](socket string, body io.Reader) Response[T] {
r, err := readSocketRaw(socket, body)
if err != nil {
return errResponse[T](err)
}
defer r.Close()
value := new(Response[T])
if err := json.NewDecoder(r).Decode(value); err != nil {
return errResponse[T](err)
}
return *value
}
func subscribeSocket[T any](ctx context.Context, socket string, body io.Reader) (<-chan T, error) {
ch, err := readSocketGeneric[T](ctx, socket, body)
if err != nil {
return nil, err
}
out := make(chan T, 10)
go func() {
defer close(out)
for {
select {
case event := <-ch:
out <- event
case <-ctx.Done():
return
}
}
}()
return out, nil
}
func readSocketGeneric[T any](ctx context.Context, socket string, body io.Reader) (<-chan T, error) {
r, err := readSocketRaw(socket, body)
if err != nil {
return nil, err
}
out := make(chan T, 10)
go func() {
defer close(out)
defer r.Close()
for ctx.Err() == nil {
value := new(T)
if err := json.NewDecoder(r).Decode(value); err != nil {
if errors.Is(err, io.EOF) {
return
} else {
continue
}
}
out <- *value
}
}()
return out, nil
}
func readSocketRaw(socket string, body io.Reader) (io.ReadCloser, error) {
conn, err := net.Dial("unix", socket)
if err != nil {
return nil, err
}
if _, err := io.Copy(conn, body); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
func asJsonReader(v any) io.Reader {
data, _ := json.Marshal(v)
return bytes.NewReader(append(data, '\n'))
}