diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da58f3..582ec99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [0.15.0] - 2026-01-26 + +### Added + +- new subcommands added to input, see [InputCmd](https://github.com/onyx-and-iris/gobs-cli?tab=readme-ov-file#inputcmd) + # [0.14.1] - 2025-07-14 ### Added diff --git a/README.md b/README.md index fce67de..4c8b9fa 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,20 @@ gobs-cli group status START "test_group" ### InputCmd +- create: Create input. + - args: Name Kind + +```console +gobs-cli input create 'stream mix' 'wasapi_input_capture' +``` + +- remove: Remove input. + - args: Name + +```console +gobs-cli input remove 'stream mix' +``` + - list: List all inputs. - flags: @@ -281,6 +295,12 @@ gobs-cli input list gobs-cli input list --input --colour ``` +- list-kinds: List input kinds. + +```console +gobs-cli input list-kinds +``` + - mute: Mute input. - args: InputName @@ -302,6 +322,34 @@ gobs-cli input unmute "Mic/Aux" gobs-cli input toggle "Mic/Aux" ``` +- volume: Set input volume. + - args: InputName Volume + +```console +gobs-cli input volume -- 'Mic/Aux' -30.6 +``` + +- show: Show input details. + - args: Name + - flags: + + *optional* + - --verbose: List all available input devices. + +- update: Update input settings. + - args: InputName DeviceName + +```console +gobs-cli input update 'Mic/Aux' 'Voicemeeter Out B1 (VB-Audio Voicemeeter VAIO)' +``` + +- kind-defaults: Get default settings for an input kind. + - args: Kind + +```console +gobs-cli input kind-defaults 'wasapi_input_capture' +``` + ### TextCmd - current: Display current text for a text input. diff --git a/input.go b/input.go index 9761068..977fc26 100644 --- a/input.go +++ b/input.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "maps" "sort" "strings" @@ -13,10 +14,63 @@ import ( // InputCmd provides commands to manage inputs in OBS Studio. type InputCmd struct { - List InputListCmd `cmd:"" help:"List all inputs." aliases:"ls"` - Mute InputMuteCmd `cmd:"" help:"Mute input." aliases:"m"` - Unmute InputUnmuteCmd `cmd:"" help:"Unmute input." aliases:"um"` - Toggle InputToggleCmd `cmd:"" help:"Toggle input." aliases:"tg"` + Create InputCreateCmd `cmd:"" help:"Create input." aliases:"c"` + Remove InputRemoveCmd `cmd:"" help:"Remove input." aliases:"d"` + List InputListCmd `cmd:"" help:"List all inputs." aliases:"ls"` + ListKinds InputListKindsCmd `cmd:"" help:"List input kinds." aliases:"k"` + Mute InputMuteCmd `cmd:"" help:"Mute input." aliases:"m"` + Unmute InputUnmuteCmd `cmd:"" help:"Unmute input." aliases:"um"` + Toggle InputToggleCmd `cmd:"" help:"Toggle input." aliases:"tg"` + Volume InputVolumeCmd `cmd:"" help:"Set input volume." aliases:"v"` + Show InputShowCmd `cmd:"" help:"Show input details." aliases:"s"` + Update InputUpdateCmd `cmd:"" help:"Update input settings." aliases:"up"` + KindDefaults InputKindDefaultsCmd `cmd:"" help:"Get default settings for an input kind." aliases:"df"` +} + +// InputCreateCmd provides a command to create an input. +type InputCreateCmd struct { + Name string `arg:"" help:"Name for the input." required:""` + Kind string `arg:"" help:"Input kind (e.g., coreaudio_input_capture, macos-avcapture)." required:""` +} + +// Run executes the command to create an input. +func (cmd *InputCreateCmd) Run(ctx *context) error { + currentScene, err := ctx.Client.Scenes.GetCurrentProgramScene() + if err != nil { + return err + } + + _, err = ctx.Client.Inputs.CreateInput( + inputs.NewCreateInputParams(). + WithInputKind(cmd.Kind). + WithInputName(cmd.Name). + WithSceneName(currentScene.CurrentProgramSceneName), + ) + if err != nil { + return err + } + + fmt.Fprintf(ctx.Out, "Created input: %s (%s) in scene %s\n", + ctx.Style.Highlight(cmd.Name), cmd.Kind, ctx.Style.Highlight(currentScene.CurrentProgramSceneName)) + return nil +} + +// InputRemoveCmd provides a command to remove an input. +type InputRemoveCmd struct { + Name string `arg:"" help:"Name of the input to remove." required:""` +} + +// Run executes the command to remove an input. +func (cmd *InputRemoveCmd) Run(ctx *context) error { + _, err := ctx.Client.Inputs.RemoveInput( + inputs.NewRemoveInputParams().WithInputName(cmd.Name), + ) + if err != nil { + return fmt.Errorf("failed to delete input: %w", err) + } + + fmt.Fprintf(ctx.Out, "Deleted %s\n", ctx.Style.Highlight(cmd.Name)) + return nil } // InputListCmd provides a command to list all inputs. @@ -122,6 +176,47 @@ func (cmd *InputListCmd) Run(ctx *context) error { return nil } +// InputListKindsCmd provides a command to list all input kinds. +type InputListKindsCmd struct{} + +// Run executes the command to list all input kinds. +func (cmd *InputListKindsCmd) Run(ctx *context) error { + resp, err := ctx.Client.Inputs.GetInputKindList( + inputs.NewGetInputKindListParams().WithUnversioned(false), + ) + if err != nil { + return fmt.Errorf("failed to get input kinds: %w", err) + } + + t := table.New().Border(lipgloss.RoundedBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border)) + t.Headers("Kind") + t.StyleFunc(func(row, col int) lipgloss.Style { + style := lipgloss.NewStyle().Padding(0, 3) + switch col { + case 0: + style = style.Align(lipgloss.Left) + } + switch { + case row == table.HeaderRow: + style = style.Bold(true).Align(lipgloss.Center) + case row%2 == 0: + style = style.Foreground(ctx.Style.evenRows) + default: + style = style.Foreground(ctx.Style.oddRows) + } + return style + }) + + for _, kind := range resp.InputKinds { + t.Row(kind) + } + + fmt.Fprintln(ctx.Out, t.Render()) + + return nil +} + // InputMuteCmd provides a command to mute an input. type InputMuteCmd struct { InputName string `arg:"" help:"Name of the input to mute."` @@ -188,3 +283,273 @@ func (cmd *InputToggleCmd) Run(ctx *context) error { } return nil } + +// InputVolumeCmd provides a command to set the volume of an input. +type InputVolumeCmd struct { + InputName string `arg:"" help:"Name of the input to set volume for." required:""` + Volume float64 `arg:"" help:"Volume level (-90.0 to 0.0)." required:""` +} + +// Run executes the command to set the volume of an input. +// accepts values between -90.0 and 0.0 representing decibels (dB). +func (cmd *InputVolumeCmd) Run(ctx *context) error { + if cmd.Volume < -90.0 || cmd.Volume > 0.0 { + return fmt.Errorf("volume must be between -90.0 and 0.0 dB") + } + + _, err := ctx.Client.Inputs.SetInputVolume( + inputs.NewSetInputVolumeParams(). + WithInputName(cmd.InputName). + WithInputVolumeDb(cmd.Volume), + ) + if err != nil { + return fmt.Errorf("failed to set input volume: %w", err) + } + + fmt.Fprintf(ctx.Out, "Set volume of input %s to %.1f dB\n", + ctx.Style.Highlight(cmd.InputName), cmd.Volume) + return nil +} + +// InputShowCmd provides a command to show input details. +type InputShowCmd struct { + Name string `arg:"" help:"Name of the input to show." required:""` + Verbose bool ` help:"List all available input devices." flag:""` +} + +// Run executes the command to show input details. +func (cmd *InputShowCmd) Run(ctx *context) error { + lresp, err := ctx.Client.Inputs.GetInputList(inputs.NewGetInputListParams()) + if err != nil { + return fmt.Errorf("failed to get input list: %w", err) + } + + var inputKind string + var found bool + for _, input := range lresp.Inputs { + if input.InputName == cmd.Name { + inputKind = input.InputKind + found = true + break + } + } + + if !found { + return fmt.Errorf("input '%s' not found", cmd.Name) + } + + prop, name := device(ctx, cmd.Name) + if prop == "" { + return fmt.Errorf("no device property found for input '%s'", cmd.Name) + } + + t := table.New().Border(lipgloss.RoundedBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border)) + t.Headers("Input Name", "Kind", "Device") + t.StyleFunc(func(row, col int) lipgloss.Style { + style := lipgloss.NewStyle().Padding(0, 3) + switch col { + case 0: + style = style.Align(lipgloss.Left) + case 1: + style = style.Align(lipgloss.Left) + case 2: + style = style.Align(lipgloss.Center) + } + switch { + case row == table.HeaderRow: + style = style.Bold(true).Align(lipgloss.Center) + case row%2 == 0: + style = style.Foreground(ctx.Style.evenRows) + default: + style = style.Foreground(ctx.Style.oddRows) + } + return style + }) + t.Row(cmd.Name, snakeCaseToTitleCase(inputKind), name) + + fmt.Fprintln(ctx.Out, t.Render()) + + if cmd.Verbose { + deviceListResp, err := ctx.Client.Inputs.GetInputPropertiesListPropertyItems( + inputs.NewGetInputPropertiesListPropertyItemsParams(). + WithInputName(cmd.Name). + WithPropertyName(prop), + ) + if err != nil { + return fmt.Errorf("failed to get device list: %w", err) + } + + t := table.New().Border(lipgloss.RoundedBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border)) + t.StyleFunc(func(row, col int) lipgloss.Style { + style := lipgloss.NewStyle().Padding(0, 3) + switch col { + case 0: + style = style.Align(lipgloss.Left) + } + switch { + case row == table.HeaderRow: + style = style.Bold(true).Align(lipgloss.Center) + case row%2 == 0: + style = style.Foreground(ctx.Style.evenRows) + default: + style = style.Foreground(ctx.Style.oddRows) + } + return style + }) + + t.Headers("Devices") + + for _, item := range deviceListResp.PropertyItems { + if item.ItemName != "" { + t.Row(item.ItemName) + } + } + + fmt.Fprintln(ctx.Out, t.Render()) + } + + return nil +} + +func device(ctx *context, inputName string) (string, string) { + settings, err := ctx.Client.Inputs.GetInputSettings( + inputs.NewGetInputSettingsParams().WithInputName(inputName), + ) + if err != nil { + return "", "" + } + + for _, propName := range []string{"device", "device_id"} { + deviceListResp, err := ctx.Client.Inputs.GetInputPropertiesListPropertyItems( + inputs.NewGetInputPropertiesListPropertyItemsParams(). + WithInputName(inputName). + WithPropertyName(propName), + ) + if err == nil && len(deviceListResp.PropertyItems) > 0 { + for _, item := range deviceListResp.PropertyItems { + if item.ItemValue == settings.InputSettings[propName] { + return propName, item.ItemName + } + } + } + } + + return "", "" +} + +// InputUpdateCmd provides a command to update input settings. +type InputUpdateCmd struct { + InputName string `arg:"" help:"Name of the input to update." required:""` + DeviceName string `arg:"" help:"Name of the device to set." required:""` +} + +// Run executes the command to update input settings. +func (cmd *InputUpdateCmd) Run(ctx *context) error { + // Use the device helper to find the correct device property name + prop, _ := device(ctx, cmd.InputName) + if prop == "" { + return fmt.Errorf("no device property found for input '%s'", cmd.InputName) + } + + resp, err := ctx.Client.Inputs.GetInputPropertiesListPropertyItems( + inputs.NewGetInputPropertiesListPropertyItemsParams(). + WithInputName(cmd.InputName). + WithPropertyName(prop), + ) + if err != nil { + return err + } + + var deviceValue any + var found bool + for _, item := range resp.PropertyItems { + if item.ItemName == cmd.DeviceName { + deviceValue = item.ItemValue + found = true + break + } + } + + if !found { + return fmt.Errorf("device '%s' not found for input '%s'", cmd.DeviceName, cmd.InputName) + } + + sresp, err := ctx.Client.Inputs.GetInputSettings( + inputs.NewGetInputSettingsParams().WithInputName(cmd.InputName), + ) + if err != nil { + return err + } + + settings := make(map[string]any) + maps.Copy(settings, sresp.InputSettings) + settings[prop] = deviceValue + + _, err = ctx.Client.Inputs.SetInputSettings( + inputs.NewSetInputSettingsParams(). + WithInputName(cmd.InputName). + WithInputSettings(settings), + ) + if err != nil { + return fmt.Errorf("failed to update input settings: %w", err) + } + + fmt.Fprintf(ctx.Out, "Input %s %s set to %s\n", + ctx.Style.Highlight(cmd.InputName), prop, ctx.Style.Highlight(cmd.DeviceName)) + + return nil +} + +// InputKindDefaultsCmd provides a command to get default settings for an input kind. +type InputKindDefaultsCmd struct { + Kind string `arg:"" help:"Input kind to get default settings for." required:""` +} + +// Run executes the command to get default settings for an input kind. +func (cmd *InputKindDefaultsCmd) Run(ctx *context) error { + resp, err := ctx.Client.Inputs.GetInputDefaultSettings( + inputs.NewGetInputDefaultSettingsParams(). + WithInputKind(cmd.Kind), + ) + if err != nil { + return fmt.Errorf("failed to get default settings for input kind '%s': %w", cmd.Kind, err) + } + + t := table.New().Border(lipgloss.RoundedBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border)) + t.Headers("Setting", "Value") + t.StyleFunc(func(row, col int) lipgloss.Style { + style := lipgloss.NewStyle().Padding(0, 3) + switch col { + case 0: + style = style.Align(lipgloss.Left) + case 1: + style = style.Align(lipgloss.Center) + } + switch { + case row == table.HeaderRow: + style = style.Bold(true).Align(lipgloss.Center) + case row%2 == 0: + style = style.Foreground(ctx.Style.evenRows) + default: + style = style.Foreground(ctx.Style.oddRows) + } + return style + }) + + keys := make([]string, 0, len(resp.DefaultInputSettings)) + for k := range resp.DefaultInputSettings { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, key := range keys { + value := resp.DefaultInputSettings[key] + t.Row(key, fmt.Sprintf("%v", value)) + } + + fmt.Fprintln(ctx.Out, t.Render()) + return nil +}