Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 107 additions & 8 deletions console/cli_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/charmbracelet/huh"
"github.com/charmbracelet/huh/spinner"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/pterm/pterm"
"github.com/spf13/cast"
"github.com/urfave/cli/v3"
Expand Down Expand Up @@ -45,7 +46,7 @@ func (r *CliContext) Ask(question string, option ...console.AskOption) (string,
}
}

err := input.Value(&answer).Run()
err := input.Value(&answer).WithTheme(GlobalHuhTheme).Run()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a screenshot for the theme?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image image image image

if err != nil {
return "", err
}
Expand All @@ -59,7 +60,7 @@ func (r *CliContext) Ask(question string, option ...console.AskOption) (string,
}
}

err := input.Value(&answer).Run()
err := input.Value(&answer).WithTheme(GlobalHuhTheme).Run()
if err != nil {
return "", err
}
Expand Down Expand Up @@ -100,7 +101,7 @@ func (r *CliContext) Choice(question string, choices []console.Choice, option ..
}
}

err := huh.NewForm(huh.NewGroup(input.Value(&answer))).Run()
err := huh.NewForm(huh.NewGroup(input.Value(&answer))).WithTheme(GlobalHuhTheme).Run()
if err != nil {
return "", err
}
Expand All @@ -127,7 +128,7 @@ func (r *CliContext) Confirm(question string, option ...console.ConfirmOption) b
answer = option[0].Default
}

if err := input.Value(&answer).Run(); err != nil {
if err := input.Value(&answer).WithTheme(GlobalHuhTheme).Run(); err != nil {
r.Error(err.Error())

return false
Expand Down Expand Up @@ -172,7 +173,7 @@ func (r *CliContext) MultiSelect(question string, choices []console.Choice, opti
}
}

err := huh.NewForm(huh.NewGroup(input.Value(&answer))).Run()
err := huh.NewForm(huh.NewGroup(input.Value(&answer))).WithTheme(GlobalHuhTheme).Run()
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -529,7 +530,7 @@ func (r *CliContext) Secret(question string, option ...console.SecretOption) (st
}
}

err := input.Value(&answer).Run()
err := input.Value(&answer).WithTheme(GlobalHuhTheme).Run()
if err != nil {
return "", err
}
Expand All @@ -538,8 +539,10 @@ func (r *CliContext) Secret(question string, option ...console.SecretOption) (st
}

func (r *CliContext) Spinner(message string, option console.SpinnerOption) error {
style := lipgloss.NewStyle().Foreground(lipgloss.CompleteColor{TrueColor: "#3D8C8D", ANSI256: "30", ANSI: "6"})
spin := spinner.New().Title(message).Style(style).TitleStyle(style)
spin := spinner.New().
Title(message).
Style(DefaultSpinnerStyle).
TitleStyle(DefaultSpinnerTitleStyle)

var err error
if err := spin.Context(option.Ctx).Action(func() {
Expand All @@ -555,6 +558,102 @@ func (r *CliContext) Success(message string) {
color.Successln(message)
}

func (r *CliContext) Table(headers []string, rows [][]string, option ...console.TableOption) {
t := table.New().Headers(headers...).Rows(rows...)

opt := DefaultTableOption
if len(option) > 0 {
userOpt := option[0]
if (userOpt.Border != lipgloss.Border{}) {
opt.Border = userOpt.Border
}
if userOpt.BorderStyle.Value() != "" {
opt.BorderStyle = userOpt.BorderStyle
}
if userOpt.StyleFunc != nil {
opt.StyleFunc = userOpt.StyleFunc
}
if userOpt.BorderTop != nil {
opt.BorderTop = userOpt.BorderTop
}
if userOpt.BorderBottom != nil {
opt.BorderBottom = userOpt.BorderBottom
}
if userOpt.BorderLeft != nil {
opt.BorderLeft = userOpt.BorderLeft
}
if userOpt.BorderRight != nil {
opt.BorderRight = userOpt.BorderRight
}
if userOpt.BorderHeader != nil {
opt.BorderHeader = userOpt.BorderHeader
}
if userOpt.BorderColumn != nil {
opt.BorderColumn = userOpt.BorderColumn
}
if userOpt.BorderRow != nil {
opt.BorderRow = userOpt.BorderRow
}
if userOpt.Width > 0 {
opt.Width = userOpt.Width
}
if userOpt.Height > 0 {
opt.Height = userOpt.Height
}
if userOpt.ColumnStyles != nil {
opt.ColumnStyles = userOpt.ColumnStyles
}
}

t.Border(opt.Border).BorderStyle(opt.BorderStyle)

if opt.BorderTop != nil {
t.BorderTop(*opt.BorderTop)
}
if opt.BorderBottom != nil {
t.BorderBottom(*opt.BorderBottom)
}
if opt.BorderLeft != nil {
t.BorderLeft(*opt.BorderLeft)
}
if opt.BorderRight != nil {
t.BorderRight(*opt.BorderRight)
}
if opt.BorderHeader != nil {
t.BorderHeader(*opt.BorderHeader)
}
if opt.BorderColumn != nil {
t.BorderColumn(*opt.BorderColumn)
}
if opt.BorderRow != nil {
t.BorderRow(*opt.BorderRow)
}

if opt.Width > 0 {
t.Width(opt.Width)
}
if opt.Height > 0 {
t.Height(opt.Height)
}

if opt.StyleFunc == nil {
opt.StyleFunc = DefaultTableStyleFunc
}
t.StyleFunc(func(row, col int) lipgloss.Style {
style := opt.StyleFunc(row, col)

if opt.ColumnStyles != nil {
if colStyle, ok := opt.ColumnStyles[col]; ok {
style = style.Inherit(colStyle)
}
}

return style
})

r.Line(t.Render())
}

func (r *CliContext) Warning(message string) {
color.Warningln(message)
}
Expand Down
59 changes: 59 additions & 0 deletions console/cli_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import (
"testing"
"time"

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/pterm/pterm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"

"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/support/color"
"github.com/goravel/framework/support/convert"
)

func TestArgumentString(t *testing.T) {
Expand Down Expand Up @@ -2069,3 +2073,58 @@ func TestColors(t *testing.T) {
})
}
}

func TestTable(t *testing.T) {
headers := []string{"ID", "Name"}
rows := [][]string{{"1", "Goravel"}, {"2", "Framework"}}

testCases := []struct {
name string
option console.TableOption
contains []string
}{
{
name: "default table",
option: console.TableOption{},
contains: []string{
"ID", "Name", "Goravel",
"╭", "─", "╮",
},
},
{
name: "compact style using boolean flags",
option: console.TableOption{
BorderTop: convert.Pointer(false),
BorderBottom: convert.Pointer(false),
BorderLeft: convert.Pointer(false),
BorderRight: convert.Pointer(false),
},
contains: []string{"ID", "Name", "│"},
},
{
name: "custom style func",
option: console.TableOption{
StyleFunc: func(row, col int) lipgloss.Style {
if row == table.HeaderRow {
return lipgloss.NewStyle().Bold(true)
}
return lipgloss.NewStyle()
},
},
contains: []string{"ID", "Name", "Goravel"},
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := CliContext{}
got := color.CaptureOutput(func(io.Writer) {
ctx.Table(headers, rows, tt.option)
})

for _, s := range tt.contains {
assert.Contains(t, got, s, "Output should contain expected table element")
}
})
}
}
4 changes: 2 additions & 2 deletions console/progress_bar.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ type ProgressBar struct {
func NewProgressBar(total int) *ProgressBar {
return &ProgressBar{
instance: pterm.DefaultProgressbar.WithTotal(total).
WithBarStyle(pterm.NewStyle(pterm.FgLightGreen)).
WithTitleStyle(pterm.NewStyle(pterm.FgWhite)),
WithBarStyle(DefaultProgressBarStyle).
WithTitleStyle(DefaultProgressTitleStyle),
}
}

Expand Down
68 changes: 68 additions & 0 deletions console/style.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package console

import (
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/pterm/pterm"

"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/support/convert"
)

var (
BrandColor = lipgloss.CompleteColor{TrueColor: "#3D8C8D", ANSI256: "30", ANSI: "6"}
MutedColor = lipgloss.CompleteColor{TrueColor: "#4a4a4a", ANSI256: "240", ANSI: "8"}
WhiteColor = lipgloss.CompleteColor{TrueColor: "#ffffff", ANSI256: "255", ANSI: "15"}

DefaultProgressBarStyle = pterm.NewStyle(pterm.FgLightGreen)
DefaultProgressTitleStyle = pterm.NewStyle(pterm.FgWhite)

DefaultTableHeaderColor = BrandColor
DefaultTableBorderColor = MutedColor

DefaultTableHeaderStyle = lipgloss.NewStyle().Foreground(DefaultTableHeaderColor).Bold(true).Padding(0, 1)
DefaultTableCellStyle = lipgloss.NewStyle().Padding(0, 1)
DefaultSpinnerStyle = lipgloss.NewStyle().Foreground(BrandColor)
DefaultSpinnerTitleStyle = lipgloss.NewStyle().Foreground(BrandColor)

DefaultTableStyleFunc = func(row, col int) lipgloss.Style {
if row == table.HeaderRow {
return DefaultTableHeaderStyle
}
return DefaultTableCellStyle
}

DefaultTableOption = console.TableOption{
Border: lipgloss.RoundedBorder(),

BorderStyle: lipgloss.NewStyle().Foreground(DefaultTableBorderColor),

StyleFunc: DefaultTableStyleFunc,

BorderTop: convert.Pointer(true),
BorderBottom: convert.Pointer(true),
BorderLeft: convert.Pointer(true),
BorderRight: convert.Pointer(true),
BorderHeader: convert.Pointer(true),
BorderColumn: convert.Pointer(true),
BorderRow: convert.Pointer(false),
}

GlobalHuhTheme = func() *huh.Theme {
t := huh.ThemeCharm()
t.Focused.Title = t.Focused.Title.Foreground(BrandColor)
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(BrandColor)
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(BrandColor)
t.Focused.Base = t.Focused.Base.BorderForeground(BrandColor)

t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(BrandColor)
t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(BrandColor)

t.Focused.FocusedButton = t.Focused.FocusedButton.
Background(BrandColor).
Foreground(WhiteColor)

return t
}()
)
36 changes: 36 additions & 0 deletions contracts/console/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"time"

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/urfave/cli/v3"

"github.com/goravel/framework/contracts/console/command"
Expand Down Expand Up @@ -127,6 +129,8 @@ type Context interface {
Spinner(message string, option SpinnerOption) error
// Success writes a success message to the console.
Success(message string)
// Table renders a formatted table to the console.
Table(headers []string, rows [][]string, option ...TableOption)
// Warning writes a warning message to the console.
Warning(message string)
// WithProgressBar executes a callback with a progress bar.
Expand Down Expand Up @@ -248,3 +252,35 @@ type SpinnerOption struct {
// Action the action to execute.
Action func() error
}

type TableOption struct {
// BorderTop enables/disables the very top horizontal line.
BorderTop *bool
// BorderBottom enables/disables the very bottom horizontal line.
BorderBottom *bool
// BorderLeft enables/disables the leftmost vertical line.
BorderLeft *bool
// BorderRight enables/disables the rightmost vertical line.
BorderRight *bool
// BorderHeader enables/disables the separator line between the header and the first row.
BorderHeader *bool
// BorderColumn enables/disables vertical lines between columns.
BorderColumn *bool
// BorderRow enables/disables horizontal lines between every data row.
BorderRow *bool

// Border allows setting a custom lipgloss.Border (e.g., lipgloss.DoubleBorder()).
Border lipgloss.Border
// BorderStyle sets the color and style for the grid lines.
BorderStyle lipgloss.Style
// ColumnStyles allows specific styling for individual columns (key is column index).
// Useful for right-aligning numbers: map[int]lipgloss.Style{1: lipgloss.NewStyle().Align(lipgloss.Right)}
ColumnStyles map[int]lipgloss.Style
// StyleFunc is an escape hatch for advanced cell-by-cell styling based on content or position.
StyleFunc table.StyleFunc

// Width sets a fixed total width for the table. Columns will auto-scale to fit.
Width int
// Height sets a fixed total height for the table.
Height int
}
Loading
Loading