From 7147c3f1cac29cd570dbb3f8b125a1fc2d268387 Mon Sep 17 00:00:00 2001 From: onyx-and-iris Date: Sat, 21 Jun 2025 06:41:12 +0100 Subject: [PATCH] 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. --- filter.go | 63 +++++++++++++++------- group.go | 75 ++++++++++++++++++++------ hotkey.go | 28 +++++++--- input.go | 54 +++++++++++++------ profile.go | 62 +++++++++++++++------- projector.go | 63 ++++++++++++++++++---- record.go | 10 ++-- scene.go | 48 ++++++++++++----- scenecollection.go | 41 ++++++++++----- sceneitem.go | 128 +++++++++++++++++++++++++++++++++------------ screenshot.go | 2 +- 11 files changed, 424 insertions(+), 150 deletions(-) diff --git a/filter.go b/filter.go index 7e9b659..a4f6f38 100644 --- a/filter.go +++ b/filter.go @@ -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 } diff --git a/group.go b/group.go index e16d323..078f7bb 100644 --- a/group.go +++ b/group.go @@ -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)) } diff --git a/hotkey.go b/hotkey.go index 90bf9b1..42b164b 100644 --- a/hotkey.go +++ b/hotkey.go @@ -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 } diff --git a/input.go b/input.go index 4413e23..9761068 100644 --- a/input.go +++ b/input.go @@ -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 } diff --git a/profile.go b/profile.go index 7913f0e..f1253d6 100644 --- a/profile.go +++ b/profile.go @@ -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 } diff --git a/projector.go b/projector.go index 49d7bc3..6242973 100644 --- a/projector.go +++ b/projector.go @@ -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 } diff --git a/record.go b/record.go index a5978e6..f8604ee 100644 --- a/record.go +++ b/record.go @@ -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 } diff --git a/scene.go b/scene.go index 9ff42da..7e3950a 100644 --- a/scene.go +++ b/scene.go @@ -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 } diff --git a/scenecollection.go b/scenecollection.go index e190a92..85b7cec 100644 --- a/scenecollection.go +++ b/scenecollection.go @@ -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 } diff --git a/sceneitem.go b/sceneitem.go index 29b7242..c6055e5 100644 --- a/sceneitem.go +++ b/sceneitem.go @@ -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 diff --git a/screenshot.go b/screenshot.go index 5472dd3..382e724 100644 --- a/screenshot.go +++ b/screenshot.go @@ -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 }