mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
Skeleton of the TextInput widget.
This commit is contained in:
parent
ea2e0b7855
commit
bf72b5ddc2
171
widgets/textinput/options.go
Normal file
171
widgets/textinput/options.go
Normal file
@ -0,0 +1,171 @@
|
||||
// Copyright 2019 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package textinput
|
||||
|
||||
// options.go contains configurable options for TextInput.
|
||||
|
||||
import (
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/linestyle"
|
||||
)
|
||||
|
||||
// Option is used to provide options.
|
||||
type Option interface {
|
||||
// set sets the provided option.
|
||||
set(*options)
|
||||
}
|
||||
|
||||
// option implements Option.
|
||||
type option func(*options)
|
||||
|
||||
// set implements Option.set.
|
||||
func (o option) set(opts *options) {
|
||||
o(opts)
|
||||
}
|
||||
|
||||
// options holds the provided options.
|
||||
type options struct {
|
||||
fillColor cell.Color
|
||||
textColor cell.Color
|
||||
cursorColor cell.Color
|
||||
border linestyle.LineStyle
|
||||
|
||||
textWidthPerc *int
|
||||
label string
|
||||
labelCellOpts []cell.Option
|
||||
labelAlign align.Horizontal
|
||||
|
||||
placeHolder string
|
||||
hideTextWith rune
|
||||
|
||||
filter FilterFn
|
||||
onSubmit SubmitFn
|
||||
}
|
||||
|
||||
// validate validates the provided options.
|
||||
func (o *options) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// newOptions returns options with the default values set.
|
||||
func newOptions() *options {
|
||||
return &options{
|
||||
fillColor: cell.ColorNumber(DefaultFillColorNumber),
|
||||
cursorColor: cell.ColorNumber(DefaultCursorColorNumber),
|
||||
labelAlign: DefaultLabelAlign,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultFillColorNumber is the default color number for the FillColor option.
|
||||
const DefaultFillColorNumber = 33
|
||||
|
||||
// FillColor sets the fill color for the text input field.
|
||||
// Defaults to DefaultFillColorNumber.
|
||||
func FillColor(c cell.Color) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.fillColor = c
|
||||
})
|
||||
}
|
||||
|
||||
// TextColor sets the color of the text in the input field.
|
||||
// Defaults to the default terminal color.
|
||||
func TextColor(c cell.Color) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.textColor = c
|
||||
})
|
||||
}
|
||||
|
||||
// DefaultCursorColorNumber is the default color number for the CursorColor
|
||||
// option.
|
||||
const DefaultCursorColorNumber = 235
|
||||
|
||||
// CursorColor sets the color of the cursor.
|
||||
// Defaults to DefaultCursorColorNumber.
|
||||
func CursorColor(c cell.Color) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.cursorColor = c
|
||||
})
|
||||
}
|
||||
|
||||
// Border adds a border around the text input field.
|
||||
func Border(ls linestyle.LineStyle) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.border = ls
|
||||
})
|
||||
}
|
||||
|
||||
// TextWidthPerc sets the width for the text input field as a percentage of the
|
||||
// container width. Must be a value in the range 0 < perc < 100.
|
||||
// Defaults to the width adjusted automatically base on the label length.
|
||||
func TextWidthPerc(perc int) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.textWidthPerc = &perc
|
||||
})
|
||||
}
|
||||
|
||||
// Label adds a text label to the left of the input field.
|
||||
func Label(label string, cOpts ...cell.Option) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.label = label
|
||||
opts.labelCellOpts = cOpts
|
||||
})
|
||||
}
|
||||
|
||||
// DefaultLabelAlign is the default value for the LabelAlign option.
|
||||
const DefaultLabelAlign = align.HorizontalLeft
|
||||
|
||||
// LabelAlign sets the alignment of the label within its area.
|
||||
// The label is placed to the left of the input field. The width of this area
|
||||
// can be specified using the LabelWidthPerc option.
|
||||
// Defaults to DefaultLabelAlign.
|
||||
func LabelAlign(la align.Horizontal) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.labelAlign = la
|
||||
})
|
||||
}
|
||||
|
||||
// PlaceHolder sets text to be displayed in the input field when it is empty.
|
||||
// This text disappears when the text input field becomes focused.
|
||||
func PlaceHolder(text string) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.placeHolder = text
|
||||
})
|
||||
}
|
||||
|
||||
// HideTextWith sets the rune that should be displayed instead of displaying
|
||||
// the text. Useful for fields that accept sensitive information like
|
||||
// passwords.
|
||||
func HideTextWith(r rune) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.hideTextWith = r
|
||||
})
|
||||
}
|
||||
|
||||
// Filter sets a function that will be used to filter characters the user can
|
||||
// input.
|
||||
func Filter(fn FilterFn) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.filter = fn
|
||||
})
|
||||
}
|
||||
|
||||
// OnSubmit sets a function that will be called with the text typed by the user
|
||||
// when they submit the content by pressing the Enter key.
|
||||
func OnSubmit(fn SubmitFn) Option {
|
||||
return option(func(opts *options) {
|
||||
opts.onSubmit = fn
|
||||
})
|
||||
}
|
107
widgets/textinput/textinput.go
Normal file
107
widgets/textinput/textinput.go
Normal file
@ -0,0 +1,107 @@
|
||||
// Copyright 2019 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package textinput implements a widget that accepts text input.
|
||||
package textinput
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/mum4k/termdash/internal/canvas"
|
||||
"github.com/mum4k/termdash/terminal/terminalapi"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
)
|
||||
|
||||
// FilterFn if provided can be used to filter runes that are allowed in the
|
||||
// text input field. Any rune for which this function returns false will be
|
||||
// rejected.
|
||||
type FilterFn func(rune) bool
|
||||
|
||||
// SubmitFn if provided is called when the user submits the content of the text
|
||||
// input field, the argument text contains all the text in the field.
|
||||
// Submitting the input field clears its content.
|
||||
//
|
||||
// The callback function must be thread-safe as the keyboard event that
|
||||
// triggers the submission comes from a separate goroutine.
|
||||
type SubmitFn func(text string) error
|
||||
|
||||
// TextInput accepts text input from the user.
|
||||
//
|
||||
// Displays an input field where the user can edit text and an optional label.
|
||||
//
|
||||
// The text can be submitted by pressing enter or read at any time by calling
|
||||
// Read.
|
||||
//
|
||||
// Implements widgetapi.Widget. This object is thread-safe.
|
||||
type TextInput struct {
|
||||
// mu protects the widget.
|
||||
mu sync.Mutex
|
||||
|
||||
// opts are the provided options.
|
||||
opts *options
|
||||
}
|
||||
|
||||
// New returns a new TextInput.
|
||||
func New(opts ...Option) (*TextInput, error) {
|
||||
opt := newOptions()
|
||||
for _, o := range opts {
|
||||
o.set(opt)
|
||||
}
|
||||
if err := opt.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &TextInput{
|
||||
opts: opt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Vars to be replaced from tests.
|
||||
var (
|
||||
// Runes to use in cells that contain are reserved for the text input
|
||||
// field if no text is present.
|
||||
// Changed from tests to provide readable test failures.
|
||||
textFieldRune = ' '
|
||||
)
|
||||
|
||||
// Draw draws the TextInput widget onto the canvas.
|
||||
// Implements widgetapi.Widget.Draw.
|
||||
func (ti *TextInput) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
|
||||
ti.mu.Lock()
|
||||
defer ti.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Keyboard processes keyboard events.
|
||||
// Implements widgetapi.Widget.Keyboard.
|
||||
func (ti *TextInput) Keyboard(k *terminalapi.Keyboard) error {
|
||||
ti.mu.Lock()
|
||||
defer ti.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mouse processes mouse events.
|
||||
// Implements widgetapi.Widget.Mouse.
|
||||
func (ti *TextInput) Mouse(m *terminalapi.Mouse) error {
|
||||
ti.mu.Lock()
|
||||
defer ti.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options implements widgetapi.Widget.Options.
|
||||
func (ti *TextInput) Options() widgetapi.Options {
|
||||
return widgetapi.Options{}
|
||||
}
|
179
widgets/textinput/textinput_test.go
Normal file
179
widgets/textinput/textinput_test.go
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright 2019 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package textinput
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/mum4k/termdash/internal/canvas"
|
||||
"github.com/mum4k/termdash/internal/faketerm"
|
||||
"github.com/mum4k/termdash/terminal/terminalapi"
|
||||
"github.com/mum4k/termdash/widgetapi"
|
||||
)
|
||||
|
||||
// callbackTracker tracks whether callback was called.
|
||||
type callbackTracker struct {
|
||||
// wantErr when set to true, makes callback return an error.
|
||||
wantErr bool
|
||||
|
||||
// text is the text received OnSubmit.
|
||||
text string
|
||||
|
||||
// count is the number of times the callback was called.
|
||||
count int
|
||||
|
||||
// mu protects the tracker.
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// submit is the callback function called OnSubmit.
|
||||
func (ct *callbackTracker) submit(text string) error {
|
||||
ct.mu.Lock()
|
||||
defer ct.mu.Unlock()
|
||||
|
||||
if ct.wantErr {
|
||||
return errors.New("ct.wantErr set to true")
|
||||
}
|
||||
|
||||
ct.count++
|
||||
ct.text = text
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestTextInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
text string
|
||||
callback *callbackTracker
|
||||
opts []Option
|
||||
events []terminalapi.Event
|
||||
canvas image.Rectangle
|
||||
meta *widgetapi.Meta
|
||||
want func(size image.Point) *faketerm.Terminal
|
||||
wantCallback *callbackTracker
|
||||
wantNewErr bool
|
||||
wantDrawErr bool
|
||||
wantEventErr bool
|
||||
}{}
|
||||
|
||||
textFieldRune = 'x'
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
gotCallback := tc.callback
|
||||
if gotCallback != nil {
|
||||
tc.opts = append(tc.opts, OnSubmit(gotCallback.submit))
|
||||
}
|
||||
|
||||
ti, err := New(tc.opts...)
|
||||
if (err != nil) != tc.wantNewErr {
|
||||
t.Errorf("New => unexpected error: %v, wantNewErr: %v", err, tc.wantNewErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, ev := range tc.events {
|
||||
switch e := ev.(type) {
|
||||
case *terminalapi.Mouse:
|
||||
err := ti.Mouse(e)
|
||||
if (err != nil) != tc.wantEventErr {
|
||||
t.Errorf("Mouse => unexpected error: %v, wantEventErr: %v", err, tc.wantEventErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case *terminalapi.Keyboard:
|
||||
err := ti.Keyboard(e)
|
||||
if (err != nil) != tc.wantEventErr {
|
||||
t.Errorf("Keyboard => unexpected error: %v, wantEventErr: %v", err, tc.wantEventErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
t.Fatalf("unsupported event type: %T", ev)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := canvas.New(tc.canvas)
|
||||
if err != nil {
|
||||
t.Fatalf("canvas.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
{
|
||||
err = ti.Draw(c, tc.meta)
|
||||
if (err != nil) != tc.wantDrawErr {
|
||||
t.Errorf("Draw => unexpected error: %v, wantDrawErr: %v", err, tc.wantDrawErr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
got, err := faketerm.New(c.Size())
|
||||
if err != nil {
|
||||
t.Fatalf("faketerm.New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := c.Apply(got); err != nil {
|
||||
t.Fatalf("Apply => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var want *faketerm.Terminal
|
||||
if tc.want != nil {
|
||||
want = tc.want(c.Size())
|
||||
} else {
|
||||
want = faketerm.MustNew(c.Size())
|
||||
}
|
||||
|
||||
if diff := faketerm.Diff(want, got); diff != "" {
|
||||
t.Errorf("Draw => %v", diff)
|
||||
}
|
||||
|
||||
if diff := pretty.Compare(tc.wantCallback, gotCallback); diff != "" {
|
||||
t.Errorf("CallbackFn => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
text string
|
||||
opts []Option
|
||||
want widgetapi.Options
|
||||
}{}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
ti, err := New(tc.opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("New => unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := ti.Options()
|
||||
if diff := pretty.Compare(tc.want, got); diff != "" {
|
||||
t.Errorf("Options => unexpected diff (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
197
widgets/textinput/textinputdemo/textinputdemo.go
Normal file
197
widgets/textinput/textinputdemo/textinputdemo.go
Normal file
@ -0,0 +1,197 @@
|
||||
// Copyright 2019 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Binary textinputdemo shows the functionality of a text input field.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/mum4k/termdash"
|
||||
"github.com/mum4k/termdash/align"
|
||||
"github.com/mum4k/termdash/cell"
|
||||
"github.com/mum4k/termdash/container"
|
||||
"github.com/mum4k/termdash/keyboard"
|
||||
"github.com/mum4k/termdash/linestyle"
|
||||
"github.com/mum4k/termdash/terminal/termbox"
|
||||
"github.com/mum4k/termdash/terminal/terminalapi"
|
||||
"github.com/mum4k/termdash/widgets/button"
|
||||
"github.com/mum4k/termdash/widgets/segmentdisplay"
|
||||
"github.com/mum4k/termdash/widgets/textinput"
|
||||
)
|
||||
|
||||
// rotate returns a new slice with inputs rotated by step.
|
||||
// I.e. for a step of one:
|
||||
// inputs[0] -> inputs[len(inputs)-1]
|
||||
// inputs[1] -> inputs[0]
|
||||
// And so on.
|
||||
func rotate(inputs []rune, step int) []rune {
|
||||
return append(inputs[step:], inputs[:step]...)
|
||||
}
|
||||
|
||||
// textState creates a rotated state for the text we are displaying.
|
||||
func textState(text string, capacity, step int) []rune {
|
||||
if capacity == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var state []rune
|
||||
for i := 0; i < capacity; i++ {
|
||||
state = append(state, ' ')
|
||||
}
|
||||
state = append(state, []rune(text)...)
|
||||
step = step % len(state)
|
||||
return rotate(state, step)
|
||||
}
|
||||
|
||||
// rollText rolls a text across the segment display.
|
||||
// Exists when the context expires.
|
||||
func rollText(ctx context.Context, sd *segmentdisplay.SegmentDisplay, updateText <-chan string) {
|
||||
colors := []cell.Color{
|
||||
cell.ColorBlue,
|
||||
cell.ColorRed,
|
||||
cell.ColorYellow,
|
||||
cell.ColorBlue,
|
||||
cell.ColorGreen,
|
||||
cell.ColorRed,
|
||||
cell.ColorGreen,
|
||||
cell.ColorRed,
|
||||
}
|
||||
|
||||
text := "Termdash"
|
||||
step := 0
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
state := textState(text, sd.Capacity(), step)
|
||||
var chunks []*segmentdisplay.TextChunk
|
||||
for i := 0; i < sd.Capacity(); i++ {
|
||||
if i >= len(state) {
|
||||
break
|
||||
}
|
||||
|
||||
color := colors[i%len(colors)]
|
||||
chunks = append(chunks, segmentdisplay.NewChunk(
|
||||
string(state[i]),
|
||||
segmentdisplay.WriteCellOpts(cell.FgColor(color)),
|
||||
))
|
||||
}
|
||||
if len(chunks) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := sd.Write(chunks); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
step++
|
||||
|
||||
case t := <-updateText:
|
||||
text = t
|
||||
sd.Reset()
|
||||
step = 0
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
t, err := termbox.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer t.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
rollingSD, err := segmentdisplay.New(
|
||||
segmentdisplay.MaximizeSegmentHeight(),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
updateText := make(chan string)
|
||||
go rollText(ctx, rollingSD, updateText)
|
||||
|
||||
submitB, err := button.New("Submit", func() error {
|
||||
updateText <- "Hello World"
|
||||
return nil
|
||||
},
|
||||
button.GlobalKey(keyboard.KeyEnter),
|
||||
button.FillColor(cell.ColorNumber(220)),
|
||||
)
|
||||
clearB, err := button.New("Clear", func() error {
|
||||
updateText <- ""
|
||||
return nil
|
||||
},
|
||||
button.WidthFor("Submit"),
|
||||
button.FillColor(cell.ColorNumber(196)),
|
||||
)
|
||||
|
||||
input, err := textinput.New()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c, err := container.New(
|
||||
t,
|
||||
container.Border(linestyle.Light),
|
||||
container.BorderTitle("PRESS Q TO QUIT"),
|
||||
container.SplitHorizontal(
|
||||
container.Top(
|
||||
container.PlaceWidget(rollingSD),
|
||||
),
|
||||
container.Bottom(
|
||||
container.SplitHorizontal(
|
||||
container.Top(
|
||||
container.PlaceWidget(input),
|
||||
),
|
||||
container.Bottom(
|
||||
container.SplitVertical(
|
||||
container.Left(
|
||||
container.AlignVertical(align.VerticalTop),
|
||||
container.AlignHorizontal(align.HorizontalRight),
|
||||
container.PaddingRight(1),
|
||||
container.PlaceWidget(submitB),
|
||||
),
|
||||
container.Right(
|
||||
container.AlignVertical(align.VerticalTop),
|
||||
container.AlignHorizontal(align.HorizontalLeft),
|
||||
container.PaddingLeft(1),
|
||||
container.PlaceWidget(clearB),
|
||||
),
|
||||
),
|
||||
),
|
||||
container.SplitPercent(30),
|
||||
),
|
||||
),
|
||||
container.SplitPercent(40),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
quitter := func(k *terminalapi.Keyboard) {
|
||||
if k.Key == 'q' || k.Key == 'Q' {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(500*time.Millisecond)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user