style tables and add out/err highlights

improve projector open error message if an invalid monitor index is passed.
it now prints the monitor name and suggests `gobs-cli prj ls-m`

improve error message for sceneitem commands if a scene item in a group is queried without the --group flag.
This commit is contained in:
onyx-and-iris 2025-06-21 06:41:12 +01:00
parent d699939298
commit 7147c3f1ca
11 changed files with 424 additions and 150 deletions

View File

@ -7,7 +7,8 @@ import (
"strings"
"github.com/andreykaipov/goobs/api/requests/filters"
"github.com/aquasecurity/table"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
// FilterCmd provides commands to manage filters in OBS Studio.
@ -25,6 +26,7 @@ type FilterListCmd struct {
}
// Run executes the command to list all filters in a scene.
// nolint: misspell
func (cmd *FilterListCmd) Run(ctx *context) error {
if cmd.SourceName == "" {
currentScene, err := ctx.Client.Scenes.GetCurrentProgramScene()
@ -42,14 +44,35 @@ func (cmd *FilterListCmd) Run(ctx *context) error {
}
if len(sourceFilters.Filters) == 0 {
fmt.Fprintf(ctx.Out, "No filters found for source %s.\n", cmd.SourceName)
fmt.Fprintf(ctx.Out, "No filters found for source %s.\n", ctx.Style.Highlight(cmd.SourceName))
return nil
}
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignCenter, table.AlignLeft)
t.SetHeaders("Filter Name", "Kind", "Enabled", "Settings")
t := table.New().Border(lipgloss.RoundedBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border)).
Headers("Filter Name", "Kind", "Enabled", "Settings").
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)
case 3:
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 _, filter := range sourceFilters.Filters {
defaultSettings, err := ctx.Client.Filters.GetSourceFilterDefaultSettings(
@ -58,7 +81,7 @@ func (cmd *FilterListCmd) Run(ctx *context) error {
)
if err != nil {
return fmt.Errorf("failed to get default settings for filter %s: %w",
filter.FilterName, err)
ctx.Style.Error(filter.FilterName), err)
}
maps.Insert(defaultSettings.DefaultFilterSettings, maps.All(filter.FilterSettings))
@ -70,14 +93,14 @@ func (cmd *FilterListCmd) Run(ctx *context) error {
return strings.ToLower(lines[i]) < strings.ToLower(lines[j])
})
t.AddRow(
t.Row(
filter.FilterName,
snakeCaseToTitleCase(filter.FilterKind),
getEnabledMark(filter.FilterEnabled),
strings.Join(lines, "\n"),
)
}
t.Render()
fmt.Fprintln(ctx.Out, t.Render())
return nil
}
@ -97,10 +120,10 @@ func (cmd *FilterEnableCmd) Run(ctx *context) error {
)
if err != nil {
return fmt.Errorf("failed to enable filter %s on source %s: %w",
cmd.FilterName, cmd.SourceName, err)
ctx.Style.Error(cmd.FilterName), ctx.Style.Error(cmd.SourceName), err)
}
fmt.Fprintf(ctx.Out, "Filter %s enabled on source %s.\n",
cmd.FilterName, cmd.SourceName)
ctx.Style.Highlight(cmd.FilterName), ctx.Style.Highlight(cmd.SourceName))
return nil
}
@ -120,10 +143,10 @@ func (cmd *FilterDisableCmd) Run(ctx *context) error {
)
if err != nil {
return fmt.Errorf("failed to disable filter %s on source %s: %w",
cmd.FilterName, cmd.SourceName, err)
ctx.Style.Error(cmd.FilterName), ctx.Style.Error(cmd.SourceName), err)
}
fmt.Fprintf(ctx.Out, "Filter %s disabled on source %s.\n",
cmd.FilterName, cmd.SourceName)
ctx.Style.Highlight(cmd.FilterName), ctx.Style.Highlight(cmd.SourceName))
return nil
}
@ -142,7 +165,7 @@ func (cmd *FilterToggleCmd) Run(ctx *context) error {
)
if err != nil {
return fmt.Errorf("failed to get filter %s on source %s: %w",
cmd.FilterName, cmd.SourceName, err)
ctx.Style.Error(cmd.FilterName), ctx.Style.Error(cmd.SourceName), err)
}
newStatus := !filter.FilterEnabled
@ -154,15 +177,15 @@ func (cmd *FilterToggleCmd) Run(ctx *context) error {
)
if err != nil {
return fmt.Errorf("failed to toggle filter %s on source %s: %w",
cmd.FilterName, cmd.SourceName, err)
ctx.Style.Error(cmd.FilterName), ctx.Style.Error(cmd.SourceName), err)
}
if newStatus {
fmt.Fprintf(ctx.Out, "Filter %s on source %s is now enabled.\n",
cmd.FilterName, cmd.SourceName)
ctx.Style.Highlight(cmd.FilterName), ctx.Style.Highlight(cmd.SourceName))
} else {
fmt.Fprintf(ctx.Out, "Filter %s on source %s is now disabled.\n",
cmd.FilterName, cmd.SourceName)
ctx.Style.Highlight(cmd.FilterName), ctx.Style.Highlight(cmd.SourceName))
}
return nil
}
@ -182,14 +205,14 @@ func (cmd *FilterStatusCmd) Run(ctx *context) error {
)
if err != nil {
return fmt.Errorf("failed to get status of filter %s on source %s: %w",
cmd.FilterName, cmd.SourceName, err)
ctx.Style.Error(cmd.FilterName), ctx.Style.Error(cmd.SourceName), err)
}
if filter.FilterEnabled {
fmt.Fprintf(ctx.Out, "Filter %s on source %s is enabled.\n",
cmd.FilterName, cmd.SourceName)
ctx.Style.Highlight(cmd.FilterName), ctx.Style.Highlight(cmd.SourceName))
} else {
fmt.Fprintf(ctx.Out, "Filter %s on source %s is disabled.\n",
cmd.FilterName, cmd.SourceName)
ctx.Style.Highlight(cmd.FilterName), ctx.Style.Highlight(cmd.SourceName))
}
return nil
}

View File

@ -4,7 +4,8 @@ import (
"fmt"
"github.com/andreykaipov/goobs/api/requests/sceneitems"
"github.com/aquasecurity/table"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
// GroupCmd provides commands to manage groups in OBS Studio.
@ -22,6 +23,7 @@ type GroupListCmd struct {
}
// Run executes the command to list all groups in a scene.
// nolint: misspell
func (cmd *GroupListCmd) Run(ctx *context) error {
if cmd.SceneName == "" {
currentScene, err := ctx.Client.Scenes.GetCurrentProgramScene()
@ -37,17 +39,44 @@ func (cmd *GroupListCmd) Run(ctx *context) error {
return fmt.Errorf("failed to get scene item list: %w", err)
}
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignCenter, table.AlignLeft, table.AlignCenter)
t.SetHeaders("ID", "Group Name", "Enabled")
t := table.New().Border(lipgloss.RoundedBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border)).
Headers("ID", "Group Name", "Enabled").
StyleFunc(func(row, col int) lipgloss.Style {
style := lipgloss.NewStyle().Padding(0, 3)
switch col {
case 0:
style = style.Align(lipgloss.Center)
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
})
var found bool
for _, item := range resp.SceneItems {
if item.IsGroup {
t.AddRow(fmt.Sprintf("%d", item.SceneItemID), item.SourceName, getEnabledMark(item.SceneItemEnabled))
t.Row(fmt.Sprintf("%d", item.SceneItemID), item.SourceName, getEnabledMark(item.SceneItemEnabled))
found = true
}
}
t.Render()
if !found {
fmt.Fprintf(ctx.Out, "No groups found in scene %s.\n", ctx.Style.Highlight(cmd.SceneName))
return nil
}
fmt.Fprintln(ctx.Out, t.Render())
return nil
}
@ -75,13 +104,17 @@ func (cmd *GroupShowCmd) Run(ctx *context) error {
if err != nil {
return fmt.Errorf("failed to set scene item enabled: %w", err)
}
fmt.Fprintf(ctx.Out, "Group %s is now shown.\n", cmd.GroupName)
fmt.Fprintf(ctx.Out, "Group %s is now shown.\n", ctx.Style.Highlight(cmd.GroupName))
found = true
break
}
}
if !found {
return fmt.Errorf("group '%s' not found", cmd.GroupName)
return fmt.Errorf(
"group %s not found in scene %s",
ctx.Style.Error(cmd.GroupName),
ctx.Style.Error(cmd.SceneName),
)
}
return nil
}
@ -110,13 +143,17 @@ func (cmd *GroupHideCmd) Run(ctx *context) error {
if err != nil {
return fmt.Errorf("failed to set scene item enabled: %w", err)
}
fmt.Fprintf(ctx.Out, "Group %s is now hidden.\n", cmd.GroupName)
fmt.Fprintf(ctx.Out, "Group %s is now hidden.\n", ctx.Style.Highlight(cmd.GroupName))
found = true
break
}
}
if !found {
return fmt.Errorf("group '%s' not found", cmd.GroupName)
return fmt.Errorf(
"group %s not found in scene %s",
ctx.Style.Error(cmd.GroupName),
ctx.Style.Error(cmd.SceneName),
)
}
return nil
}
@ -147,16 +184,20 @@ func (cmd *GroupToggleCmd) Run(ctx *context) error {
return fmt.Errorf("failed to set scene item enabled: %w", err)
}
if newState {
fmt.Fprintf(ctx.Out, "Group %s is now shown.\n", cmd.GroupName)
fmt.Fprintf(ctx.Out, "Group %s is now shown.\n", ctx.Style.Highlight(cmd.GroupName))
} else {
fmt.Fprintf(ctx.Out, "Group %s is now hidden.\n", cmd.GroupName)
fmt.Fprintf(ctx.Out, "Group %s is now hidden.\n", ctx.Style.Highlight(cmd.GroupName))
}
found = true
break
}
}
if !found {
return fmt.Errorf("group '%s' not found", cmd.GroupName)
return fmt.Errorf(
"group %s not found in scene %s",
ctx.Style.Error(cmd.GroupName),
ctx.Style.Error(cmd.SceneName),
)
}
return nil
@ -178,12 +219,12 @@ func (cmd *GroupStatusCmd) Run(ctx *context) error {
for _, item := range resp.SceneItems {
if item.IsGroup && item.SourceName == cmd.GroupName {
if item.SceneItemEnabled {
fmt.Fprintf(ctx.Out, "Group %s is shown.\n", cmd.GroupName)
fmt.Fprintf(ctx.Out, "Group %s is shown.\n", ctx.Style.Highlight(cmd.GroupName))
} else {
fmt.Fprintf(ctx.Out, "Group %s is hidden.\n", cmd.GroupName)
fmt.Fprintf(ctx.Out, "Group %s is hidden.\n", ctx.Style.Highlight(cmd.GroupName))
}
return nil
}
}
return fmt.Errorf("group '%s' not found", cmd.GroupName)
return fmt.Errorf("group %s not found in scene %s", ctx.Style.Error(cmd.GroupName), ctx.Style.Error(cmd.SceneName))
}

View File

@ -1,9 +1,12 @@
package main
import (
"fmt"
"github.com/andreykaipov/goobs/api/requests/general"
"github.com/andreykaipov/goobs/api/typedefs"
"github.com/aquasecurity/table"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
// HotkeyCmd provides commands to manage hotkeys in OBS Studio.
@ -23,15 +26,26 @@ func (cmd *HotkeyListCmd) Run(ctx *context) error {
return err
}
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignLeft)
t.SetHeaders("Hotkey Name")
t := table.New().Border(lipgloss.RoundedBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border)).
Headers("Hotkey Name").
StyleFunc(func(row, _ int) lipgloss.Style {
style := lipgloss.NewStyle().Padding(0, 3)
switch {
case row == table.HeaderRow:
style = style.Bold(true).Align(lipgloss.Center) // nolint: misspell
case row%2 == 0:
style = style.Foreground(ctx.Style.evenRows)
default:
style = style.Foreground(ctx.Style.oddRows)
}
return style
})
for _, hotkey := range resp.Hotkeys {
t.AddRow(hotkey)
t.Row(hotkey)
}
t.Render()
fmt.Fprintln(ctx.Out, t.Render())
return nil
}

View File

@ -1,3 +1,4 @@
// nolint: misspell
package main
import (
@ -6,7 +7,8 @@ import (
"strings"
"github.com/andreykaipov/goobs/api/requests/inputs"
"github.com/aquasecurity/table"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
// InputCmd provides commands to manage inputs in OBS Studio.
@ -34,15 +36,35 @@ func (cmd *InputListCmd) Run(ctx *context) error {
return err
}
t := table.New(ctx.Out)
t.SetPadding(3)
t := table.New().Border(lipgloss.RoundedBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border))
if cmd.UUID {
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignCenter, table.AlignLeft)
t.SetHeaders("Input Name", "Kind", "Muted", "UUID")
t.Headers("Input Name", "Kind", "Muted", "UUID")
} else {
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignCenter)
t.SetHeaders("Input Name", "Kind", "Muted")
t.Headers("Input Name", "Kind", "Muted")
}
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)
case 3:
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
})
sort.Slice(resp.Inputs, func(i, j int) bool {
return resp.Inputs[i].InputName < resp.Inputs[j].InputName
@ -79,9 +101,9 @@ func (cmd *InputListCmd) Run(ctx *context) error {
for _, f := range filters {
if f.enabled && strings.Contains(input.InputKind, f.keyword) {
if cmd.UUID {
t.AddRow(input.InputName, input.InputKind, muteMark, input.InputUuid)
t.Row(input.InputName, input.InputKind, muteMark, input.InputUuid)
} else {
t.AddRow(input.InputName, input.InputKind, muteMark)
t.Row(input.InputName, input.InputKind, muteMark)
}
added = true
break
@ -90,13 +112,13 @@ func (cmd *InputListCmd) Run(ctx *context) error {
if !added && (!cmd.Input && !cmd.Output && !cmd.Colour && !cmd.Ffmpeg && !cmd.Vlc) {
if cmd.UUID {
t.AddRow(input.InputName, snakeCaseToTitleCase(input.InputKind), muteMark, input.InputUuid)
t.Row(input.InputName, snakeCaseToTitleCase(input.InputKind), muteMark, input.InputUuid)
} else {
t.AddRow(input.InputName, snakeCaseToTitleCase(input.InputKind), muteMark)
t.Row(input.InputName, snakeCaseToTitleCase(input.InputKind), muteMark)
}
}
}
t.Render()
fmt.Fprintln(ctx.Out, t.Render())
return nil
}
@ -114,7 +136,7 @@ func (cmd *InputMuteCmd) Run(ctx *context) error {
return fmt.Errorf("failed to mute input: %w", err)
}
fmt.Fprintf(ctx.Out, "Muted input: %s\n", cmd.InputName)
fmt.Fprintf(ctx.Out, "Muted input: %s\n", ctx.Style.Highlight(cmd.InputName))
return nil
}
@ -132,7 +154,7 @@ func (cmd *InputUnmuteCmd) Run(ctx *context) error {
return fmt.Errorf("failed to unmute input: %w", err)
}
fmt.Fprintf(ctx.Out, "Unmuted input: %s\n", cmd.InputName)
fmt.Fprintf(ctx.Out, "Unmuted input: %s\n", ctx.Style.Highlight(cmd.InputName))
return nil
}
@ -160,9 +182,9 @@ func (cmd *InputToggleCmd) Run(ctx *context) error {
}
if newMuteState {
fmt.Fprintf(ctx.Out, "Muted input: %s\n", cmd.InputName)
fmt.Fprintf(ctx.Out, "Muted input: %s\n", ctx.Style.Highlight(cmd.InputName))
} else {
fmt.Fprintf(ctx.Out, "Unmuted input: %s\n", cmd.InputName)
fmt.Fprintf(ctx.Out, "Unmuted input: %s\n", ctx.Style.Highlight(cmd.InputName))
}
return nil
}

View File

@ -5,7 +5,8 @@ import (
"slices"
"github.com/andreykaipov/goobs/api/requests/config"
"github.com/aquasecurity/table"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
// ProfileCmd provides commands to manage profiles in OBS Studio.
@ -21,16 +22,34 @@ type ProfileCmd struct {
type ProfileListCmd struct{} // size = 0x0
// Run executes the command to list all profiles.
// nolint: misspell
func (cmd *ProfileListCmd) Run(ctx *context) error {
profiles, err := ctx.Client.Config.GetProfileList()
if err != nil {
return err
}
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignLeft, table.AlignCenter)
t.SetHeaders("Profile Name", "Current")
t := table.New().Border(lipgloss.RoundedBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border)).
Headers("Profile Name", "Current").
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
})
for _, profile := range profiles.Profiles {
var enabledMark string
@ -38,9 +57,9 @@ func (cmd *ProfileListCmd) Run(ctx *context) error {
enabledMark = getEnabledMark(true)
}
t.AddRow(profile, enabledMark)
t.Row(profile, enabledMark)
}
t.Render()
fmt.Fprintln(ctx.Out, t.Render())
return nil
}
@ -53,7 +72,7 @@ func (cmd *ProfileCurrentCmd) Run(ctx *context) error {
if err != nil {
return err
}
fmt.Fprintf(ctx.Out, "Current profile: %s\n", profiles.CurrentProfileName)
fmt.Fprintf(ctx.Out, "Current profile: %s\n", ctx.Style.Highlight(profiles.CurrentProfileName))
return nil
}
@ -72,15 +91,20 @@ func (cmd *ProfileSwitchCmd) Run(ctx *context) error {
current := profiles.CurrentProfileName
if current == cmd.Name {
return nil
return fmt.Errorf("already using profile %s", ctx.Style.Error(cmd.Name))
}
_, err = ctx.Client.Config.SetCurrentProfile(config.NewSetCurrentProfileParams().WithProfileName(cmd.Name))
if err != nil {
return err
return fmt.Errorf("failed to switch to profile %s: %w", ctx.Style.Error(cmd.Name), err)
}
fmt.Fprintf(ctx.Out, "Switched from profile %s to %s\n", current, cmd.Name)
fmt.Fprintf(
ctx.Out,
"Switched from profile %s to %s\n",
ctx.Style.Highlight(current),
ctx.Style.Highlight(cmd.Name),
)
return nil
}
@ -98,15 +122,15 @@ func (cmd *ProfileCreateCmd) Run(ctx *context) error {
}
if slices.Contains(profiles.Profiles, cmd.Name) {
return fmt.Errorf("profile %s already exists", cmd.Name)
return fmt.Errorf("profile %s already exists", ctx.Style.Error(cmd.Name))
}
_, err = ctx.Client.Config.CreateProfile(config.NewCreateProfileParams().WithProfileName(cmd.Name))
if err != nil {
return err
return fmt.Errorf("failed to create profile %s: %w", ctx.Style.Error(cmd.Name), err)
}
fmt.Fprintf(ctx.Out, "Created profile: %s\n", cmd.Name)
fmt.Fprintf(ctx.Out, "Created profile: %s\n", ctx.Style.Highlight(cmd.Name))
return nil
}
@ -124,19 +148,21 @@ func (cmd *ProfileRemoveCmd) Run(ctx *context) error {
}
if !slices.Contains(profiles.Profiles, cmd.Name) {
return fmt.Errorf("profile %s does not exist", cmd.Name)
return fmt.Errorf("profile %s does not exist", ctx.Style.Error(cmd.Name))
}
// Prevent deletion of the current profile
// This is allowed in OBS Studio (with a confirmation prompt), but we want to prevent it here
if profiles.CurrentProfileName == cmd.Name {
return fmt.Errorf("cannot delete current profile %s", cmd.Name)
return fmt.Errorf("cannot delete current profile %s", ctx.Style.Error(cmd.Name))
}
_, err = ctx.Client.Config.RemoveProfile(config.NewRemoveProfileParams().WithProfileName(cmd.Name))
if err != nil {
return err
return fmt.Errorf("failed to delete profile %s: %w", ctx.Style.Error(cmd.Name), err)
}
fmt.Fprintf(ctx.Out, "Deleted profile: %s\n", cmd.Name)
fmt.Fprintf(ctx.Out, "Deleted profile: %s\n", ctx.Style.Highlight(cmd.Name))
return nil
}

View File

@ -4,7 +4,8 @@ import (
"fmt"
"github.com/andreykaipov/goobs/api/requests/ui"
"github.com/aquasecurity/table"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
// ProjectorCmd provides a command to manage projectors in OBS.
@ -17,6 +18,7 @@ type ProjectorCmd struct {
type ProjectorListMonitorsCmd struct{} // size = 0x0
// Run executes the command to list all monitors available for projectors.
// nolint: misspell
func (cmd *ProjectorListMonitorsCmd) Run(ctx *context) error {
monitors, err := ctx.Client.Ui.GetMonitorList()
if err != nil {
@ -24,20 +26,37 @@ func (cmd *ProjectorListMonitorsCmd) Run(ctx *context) error {
}
if len(monitors.Monitors) == 0 {
ctx.Out.Write([]byte("No monitors found for projectors.\n"))
fmt.Fprintf(ctx.Out, "No monitors found.\n")
return nil
}
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignCenter, table.AlignLeft)
t.SetHeaders("Monitor ID", "Monitor Name")
t := table.New().Border(lipgloss.RoundedBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border)).
Headers("Monitor ID", "Monitor Name").
StyleFunc(func(row, col int) lipgloss.Style {
style := lipgloss.NewStyle().Padding(0, 3)
switch col {
case 0:
style = style.Align(lipgloss.Center)
case 1:
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 _, monitor := range monitors.Monitors {
t.AddRow(fmt.Sprintf("%d", monitor.MonitorIndex), monitor.MonitorName)
t.Row(fmt.Sprintf("%d", monitor.MonitorIndex), monitor.MonitorName)
}
t.Render()
fmt.Fprintln(ctx.Out, t.Render())
return nil
}
@ -57,10 +76,36 @@ func (cmd *ProjectorOpenCmd) Run(ctx *context) error {
cmd.SourceName = currentScene.SceneName
}
monitors, err := ctx.Client.Ui.GetMonitorList()
if err != nil {
return err
}
var monitorName string
for _, monitor := range monitors.Monitors {
if monitor.MonitorIndex == cmd.MonitorIndex {
monitorName = monitor.MonitorName
break
}
}
if monitorName == "" {
return fmt.Errorf(
"monitor with index %s not found. use %s to list available monitors",
ctx.Style.Error(fmt.Sprintf("%d", cmd.MonitorIndex)),
ctx.Style.Error("gobs-cli prj ls-m"),
)
}
ctx.Client.Ui.OpenSourceProjector(ui.NewOpenSourceProjectorParams().
WithSourceName(cmd.SourceName).
WithMonitorIndex(cmd.MonitorIndex))
fmt.Fprintf(ctx.Out, "Opened projector for source '%s' on monitor index %d.\n", cmd.SourceName, cmd.MonitorIndex)
fmt.Fprintf(
ctx.Out,
"Opened projector for source %s on monitor %s.\n",
ctx.Style.Highlight(cmd.SourceName),
ctx.Style.Highlight(monitorName),
)
return nil
}

View File

@ -60,7 +60,11 @@ func (cmd *RecordStopCmd) Run(ctx *context) error {
if err != nil {
return err
}
fmt.Fprintf(ctx.Out, "%s", fmt.Sprintf("Recording stopped successfully. Output file: %s\n", resp.OutputPath))
fmt.Fprintf(
ctx.Out,
"%s",
fmt.Sprintf("Recording stopped successfully. Output file: %s\n", ctx.Style.Highlight(resp.OutputPath)),
)
return nil
}
@ -169,7 +173,7 @@ func (cmd *RecordDirectoryCmd) Run(ctx *context) error {
if err != nil {
return err
}
fmt.Fprintf(ctx.Out, "Current recording directory: %s\n", resp.RecordDirectory)
fmt.Fprintf(ctx.Out, "Current recording directory: %s\n", ctx.Style.Highlight(resp.RecordDirectory))
return nil
}
@ -180,6 +184,6 @@ func (cmd *RecordDirectoryCmd) Run(ctx *context) error {
return err
}
fmt.Fprintf(ctx.Out, "Recording directory set to: %s\n", cmd.RecordDirectory)
fmt.Fprintf(ctx.Out, "Recording directory set to: %s\n", ctx.Style.Highlight(cmd.RecordDirectory))
return nil
}

View File

@ -5,7 +5,8 @@ import (
"slices"
"github.com/andreykaipov/goobs/api/requests/scenes"
"github.com/aquasecurity/table"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
// SceneCmd provides commands to manage scenes in OBS Studio.
@ -21,6 +22,7 @@ type SceneListCmd struct {
}
// Run executes the command to list all scenes.
// nolint: misspell
func (cmd *SceneListCmd) Run(ctx *context) error {
scenes, err := ctx.Client.Scenes.GetSceneList()
if err != nil {
@ -32,15 +34,33 @@ func (cmd *SceneListCmd) Run(ctx *context) error {
return err
}
t := table.New(ctx.Out)
t.SetPadding(3)
t := table.New().Border(lipgloss.RoundedBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border))
if cmd.UUID {
t.SetAlignment(table.AlignLeft, table.AlignCenter, table.AlignLeft)
t.SetHeaders("Scene Name", "Active", "UUID")
t.Headers("Scene Name", "Active", "UUID")
} else {
t.SetAlignment(table.AlignLeft, table.AlignCenter)
t.SetHeaders("Scene Name", "Active")
t.Headers("Scene Name", "Active")
}
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)
case 2:
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
})
slices.Reverse(scenes.Scenes)
for _, scene := range scenes.Scenes {
@ -49,12 +69,12 @@ func (cmd *SceneListCmd) Run(ctx *context) error {
activeMark = getEnabledMark(true)
}
if cmd.UUID {
t.AddRow(scene.SceneName, activeMark, scene.SceneUuid)
t.Row(scene.SceneName, activeMark, scene.SceneUuid)
} else {
t.AddRow(scene.SceneName, activeMark)
t.Row(scene.SceneName, activeMark)
}
}
t.Render()
fmt.Fprintln(ctx.Out, t.Render())
return nil
}
@ -70,13 +90,13 @@ func (cmd *SceneCurrentCmd) Run(ctx *context) error {
if err != nil {
return err
}
fmt.Fprintln(ctx.Out, scene.SceneName)
fmt.Fprintf(ctx.Out, "Current preview scene: %s\n", ctx.Style.Highlight(scene.SceneName))
} else {
scene, err := ctx.Client.Scenes.GetCurrentProgramScene()
if err != nil {
return err
}
fmt.Fprintln(ctx.Out, scene.SceneName)
fmt.Fprintf(ctx.Out, "Current program scene: %s\n", ctx.Style.Highlight(scene.SceneName))
}
return nil
}
@ -96,7 +116,7 @@ func (cmd *SceneSwitchCmd) Run(ctx *context) error {
return err
}
fmt.Fprintln(ctx.Out, "Switched to preview scene:", cmd.NewScene)
fmt.Fprintf(ctx.Out, "Switched to preview scene: %s\n", ctx.Style.Highlight(cmd.NewScene))
} else {
_, err := ctx.Client.Scenes.SetCurrentProgramScene(scenes.NewSetCurrentProgramSceneParams().
WithSceneName(cmd.NewScene))
@ -104,7 +124,7 @@ func (cmd *SceneSwitchCmd) Run(ctx *context) error {
return err
}
fmt.Fprintln(ctx.Out, "Switched to program scene:", cmd.NewScene)
fmt.Fprintf(ctx.Out, "Switched to program scene: %s\n", ctx.Style.Highlight(cmd.NewScene))
}
return nil
}

View File

@ -4,7 +4,8 @@ import (
"fmt"
"github.com/andreykaipov/goobs/api/requests/config"
"github.com/aquasecurity/table"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
// SceneCollectionCmd provides commands to manage scene collections in OBS Studio.
@ -19,21 +20,37 @@ type SceneCollectionCmd struct {
type SceneCollectionListCmd struct{} // size = 0x0
// Run executes the command to list all scene collections.
// nolint: misspell
func (cmd *SceneCollectionListCmd) Run(ctx *context) error {
collections, err := ctx.Client.Config.GetSceneCollectionList()
if err != nil {
return fmt.Errorf("failed to get scene collection list: %w", err)
}
t := table.New(ctx.Out)
t.SetPadding(3)
t.SetAlignment(table.AlignLeft)
t.SetHeaders("Scene Collection Name")
t := table.New().Border(lipgloss.RoundedBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border)).
Headers("Scene Collection Name").
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 _, collection := range collections.SceneCollections {
t.AddRow(collection)
t.Row(collection)
}
t.Render()
fmt.Fprintln(ctx.Out, t.Render())
return nil
}
@ -65,17 +82,17 @@ func (cmd *SceneCollectionSwitchCmd) Run(ctx *context) error {
current := collections.CurrentSceneCollectionName
if current == cmd.Name {
return fmt.Errorf("scene collection %s is already active", cmd.Name)
return fmt.Errorf("scene collection %s is already active", ctx.Style.Error(cmd.Name))
}
_, err = ctx.Client.Config.SetCurrentSceneCollection(
config.NewSetCurrentSceneCollectionParams().WithSceneCollectionName(cmd.Name),
)
if err != nil {
return fmt.Errorf("failed to switch scene collection: %w", err)
return fmt.Errorf("failed to switch scene collection %s: %w", ctx.Style.Error(cmd.Name), err)
}
fmt.Fprintf(ctx.Out, "Switched to scene collection: %s\n", cmd.Name)
fmt.Fprintf(ctx.Out, "Switched to scene collection: %s\n", ctx.Style.Highlight(cmd.Name))
return nil
}
@ -91,9 +108,9 @@ func (cmd *SceneCollectionCreateCmd) Run(ctx *context) error {
config.NewCreateSceneCollectionParams().WithSceneCollectionName(cmd.Name),
)
if err != nil {
return fmt.Errorf("failed to create scene collection: %w", err)
return fmt.Errorf("failed to create scene collection %s: %w", ctx.Style.Error(cmd.Name), err)
}
fmt.Fprintf(ctx.Out, "Created scene collection: %s\n", cmd.Name)
fmt.Fprintf(ctx.Out, "Created scene collection: %s\n", ctx.Style.Highlight(cmd.Name))
return nil
}

View File

@ -1,3 +1,4 @@
// nolint: misspell
package main
import (
@ -6,7 +7,8 @@ import (
"github.com/andreykaipov/goobs"
"github.com/andreykaipov/goobs/api/requests/sceneitems"
"github.com/aquasecurity/table"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
)
// SceneItemCmd provides commands to manage scene items in OBS Studio.
@ -42,19 +44,41 @@ func (cmd *SceneItemListCmd) Run(ctx *context) error {
}
if len(resp.SceneItems) == 0 {
fmt.Fprintf(ctx.Out, "No scene items found in scene '%s'.\n", cmd.SceneName)
fmt.Fprintf(ctx.Out, "No scene items found in scene %s.\n", ctx.Style.Highlight(cmd.SceneName))
return nil
}
t := table.New(ctx.Out)
t.SetPadding(3)
t := table.New().Border(lipgloss.RoundedBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(ctx.Style.border))
if cmd.UUID {
t.SetAlignment(table.AlignCenter, table.AlignLeft, table.AlignCenter, table.AlignCenter, table.AlignCenter)
t.SetHeaders("Item ID", "Item Name", "In Group", "Enabled", "UUID")
t.Headers("Item ID", "Item Name", "In Group", "Enabled", "UUID")
} else {
t.SetAlignment(table.AlignCenter, table.AlignLeft, table.AlignCenter, table.AlignCenter)
t.SetHeaders("Item ID", "Item Name", "In Group", "Enabled")
t.Headers("Item ID", "Item Name", "In Group", "Enabled")
}
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)
case 3:
style = style.Align(lipgloss.Center)
case 4:
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
})
sort.Slice(resp.SceneItems, func(i, j int) bool {
return resp.SceneItems[i].SceneItemID < resp.SceneItems[j].SceneItemID
@ -65,7 +89,11 @@ func (cmd *SceneItemListCmd) Run(ctx *context) error {
resp, err := ctx.Client.SceneItems.GetGroupSceneItemList(sceneitems.NewGetGroupSceneItemListParams().
WithSceneName(item.SourceName))
if err != nil {
return fmt.Errorf("failed to get group scene item list for '%s': %w", item.SourceName, err)
return fmt.Errorf(
"failed to get group scene item list for group %s: %w",
ctx.Style.Error(item.SourceName),
err,
)
}
sort.Slice(resp.SceneItems, func(i, j int) bool {
@ -74,7 +102,7 @@ func (cmd *SceneItemListCmd) Run(ctx *context) error {
for _, groupItem := range resp.SceneItems {
if cmd.UUID {
t.AddRow(
t.Row(
fmt.Sprintf("%d", groupItem.SceneItemID),
groupItem.SourceName,
item.SourceName,
@ -82,7 +110,7 @@ func (cmd *SceneItemListCmd) Run(ctx *context) error {
groupItem.SourceUuid,
)
} else {
t.AddRow(
t.Row(
fmt.Sprintf("%d", groupItem.SceneItemID),
groupItem.SourceName,
item.SourceName,
@ -92,26 +120,26 @@ func (cmd *SceneItemListCmd) Run(ctx *context) error {
}
} else {
if cmd.UUID {
t.AddRow(fmt.Sprintf("%d", item.SceneItemID), item.SourceName, "",
t.Row(fmt.Sprintf("%d", item.SceneItemID), item.SourceName, "",
getEnabledMark(item.SceneItemEnabled), item.SourceUuid)
} else {
t.AddRow(fmt.Sprintf("%d", item.SceneItemID), item.SourceName, "", getEnabledMark(item.SceneItemEnabled))
t.Row(fmt.Sprintf("%d", item.SceneItemID), item.SourceName, "", getEnabledMark(item.SceneItemEnabled))
}
}
}
t.Render()
fmt.Fprintln(ctx.Out, t.Render())
return nil
}
// getSceneNameAndItemID retrieves the scene name and item ID for a given item in a scene or group.
func getSceneNameAndItemID(
client *goobs.Client,
ctx *context,
sceneName string,
itemName string,
group string,
) (string, int, error) {
if group != "" {
resp, err := client.SceneItems.GetGroupSceneItemList(sceneitems.NewGetGroupSceneItemListParams().
resp, err := ctx.Client.SceneItems.GetGroupSceneItemList(sceneitems.NewGetGroupSceneItemListParams().
WithSceneName(group))
if err != nil {
return "", 0, err
@ -121,13 +149,22 @@ func getSceneNameAndItemID(
return group, int(item.SceneItemID), nil
}
}
return "", 0, fmt.Errorf("item '%s' not found in scene '%s'", itemName, sceneName)
return "", 0, fmt.Errorf("item %s not found in scene %s", ctx.Style.Error(itemName), ctx.Style.Error(sceneName))
}
itemID, err := client.SceneItems.GetSceneItemId(sceneitems.NewGetSceneItemIdParams().
itemID, err := ctx.Client.SceneItems.GetSceneItemId(sceneitems.NewGetSceneItemIdParams().
WithSceneName(sceneName).
WithSourceName(itemName))
if err != nil {
if err.Error() == "request GetSceneItemId: ResourceNotFound (600): No scene items were found in the specified scene by that name or offset." {
return "", 0, fmt.Errorf(
"item %s not found in scene %s. is it in a group? if so use the %s flag to specify the parent group\nuse %s for a list of items in the scene",
ctx.Style.Error(itemName),
ctx.Style.Error(sceneName),
ctx.Style.Error("--group"),
ctx.Style.Error("gobs-cli si ls"),
)
}
return "", 0, err
}
return sceneName, int(itemID.SceneItemId), nil
@ -143,7 +180,7 @@ type SceneItemShowCmd struct {
// Run executes the command to show a scene item.
func (cmd *SceneItemShowCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Group)
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx, cmd.SceneName, cmd.ItemName, cmd.Group)
if err != nil {
return err
}
@ -157,9 +194,14 @@ func (cmd *SceneItemShowCmd) Run(ctx *context) error {
}
if cmd.Group != "" {
fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' is now visible.\n", cmd.ItemName, cmd.Group)
fmt.Fprintf(
ctx.Out,
"Scene item %s in group %s is now visible.\n",
ctx.Style.Highlight(cmd.ItemName),
ctx.Style.Highlight(cmd.Group),
)
} else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now visible.\n", cmd.ItemName, cmd.SceneName)
fmt.Fprintf(ctx.Out, "Scene item %s in scene %s is now visible.\n", ctx.Style.Highlight(cmd.ItemName), ctx.Style.Highlight(cmd.SceneName))
}
return nil
@ -175,7 +217,7 @@ type SceneItemHideCmd struct {
// Run executes the command to hide a scene item.
func (cmd *SceneItemHideCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Group)
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx, cmd.SceneName, cmd.ItemName, cmd.Group)
if err != nil {
return err
}
@ -189,9 +231,14 @@ func (cmd *SceneItemHideCmd) Run(ctx *context) error {
}
if cmd.Group != "" {
fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' is now hidden.\n", cmd.ItemName, cmd.Group)
fmt.Fprintf(
ctx.Out,
"Scene item %s in group %s is now hidden.\n",
ctx.Style.Highlight(cmd.ItemName),
ctx.Style.Highlight(cmd.Group),
)
} else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now hidden.\n", cmd.ItemName, cmd.SceneName)
fmt.Fprintf(ctx.Out, "Scene item %s in scene %s is now hidden.\n", ctx.Style.Highlight(cmd.ItemName), ctx.Style.Highlight(cmd.SceneName))
}
return nil
@ -218,7 +265,7 @@ type SceneItemToggleCmd struct {
// Run executes the command to toggle the visibility of a scene item.
func (cmd *SceneItemToggleCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Group)
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx, cmd.SceneName, cmd.ItemName, cmd.Group)
if err != nil {
return err
}
@ -237,9 +284,14 @@ func (cmd *SceneItemToggleCmd) Run(ctx *context) error {
}
if itemEnabled {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now hidden.\n", cmd.ItemName, cmd.SceneName)
fmt.Fprintf(
ctx.Out,
"Scene item %s in scene %s is now hidden.\n",
ctx.Style.Highlight(cmd.ItemName),
ctx.Style.Highlight(cmd.SceneName),
)
} else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is now visible.\n", cmd.ItemName, cmd.SceneName)
fmt.Fprintf(ctx.Out, "Scene item %s in scene %s is now visible.\n", ctx.Style.Highlight(cmd.ItemName), ctx.Style.Highlight(cmd.SceneName))
}
return nil
@ -255,7 +307,7 @@ type SceneItemVisibleCmd struct {
// Run executes the command to check the visibility of a scene item.
func (cmd *SceneItemVisibleCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Group)
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx, cmd.SceneName, cmd.ItemName, cmd.Group)
if err != nil {
return err
}
@ -266,9 +318,14 @@ func (cmd *SceneItemVisibleCmd) Run(ctx *context) error {
}
if itemEnabled {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is visible.\n", cmd.ItemName, cmd.SceneName)
fmt.Fprintf(
ctx.Out,
"Scene item %s in scene %s is visible.\n",
ctx.Style.Highlight(cmd.ItemName),
ctx.Style.Highlight(cmd.SceneName),
)
} else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' is hidden.\n", cmd.ItemName, cmd.SceneName)
fmt.Fprintf(ctx.Out, "Scene item %s in scene %s is hidden.\n", ctx.Style.Highlight(cmd.ItemName), ctx.Style.Highlight(cmd.SceneName))
}
return nil
}
@ -299,7 +356,7 @@ type SceneItemTransformCmd struct {
// Run executes the command to transform a scene item.
func (cmd *SceneItemTransformCmd) Run(ctx *context) error {
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx.Client, cmd.SceneName, cmd.ItemName, cmd.Group)
sceneName, sceneItemID, err := getSceneNameAndItemID(ctx, cmd.SceneName, cmd.ItemName, cmd.Group)
if err != nil {
return err
}
@ -372,9 +429,14 @@ func (cmd *SceneItemTransformCmd) Run(ctx *context) error {
}
if cmd.Group != "" {
fmt.Fprintf(ctx.Out, "Scene item '%s' in group '%s' transformed.\n", cmd.ItemName, cmd.Group)
fmt.Fprintf(
ctx.Out,
"Scene item %s in group %s transformed.\n",
ctx.Style.Highlight(cmd.ItemName),
ctx.Style.Highlight(cmd.Group),
)
} else {
fmt.Fprintf(ctx.Out, "Scene item '%s' in scene '%s' transformed.\n", cmd.ItemName, cmd.SceneName)
fmt.Fprintf(ctx.Out, "Scene item %s in scene %s transformed.\n", ctx.Style.Highlight(cmd.ItemName), ctx.Style.Highlight(cmd.SceneName))
}
return nil

View File

@ -36,6 +36,6 @@ func (cmd *ScreenshotSaveCmd) Run(ctx *context) error {
return fmt.Errorf("failed to take screenshot: %w", err)
}
fmt.Fprintf(ctx.Out, "Screenshot saved to %s.\n", cmd.FilePath)
fmt.Fprintf(ctx.Out, "Screenshot saved to %s.\n", ctx.Style.Highlight(cmd.FilePath))
return nil
}