first commit
This commit is contained in:
166
cli/internal/ui/interactive.go
Normal file
166
cli/internal/ui/interactive.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type FormBuilder struct {
|
||||
widgets []huh.Field
|
||||
form huh.Form
|
||||
}
|
||||
|
||||
func NewForm() *FormBuilder {
|
||||
return &FormBuilder{}
|
||||
}
|
||||
|
||||
func (f *FormBuilder) AddWidget(w huh.Field) {
|
||||
f.widgets = append(f.widgets, w)
|
||||
}
|
||||
|
||||
func (f *FormBuilder) Group(w ...huh.Field) *huh.Group {
|
||||
group := huh.NewGroup(w...)
|
||||
return group
|
||||
}
|
||||
|
||||
func (f *FormBuilder) BuildGroups(g ...*huh.Group) *huh.Form {
|
||||
form := huh.NewForm(g...)
|
||||
return form
|
||||
}
|
||||
|
||||
func (f *FormBuilder) Build() *huh.Form {
|
||||
form := huh.NewForm(huh.NewGroup(f.widgets...)).WithTheme(ThemeCustom())
|
||||
return form
|
||||
}
|
||||
|
||||
func (f *FormBuilder) GetString(key string) string {
|
||||
fmt.Println(f.form.GetString(key))
|
||||
return f.form.GetString(key)
|
||||
}
|
||||
|
||||
func (f *FormBuilder) GetBool(key string) bool {
|
||||
fmt.Println(f.form.GetString(key))
|
||||
return f.form.GetBool(key)
|
||||
}
|
||||
|
||||
func (f *FormBuilder) GetInt(key string) int64 {
|
||||
v := f.form.GetString(key)
|
||||
fmt.Println(v)
|
||||
value, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatalf(fmt.Sprintf("failed to convert value %s to integer", v))
|
||||
return 0
|
||||
}
|
||||
return int64(value)
|
||||
}
|
||||
|
||||
func MultiSelect(title, key string, options []string) *huh.MultiSelect[string] {
|
||||
s := huh.NewMultiSelect[string]().
|
||||
Title(title).
|
||||
Options(huh.NewOptions[string](options...)...).
|
||||
Key(key)
|
||||
return s
|
||||
}
|
||||
|
||||
func InteractiveList(title, key string, opts map[string]string) *huh.Select[string] {
|
||||
var pick string
|
||||
var options []huh.Option[string]
|
||||
s := huh.NewSelect[string]().
|
||||
Title(title).
|
||||
Key(key).
|
||||
Value(&pick)
|
||||
if opts != nil {
|
||||
for k, v := range opts {
|
||||
options = append(options, huh.Option[string]{Key: k, Value: v})
|
||||
}
|
||||
|
||||
s.Options(options...)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func Input(question string, key string) *huh.Input {
|
||||
var answer string
|
||||
input := huh.NewInput().
|
||||
Title(fmt.Sprintf("%s", question)).
|
||||
Prompt("> ").
|
||||
Validate(ValidateString).
|
||||
Key(key).
|
||||
Value(&answer)
|
||||
return input
|
||||
}
|
||||
|
||||
func Confirm(question string, key string) *huh.Confirm {
|
||||
var happy bool
|
||||
confirm := huh.NewConfirm().
|
||||
Title(question).
|
||||
Affirmative("Yes").
|
||||
Negative("No").
|
||||
Value(&happy).
|
||||
Key(key)
|
||||
|
||||
return confirm
|
||||
}
|
||||
|
||||
func Text(title, desc string) *huh.Note {
|
||||
t := huh.NewNote().
|
||||
Title(title).
|
||||
Description(desc)
|
||||
return t
|
||||
}
|
||||
|
||||
func TextField(title, key string) *huh.Text {
|
||||
t := huh.NewText().
|
||||
Title(title).Key(key).ShowLineNumbers(true).Editor("vim")
|
||||
return t
|
||||
}
|
||||
|
||||
func ThemeCustom() *huh.Theme {
|
||||
t := huh.ThemeBase()
|
||||
|
||||
var (
|
||||
normalFg = lipgloss.AdaptiveColor{Light: "235", Dark: "252"}
|
||||
// indigo = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"}
|
||||
indigo = lipgloss.AdaptiveColor{Light: "32", Dark: "33"}
|
||||
cream = lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}
|
||||
// fuchsia = lipgloss.Color("#F780E2")
|
||||
green = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"}
|
||||
red = lipgloss.AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"}
|
||||
)
|
||||
|
||||
t.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color("238"))
|
||||
t.Focused.Title = t.Focused.Title.Foreground(White).Bold(true)
|
||||
t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(White).Bold(true).MarginBottom(1)
|
||||
t.Focused.Directory = t.Focused.Directory.Foreground(indigo)
|
||||
t.Focused.Description = t.Focused.Description.Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"})
|
||||
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red)
|
||||
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red)
|
||||
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(green)
|
||||
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(indigo)
|
||||
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(indigo)
|
||||
t.Focused.Option = t.Focused.Option.Foreground(normalFg)
|
||||
t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(green)
|
||||
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(green)
|
||||
t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(green).SetString("✓ ")
|
||||
t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}).SetString("• ")
|
||||
t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(normalFg)
|
||||
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(cream).Background(indigo)
|
||||
t.Focused.Next = t.Focused.FocusedButton
|
||||
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "252", Dark: "237"})
|
||||
|
||||
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(Gray)
|
||||
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"})
|
||||
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(green)
|
||||
|
||||
t.Blurred = t.Focused
|
||||
t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||
t.Blurred.NextIndicator = lipgloss.NewStyle()
|
||||
t.Blurred.PrevIndicator = lipgloss.NewStyle()
|
||||
|
||||
return t
|
||||
}
|
||||
84
cli/internal/ui/question.go
Normal file
84
cli/internal/ui/question.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
proceedTemplate = "%s (y/n): "
|
||||
defaultTemplate = "%s: "
|
||||
chooseTemplate = "%d : %s\n"
|
||||
)
|
||||
|
||||
func AskToConfirm(question, affirmative, negative string) bool {
|
||||
confirm := Confirm(question, "res")
|
||||
confirm.Affirmative(affirmative)
|
||||
confirm.Negative(negative)
|
||||
form := huh.NewForm(huh.NewGroup(confirm))
|
||||
form.Run()
|
||||
|
||||
if form.State != huh.StateCompleted {
|
||||
log.Fatal("canceled")
|
||||
}
|
||||
|
||||
return form.GetBool("res")
|
||||
}
|
||||
|
||||
func AskToValue(question string) (string, error) {
|
||||
input := Input(question, "res")
|
||||
form := huh.NewForm(huh.NewGroup(input))
|
||||
form.Run()
|
||||
|
||||
if form.State != huh.StateCompleted {
|
||||
log.Fatal("canceled")
|
||||
}
|
||||
|
||||
return form.GetString("res"), nil
|
||||
}
|
||||
|
||||
func AskToInt(question string) (int, error) {
|
||||
v, err := AskToValue(question)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
value, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func AskToChoose(question string, list map[int]string) string {
|
||||
for i, item := range list {
|
||||
fmt.Printf(chooseTemplate, i, item)
|
||||
}
|
||||
|
||||
answer, err := AskToInt(question)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to read the answer")
|
||||
}
|
||||
|
||||
return list[answer]
|
||||
}
|
||||
|
||||
func AskToChooseInteractive(question string, list []string) string {
|
||||
values := make(map[string]string)
|
||||
if len(list) == 0 {
|
||||
log.Fatal("no items found")
|
||||
}
|
||||
for _, item := range list {
|
||||
values[item] = item
|
||||
}
|
||||
widget := InteractiveList(question, "res", values)
|
||||
form := huh.NewForm(huh.NewGroup(widget)).WithShowHelp(false)
|
||||
form.Run()
|
||||
|
||||
if form.State != huh.StateCompleted {
|
||||
log.Fatal("canceled")
|
||||
}
|
||||
return form.GetString("res")
|
||||
}
|
||||
155
cli/internal/ui/render.go
Normal file
155
cli/internal/ui/render.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/lipgloss/table"
|
||||
terminal "golang.org/x/term"
|
||||
)
|
||||
|
||||
var (
|
||||
// ANSI256 256 colors, 8-bit
|
||||
|
||||
Blue = lipgloss.Color("21")
|
||||
Magenta = lipgloss.Color("13")
|
||||
Gray = lipgloss.Color("238")
|
||||
White = lipgloss.Color("15")
|
||||
Red = lipgloss.Color("9")
|
||||
Green = lipgloss.Color("10")
|
||||
Yellow = lipgloss.Color("11")
|
||||
|
||||
// styling
|
||||
headerStyle = lipgloss.NewStyle().
|
||||
Background(Blue).
|
||||
Foreground(White).
|
||||
Padding(0, 1).
|
||||
Italic(true)
|
||||
infoStyle = headerStyle.Copy().
|
||||
Foreground(White).
|
||||
Background(Gray)
|
||||
subHeaderStyle = headerStyle.Copy().
|
||||
Background(lipgloss.NoColor{})
|
||||
errorStyle = lipgloss.NewStyle().Foreground(Red)
|
||||
)
|
||||
|
||||
type TableBuilder struct {
|
||||
tableHeader []string
|
||||
rows [][]string
|
||||
Table *table.Table
|
||||
padding int
|
||||
}
|
||||
|
||||
func NewTableBuilder() *TableBuilder {
|
||||
return &TableBuilder{
|
||||
Table: table.New(),
|
||||
padding: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TableBuilder) Reset() {
|
||||
t.tableHeader = nil
|
||||
t.rows = nil
|
||||
t.Table = table.New()
|
||||
}
|
||||
|
||||
func (t *TableBuilder) AddTableHeader(strs ...string) {
|
||||
t.tableHeader = append(t.tableHeader, strs...)
|
||||
}
|
||||
|
||||
func (t *TableBuilder) AddRow(strs ...string) {
|
||||
t.rows = append(t.rows, strs)
|
||||
}
|
||||
|
||||
func (t *TableBuilder) Width(w int) {
|
||||
t.Table.Width(w)
|
||||
}
|
||||
|
||||
func (t *TableBuilder) colWidth(colNumber int) int {
|
||||
var maxWidth int
|
||||
for _, row := range t.rows {
|
||||
for col, v := range row {
|
||||
if col == colNumber && maxWidth < len(v)+t.padding*2 {
|
||||
maxWidth = len(v) + t.padding*2
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxWidth
|
||||
}
|
||||
|
||||
func (t *TableBuilder) CreateTable() *table.Table {
|
||||
width, _, _ := terminal.GetSize(0)
|
||||
t.Table.
|
||||
BorderRow(false).BorderColumn(false).
|
||||
BorderLeft(false).BorderRight(false).BorderTop(false).BorderBottom(false).
|
||||
//Border(lipgloss.RoundedBorder()).
|
||||
//BorderRow(true).
|
||||
//BorderStyle(BorderStyle).
|
||||
StyleFunc(func(row, col int) lipgloss.Style {
|
||||
re := lipgloss.NewRenderer(os.Stdout)
|
||||
if row == -1 {
|
||||
// Align(lipgloss.Center)
|
||||
return re.NewStyle().Bold(true).Align(lipgloss.Left).Padding(0, 2, 0, 0).Foreground(Green)
|
||||
}
|
||||
return re.NewStyle().Padding(0, 2, 0, 0).Align(lipgloss.Left)
|
||||
}).
|
||||
Headers(t.tableHeader...).
|
||||
Rows(t.rows...)
|
||||
|
||||
switch {
|
||||
case width < 100:
|
||||
t.Table.Width(width)
|
||||
case len(t.tableHeader) > 8:
|
||||
t.Table.Width(width)
|
||||
}
|
||||
|
||||
return t.Table
|
||||
}
|
||||
|
||||
// Justify Выравнивание по ширине терминала
|
||||
func (t *TableBuilder) Justify() {
|
||||
width, _, _ := terminal.GetSize(0)
|
||||
t.Table.Width(width)
|
||||
}
|
||||
|
||||
func Header(s string) string {
|
||||
return headerStyle.Render(s)
|
||||
}
|
||||
|
||||
func SubHeader(s string) string {
|
||||
return subHeaderStyle.Render(s)
|
||||
}
|
||||
|
||||
func Info(s string) string {
|
||||
return infoStyle.Render(s)
|
||||
}
|
||||
|
||||
func StringBar(strs ...string) string {
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, strs...)
|
||||
}
|
||||
|
||||
func ErrorBar(s string) string {
|
||||
return errorStyle.Render(s)
|
||||
}
|
||||
|
||||
func SetColor(color lipgloss.Color, str string) string {
|
||||
return lipgloss.NewStyle().Foreground(color).Render(str)
|
||||
}
|
||||
|
||||
func Spinner(stop chan bool, description string) {
|
||||
spinner := []rune{'|', '\\', '-', '/'}
|
||||
for {
|
||||
for _, r := range spinner {
|
||||
select {
|
||||
case <-stop:
|
||||
fmt.Printf("\r")
|
||||
return
|
||||
default:
|
||||
fmt.Printf("\r%c %s", r, description)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
cli/internal/ui/validate.go
Normal file
14
cli/internal/ui/validate.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func ValidateString(s string) error {
|
||||
validRegex := regexp.MustCompile(`^[a-zA-Z0-9_.]+$`)
|
||||
if !validRegex.MatchString(s) {
|
||||
return errors.New("invalid input")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user