From 840de6290215cbbe22beb45e6814557872e779bf Mon Sep 17 00:00:00 2001 From: Tordarus Date: Fri, 25 Jul 2025 13:41:23 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + do.go | 52 +++ event_stream.go | 15 + getters.go | 103 +++++ go.mod | 14 + go.sum | 18 + model_action.go | 547 +++++++++++++++++++++++ model_client.go | 109 +++++ model_color.go | 19 + model_column_display.go | 8 + model_event.go | 198 ++++++++ model_keyboard_layout.go | 13 + model_layersurface.go | 32 ++ model_layout_switch_target.go | 36 ++ model_option.go | 18 + model_output.go | 70 +++ model_output_action.go | 87 ++++ model_output_action_configured_models.go | 17 + model_output_toset.go | 75 ++++ model_overview_state.go | 12 + model_position_change.go | 61 +++ model_response.go | 11 + model_size_change.go | 83 ++++ model_window.go | 21 + model_workspace.go | 20 + model_workspace_reference.go | 19 + utils.go | 113 +++++ 27 files changed, 1772 insertions(+) create mode 100644 .gitignore create mode 100644 do.go create mode 100644 event_stream.go create mode 100644 getters.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 model_action.go create mode 100644 model_client.go create mode 100644 model_color.go create mode 100644 model_column_display.go create mode 100644 model_event.go create mode 100644 model_keyboard_layout.go create mode 100644 model_layersurface.go create mode 100644 model_layout_switch_target.go create mode 100644 model_option.go create mode 100644 model_output.go create mode 100644 model_output_action.go create mode 100644 model_output_action_configured_models.go create mode 100644 model_output_toset.go create mode 100644 model_overview_state.go create mode 100644 model_position_change.go create mode 100644 model_response.go create mode 100644 model_size_change.go create mode 100644 model_window.go create mode 100644 model_workspace.go create mode 100644 model_workspace_reference.go create mode 100644 utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b97602b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*_test.go \ No newline at end of file diff --git a/do.go b/do.go new file mode 100644 index 0000000..6c678d6 --- /dev/null +++ b/do.go @@ -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 +} diff --git a/event_stream.go b/event_stream.go new file mode 100644 index 0000000..976a398 --- /dev/null +++ b/event_stream.go @@ -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 +} diff --git a/getters.go b/getters.go new file mode 100644 index 0000000..187d12e --- /dev/null +++ b/getters.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2471c34 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4f83b27 --- /dev/null +++ b/go.sum @@ -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= diff --git a/model_action.go b/model_action.go new file mode 100644 index 0000000..9ed7d54 --- /dev/null +++ b/model_action.go @@ -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}) +} diff --git a/model_client.go b/model_client.go new file mode 100644 index 0000000..238d378 --- /dev/null +++ b/model_client.go @@ -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)) +} diff --git a/model_color.go b/model_color.go new file mode 100644 index 0000000..e6040ad --- /dev/null +++ b/model_color.go @@ -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, + } +} diff --git a/model_column_display.go b/model_column_display.go new file mode 100644 index 0000000..1ca80e0 --- /dev/null +++ b/model_column_display.go @@ -0,0 +1,8 @@ +package niri + +type ColumnDisplay string + +const ( + ColumnDisplayNormal ColumnDisplay = "Normal" + ColumnDisplayTabbed ColumnDisplay = "Tabbed" +) diff --git a/model_event.go b/model_event.go new file mode 100644 index 0000000..41b7ea4 --- /dev/null +++ b/model_event.go @@ -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) +} diff --git a/model_keyboard_layout.go b/model_keyboard_layout.go new file mode 100644 index 0000000..ca9a94e --- /dev/null +++ b/model_keyboard_layout.go @@ -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) +} diff --git a/model_layersurface.go b/model_layersurface.go new file mode 100644 index 0000000..8ba8a20 --- /dev/null +++ b/model_layersurface.go @@ -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" +) diff --git a/model_layout_switch_target.go b/model_layout_switch_target.go new file mode 100644 index 0000000..3ced64a --- /dev/null +++ b/model_layout_switch_target.go @@ -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()") +} diff --git a/model_option.go b/model_option.go new file mode 100644 index 0000000..1531974 --- /dev/null +++ b/model_option.go @@ -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)) +} diff --git a/model_output.go b/model_output.go new file mode 100644 index 0000000..0939a1e --- /dev/null +++ b/model_output.go @@ -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" +) diff --git a/model_output_action.go b/model_output_action.go new file mode 100644 index 0000000..788d818 --- /dev/null +++ b/model_output_action.go @@ -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()") +} diff --git a/model_output_action_configured_models.go b/model_output_action_configured_models.go new file mode 100644 index 0000000..8b38ecd --- /dev/null +++ b/model_output_action_configured_models.go @@ -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"` +} diff --git a/model_output_toset.go b/model_output_toset.go new file mode 100644 index 0000000..510759d --- /dev/null +++ b/model_output_toset.go @@ -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()") +} diff --git a/model_overview_state.go b/model_overview_state.go new file mode 100644 index 0000000..eceb8a9 --- /dev/null +++ b/model_overview_state.go @@ -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) +} diff --git a/model_position_change.go b/model_position_change.go new file mode 100644 index 0000000..34a9d92 --- /dev/null +++ b/model_position_change.go @@ -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) + } +} diff --git a/model_response.go b/model_response.go new file mode 100644 index 0000000..9a012c4 --- /dev/null +++ b/model_response.go @@ -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} +} diff --git a/model_size_change.go b/model_size_change.go new file mode 100644 index 0000000..66b1f4f --- /dev/null +++ b/model_size_change.go @@ -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) + } +} diff --git a/model_window.go b/model_window.go new file mode 100644 index 0000000..84a82de --- /dev/null +++ b/model_window.go @@ -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) +} diff --git a/model_workspace.go b/model_workspace.go new file mode 100644 index 0000000..af5c256 --- /dev/null +++ b/model_workspace.go @@ -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) +} diff --git a/model_workspace_reference.go b/model_workspace_reference.go new file mode 100644 index 0000000..c2c302c --- /dev/null +++ b/model_workspace_reference.go @@ -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} +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..d63cc90 --- /dev/null +++ b/utils.go @@ -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')) +}