mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-20 14:11:11 +02:00
Allows configuring: - cidr. - dhcp enable/disable. - MTU. - Ignore. - Dhcp metric. Signed-off-by: Artem Chernyshev <artem.0xD2@gmail.com>
348 lines
7.7 KiB
Go
348 lines
7.7 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package components
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
var hline string
|
|
|
|
func init() {
|
|
for i := 0; i < 2000; i++ {
|
|
hline += string(tcell.RuneHLine)
|
|
}
|
|
}
|
|
|
|
// Separator hline with a description below.
|
|
type Separator struct {
|
|
*tview.TextView
|
|
}
|
|
|
|
// GetHeight implements Multiline interface.
|
|
func (s *Separator) GetHeight() int {
|
|
return 2
|
|
}
|
|
|
|
// NewSeparator creates new form special form item which can be used as a sections Separator.
|
|
func NewSeparator(description string) *Item {
|
|
return NewItem("", "", func(item *Item) tview.Primitive {
|
|
s := &Separator{
|
|
tview.NewTextView(),
|
|
}
|
|
s.SetText(hline + "\n" + description)
|
|
s.SetWrap(false)
|
|
|
|
return s
|
|
})
|
|
}
|
|
|
|
// Item represents a single form item.
|
|
type Item struct {
|
|
Name string
|
|
description string
|
|
dest interface{}
|
|
options []interface{}
|
|
}
|
|
|
|
// TableHeaders represents table headers list for item options which are using table representation.
|
|
type TableHeaders []interface{}
|
|
|
|
// NewTableHeaders creates TableHeaders object.
|
|
func NewTableHeaders(headers ...interface{}) TableHeaders {
|
|
return TableHeaders(headers)
|
|
}
|
|
|
|
// NewItem creates new form item.
|
|
func NewItem(name, description string, dest interface{}, options ...interface{}) *Item {
|
|
return &Item{
|
|
Name: name,
|
|
dest: dest,
|
|
description: description,
|
|
options: options,
|
|
}
|
|
}
|
|
|
|
func (item *Item) assign(value string) error {
|
|
// rely on yaml parser to decode value into the right type
|
|
return yaml.Unmarshal([]byte(value), item.dest)
|
|
}
|
|
|
|
// createFormItems dynamically creates tview.FormItem list based on the wrapped type.
|
|
// nolint:gocyclo
|
|
func (item *Item) createFormItems() ([]tview.Primitive, error) {
|
|
res := []tview.Primitive{}
|
|
|
|
v := reflect.ValueOf(item.dest)
|
|
if v.Kind() == reflect.Ptr {
|
|
v = v.Elem()
|
|
}
|
|
|
|
var formItem tview.Primitive
|
|
|
|
label := fmt.Sprintf("[::b]%s[::-]:", item.Name)
|
|
addDescription := true
|
|
|
|
// nolint:exhaustive
|
|
switch v.Kind() {
|
|
case reflect.Func:
|
|
if f, ok := item.dest.(func(*Item) tview.Primitive); ok {
|
|
formItem = f(item)
|
|
}
|
|
case reflect.Bool:
|
|
// use checkbox for boolean fields
|
|
checkbox := tview.NewCheckbox()
|
|
checkbox.SetChangedFunc(func(checked bool) {
|
|
v.Set(reflect.ValueOf(checked))
|
|
})
|
|
checkbox.SetChecked(v.Bool())
|
|
checkbox.SetLabel(label)
|
|
formItem = checkbox
|
|
default:
|
|
if len(item.options) > 0 {
|
|
tableHeaders, ok := item.options[0].(TableHeaders)
|
|
if ok {
|
|
table := NewTable()
|
|
table.SetHeader(tableHeaders...)
|
|
|
|
addDescription = false
|
|
|
|
data := item.options[1:]
|
|
numColumns := len(tableHeaders)
|
|
|
|
if len(data)%numColumns != 0 {
|
|
return nil, fmt.Errorf("incorrect amount of data provided for the table")
|
|
}
|
|
|
|
selected := -1
|
|
|
|
for i := 0; i < len(data); i += numColumns {
|
|
table.AddRow(data[i : i+numColumns]...)
|
|
|
|
if v.Interface() == data[i] {
|
|
selected = i / numColumns
|
|
}
|
|
}
|
|
|
|
if selected != -1 {
|
|
table.SelectRow(selected)
|
|
}
|
|
|
|
formItem = table
|
|
table.SetRowSelectedFunc(func(row int) {
|
|
v.Set(reflect.ValueOf(table.GetValue(row, 0))) // always pick the first column
|
|
})
|
|
} else {
|
|
dropdown := tview.NewDropDown()
|
|
|
|
if len(item.options)%2 != 0 {
|
|
return nil, fmt.Errorf("wrong amount of arguments for options: should be even amount of key, value pairs")
|
|
}
|
|
|
|
for i := 0; i < len(item.options); i += 2 {
|
|
if optionName, ok := item.options[i].(string); ok {
|
|
selected := -1
|
|
|
|
func(index int) {
|
|
dropdown.AddOption(optionName, func() {
|
|
v.Set(reflect.ValueOf(item.options[index]))
|
|
})
|
|
|
|
if v.Interface() == item.options[index] {
|
|
selected = i / 2
|
|
}
|
|
}(i + 1)
|
|
|
|
if selected != -1 {
|
|
dropdown.SetCurrentOption(selected)
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("expected string option name, got %s", item.options[i])
|
|
}
|
|
}
|
|
|
|
dropdown.SetLabel(label)
|
|
|
|
formItem = dropdown
|
|
}
|
|
} else {
|
|
input := tview.NewInputField()
|
|
formItem = input
|
|
input.SetLabel(label)
|
|
text, err := yaml.Marshal(item.dest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
input.SetText(string(text))
|
|
input.SetChangedFunc(func(text string) {
|
|
if err := item.assign(text); err != nil {
|
|
// TODO: highlight red
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
res = append(res, formItem)
|
|
|
|
if item.description != "" && addDescription {
|
|
parts := strings.Split(item.description, "\n")
|
|
for _, part := range parts {
|
|
desc := NewFormLabel(part)
|
|
res = append(res, desc)
|
|
}
|
|
}
|
|
|
|
res = append(res, NewFormLabel(""))
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// NewForm creates a new form.
|
|
func NewForm(app *tview.Application) *Form {
|
|
f := &Form{
|
|
Flex: tview.NewFlex().SetDirection(tview.FlexRow),
|
|
formItems: []tview.FormItem{},
|
|
form: tview.NewFlex().SetDirection(tview.FlexRow),
|
|
buttons: tview.NewFlex(),
|
|
group: NewGroup(app),
|
|
}
|
|
|
|
f.Flex.AddItem(f.form, 0, 1, false)
|
|
f.Flex.AddItem(f.buttons, 0, 0, false)
|
|
|
|
f.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
|
// nolint:exhaustive
|
|
switch e.Key() {
|
|
case tcell.KeyTAB:
|
|
f.group.NextFocus()
|
|
case tcell.KeyBacktab:
|
|
f.group.PrevFocus()
|
|
}
|
|
|
|
return e
|
|
})
|
|
|
|
return f
|
|
}
|
|
|
|
// Form is a more flexible form component for tview lib.
|
|
type Form struct {
|
|
*tview.Flex
|
|
form *tview.Flex
|
|
buttons *tview.Flex
|
|
formItems []tview.FormItem
|
|
maxLabelLen int
|
|
hasMenuButton bool
|
|
group *Group
|
|
}
|
|
|
|
// AddFormItem adds a new item to the form.
|
|
func (f *Form) AddFormItem(item tview.Primitive) {
|
|
if formItem, ok := item.(tview.FormItem); ok {
|
|
f.formItems = append(f.formItems, formItem)
|
|
labelLen := tview.TaggedStringWidth(formItem.GetLabel()) + 1
|
|
|
|
if labelLen > f.maxLabelLen {
|
|
for _, item := range f.formItems[:len(f.formItems)-1] {
|
|
item.SetFormAttributes(
|
|
labelLen,
|
|
tview.Styles.PrimaryTextColor,
|
|
f.GetBackgroundColor(),
|
|
tview.Styles.PrimaryTextColor,
|
|
tview.Styles.ContrastBackgroundColor,
|
|
)
|
|
}
|
|
|
|
f.maxLabelLen = labelLen
|
|
}
|
|
|
|
formItem.SetFormAttributes(
|
|
f.maxLabelLen,
|
|
tview.Styles.PrimaryTextColor,
|
|
f.GetBackgroundColor(),
|
|
tview.Styles.PrimaryTextColor,
|
|
tview.Styles.ContrastBackgroundColor,
|
|
)
|
|
} else if box, ok := item.(Box); ok {
|
|
box.SetBackgroundColor(f.GetBackgroundColor())
|
|
}
|
|
|
|
height := 1
|
|
multiline, ok := item.(Multiline)
|
|
|
|
if ok {
|
|
height = multiline.GetHeight()
|
|
}
|
|
|
|
f.form.AddItem(item, height, 1, false)
|
|
|
|
switch item.(type) {
|
|
case *FormLabel:
|
|
case *Separator:
|
|
default:
|
|
f.group.AddElement(item)
|
|
}
|
|
}
|
|
|
|
// Focus overrides default focus behavior.
|
|
func (f *Form) Focus(delegate func(tview.Primitive)) {
|
|
f.group.FocusFirst()
|
|
}
|
|
|
|
// AddMenuButton adds a button to the menu at the bottom of the form.
|
|
func (f *Form) AddMenuButton(label string, alignRight bool) *tview.Button {
|
|
b := tview.NewButton(label)
|
|
|
|
if f.hasMenuButton || alignRight {
|
|
f.buttons.AddItem(
|
|
tview.NewBox().SetBackgroundColor(f.GetBackgroundColor()),
|
|
0,
|
|
1,
|
|
false,
|
|
)
|
|
}
|
|
|
|
f.ResizeItem(f.buttons, 3, 0)
|
|
|
|
f.buttons.AddItem(b, tview.TaggedStringWidth(label)+4, 0, false)
|
|
f.hasMenuButton = true
|
|
f.group.AddElement(b)
|
|
|
|
return b
|
|
}
|
|
|
|
// AddFormItems constructs form from data represented as a list of Item objects.
|
|
func (f *Form) AddFormItems(items []*Item) error {
|
|
for _, item := range items {
|
|
formItems, e := item.createFormItems()
|
|
if e != nil {
|
|
return e
|
|
}
|
|
|
|
for _, formItem := range formItems {
|
|
f.AddFormItem(formItem)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Multiline interface represents elements that can occupy more than one line.
|
|
type Multiline interface {
|
|
GetHeight() int
|
|
}
|
|
|
|
// Box interface that has just SetBackgroundColor.
|
|
type Box interface {
|
|
SetBackgroundColor(tcell.Color) *tview.Box
|
|
}
|