mirror of
https://github.com/gizak/termui.git
synced 2025-04-26 13:48:54 +08:00
feat: (widget) add Form widget
This commit is contained in:
parent
f976fe697a
commit
0b8d905cec
56
_examples/form.go
Normal file
56
_examples/form.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
ui "github.com/gizak/termui/v3"
|
||||||
|
"github.com/gizak/termui/v3/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := ui.Init(); err != nil {
|
||||||
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
|
}
|
||||||
|
defer ui.Close()
|
||||||
|
|
||||||
|
f0 := widgets.NewForm(
|
||||||
|
context.Background(),
|
||||||
|
"Add user",
|
||||||
|
func(ctx context.Context, validated bool, fields []*widgets.Field) {
|
||||||
|
ui.Close()
|
||||||
|
fmt.Printf("Form validated: %t. Values:\n", validated)
|
||||||
|
for _, f := range fields {
|
||||||
|
fmt.Printf("%s: %s\n", f.Name, f.Text)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
},
|
||||||
|
widgets.NewField("First name", ""),
|
||||||
|
widgets.NewField("Surname", ""),
|
||||||
|
widgets.NewField("Test", "Default value"),
|
||||||
|
)
|
||||||
|
f0.SetRect(0, 0, 50, 20)
|
||||||
|
|
||||||
|
p2 := widgets.NewParagraph()
|
||||||
|
p2.Title = "Multiline"
|
||||||
|
p2.Text = "Simple colored text\nwith label. It [can be](fg:red) multilined with \\n or [break automatically](fg:red,fg:bold)"
|
||||||
|
p2.SetRect(0, 50, 35, 55)
|
||||||
|
p2.BorderStyle.Fg = ui.ColorYellow
|
||||||
|
|
||||||
|
ui.Render(f0, p2)
|
||||||
|
|
||||||
|
uiEvents := ui.PollEvents()
|
||||||
|
for {
|
||||||
|
e := <-uiEvents
|
||||||
|
switch e.ID {
|
||||||
|
case "<C-c>":
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if !f0.IsDone() {
|
||||||
|
f0.Handle(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
140
widgets/form.go
Normal file
140
widgets/form.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package widgets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
. "github.com/gizak/termui/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Field struct {
|
||||||
|
Name string
|
||||||
|
Text string
|
||||||
|
cursor int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Form struct {
|
||||||
|
Paragraph
|
||||||
|
|
||||||
|
TextFgColor Style
|
||||||
|
TextBgColor Style
|
||||||
|
Fields []*Field
|
||||||
|
Done bool
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
onExitCallback func(ctx context.Context, validated bool, fields []*Field)
|
||||||
|
fieldIdx int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewField(name string, value string) *Field {
|
||||||
|
f := &Field{
|
||||||
|
Name: name,
|
||||||
|
Text: value,
|
||||||
|
}
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewForm(ctx context.Context, name string, callback func(ctx context.Context, validated bool, fields []*Field), fields ...*Field) *Form {
|
||||||
|
|
||||||
|
f := &Form{
|
||||||
|
Paragraph: *NewParagraph(),
|
||||||
|
TextFgColor: Theme.Paragraph.Text,
|
||||||
|
ctx: ctx,
|
||||||
|
onExitCallback: callback,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
f.Fields = append(f.Fields, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Border = true
|
||||||
|
f.Title = name
|
||||||
|
f._draw()
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle input event
|
||||||
|
// <Up> or <Down> to switch field
|
||||||
|
// <Enter> will switch to next field or exit scan if last field
|
||||||
|
func (f *Form) Handle(e Event) {
|
||||||
|
switch e.ID {
|
||||||
|
case "<Down>":
|
||||||
|
if f.fieldIdx < len(f.Fields)-1 {
|
||||||
|
f.fieldIdx++
|
||||||
|
}
|
||||||
|
case "<Up>":
|
||||||
|
if f.fieldIdx > 0 {
|
||||||
|
f.fieldIdx--
|
||||||
|
}
|
||||||
|
case "<Enter>":
|
||||||
|
if f.fieldIdx < len(f.Fields)-1 {
|
||||||
|
f.fieldIdx++
|
||||||
|
} else {
|
||||||
|
f.Done = true
|
||||||
|
f.onExitCallback(f.ctx, true, f.Fields)
|
||||||
|
}
|
||||||
|
case "<Esc>":
|
||||||
|
f.Done = true
|
||||||
|
f.onExitCallback(f.ctx, false, f.Fields)
|
||||||
|
default:
|
||||||
|
f.Fields[f.fieldIdx].Handle(e)
|
||||||
|
}
|
||||||
|
f._draw()
|
||||||
|
Render(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle field input
|
||||||
|
func (f *Field) Handle(e Event) {
|
||||||
|
switch e.ID {
|
||||||
|
case "<Backspace>":
|
||||||
|
if f.cursor > 0 {
|
||||||
|
f.Text = f.Text[:f.cursor-1] + f.Text[f.cursor:]
|
||||||
|
} else if len(f.Text) > 1 {
|
||||||
|
f.Text = f.Text[1:]
|
||||||
|
} else {
|
||||||
|
f.Text = ""
|
||||||
|
}
|
||||||
|
if f.cursor > 0 {
|
||||||
|
f.cursor--
|
||||||
|
}
|
||||||
|
if f.cursor > len(f.Text) {
|
||||||
|
f.cursor = len(f.Text)
|
||||||
|
}
|
||||||
|
case "<Right>":
|
||||||
|
if f.cursor < len(f.Text) {
|
||||||
|
f.cursor++
|
||||||
|
}
|
||||||
|
case "<Left>":
|
||||||
|
if f.cursor > 0 {
|
||||||
|
f.cursor--
|
||||||
|
}
|
||||||
|
case "<Space>":
|
||||||
|
f.Text = f.Text[:f.cursor] + " " + f.Text[f.cursor:]
|
||||||
|
f.cursor++
|
||||||
|
default:
|
||||||
|
f.Text = f.Text[:f.cursor] + e.ID + f.Text[f.cursor:]
|
||||||
|
f.cursor++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDone return wether user has entered <Esc> or <Enter>
|
||||||
|
func (f *Form) IsDone() bool {
|
||||||
|
return f.Done
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Form) _draw() {
|
||||||
|
cursor := "[|](bg:white)"
|
||||||
|
f.Text = ""
|
||||||
|
for i, field := range f.Fields {
|
||||||
|
txt := field.Text
|
||||||
|
if i == f.fieldIdx && field.Text != "" {
|
||||||
|
begin := field.Text[:field.cursor]
|
||||||
|
end := field.Text[field.cursor:]
|
||||||
|
txt = begin + cursor + end
|
||||||
|
} else if i == f.fieldIdx {
|
||||||
|
txt = cursor
|
||||||
|
}
|
||||||
|
f.Text += fmt.Sprintf("%s: %s\n", field.Name, txt)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user