first commit

This commit is contained in:
i.smyshlyaev
2025-10-16 12:46:23 +03:00
commit 9c813d4754
37 changed files with 1671 additions and 0 deletions

42
cli/internal/entity/vm.go Normal file
View File

@@ -0,0 +1,42 @@
package entity
import (
"awesome_cli/internal/ui"
)
type VirtualMachineOutput struct {
UUID string `json:"vm_uuid,omitempty"`
Status VMStatus `json:"status,omitempty"`
Name string `json:"name,omitempty"`
Autostart bool `json:"autostart,omitempty"`
CPU int `json:"cpu,omitempty"`
DatastoreName string `json:"datastore_name,omitempty"`
Memory int `json:"memory,omitempty"`
}
type VMStatus int
const (
Creating VMStatus = iota + 1
Running
Stopped
Failed
Paused
)
func (s VMStatus) String() string {
switch s {
case Creating:
return ui.SetColor(ui.Yellow, "Creating")
case Running:
return ui.SetColor(ui.Green, "Running")
case Stopped:
return ui.SetColor(ui.Red, "Stopped")
case Failed:
return ui.SetColor(ui.Red, "Failed")
case Paused:
return ui.SetColor(ui.Yellow, "Paused")
default:
return "Unknown"
}
}

View File

@@ -0,0 +1,37 @@
package service
import (
"awesome_cli/internal/entity"
"github.com/gofrs/uuid"
)
type VM struct {
}
func VMService() *VM {
return &VM{}
}
func (vm *VM) List() []entity.VirtualMachineOutput {
u, _ := uuid.NewV4()
return []entity.VirtualMachineOutput{
{
UUID: u.String(),
Status: 1,
Name: "Awesome VM",
Autostart: false,
CPU: 8,
DatastoreName: "datastore",
Memory: 8,
},
{
UUID: u.String(),
Status: 2,
Name: "Awesome VM2",
Autostart: true,
CPU: 18,
DatastoreName: "datastore2",
Memory: 8,
},
}
}

View 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
}

View 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
View 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)
}
}
}
}

View 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
}

35
cli/internal/views/vm.go Normal file
View File

@@ -0,0 +1,35 @@
package views
import (
"fmt"
"strconv"
"awesome_cli/internal/entity"
"awesome_cli/internal/ui"
)
func List(vms []entity.VirtualMachineOutput) {
var runCount int
t := ui.NewTableBuilder()
t.AddTableHeader("Status", "Name", "vCPU", "Memory", "Datastore")
for _, vm := range vms {
t.AddRow(
vm.Status.String(),
vm.Name,
strconv.FormatInt(int64(vm.CPU), 10),
strconv.FormatInt(int64(vm.Memory), 10),
vm.DatastoreName,
)
if vm.Status == entity.Running {
runCount++
}
}
header := ui.Header("Virtual machines")
count := ui.Info(fmt.Sprintf("Total: %d", len(vms)))
running := ui.Info(fmt.Sprintf("Run: %d", runCount))
fmt.Printf("%s\n\n", ui.StringBar(header, count, running))
fmt.Println(t.CreateTable())
}