diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go new file mode 100644 index 0000000..aff520b --- /dev/null +++ b/lib/ui/textinput.go @@ -0,0 +1,136 @@ +package ui + +import ( + "github.com/gdamore/tcell" + "github.com/mattn/go-runewidth" +) + +// TODO: Attach history and tab completion providers +// TODO: scrolling + +type TextInput struct { + Invalidatable + cells int + ctx *Context + focus bool + index int + prompt string + scroll int + text []rune +} + +// Creates a new TextInput. TextInputs will render a "textbox" in the entire +// context they're given, and process keypresses to build a string from user +// input. +func NewTextInput() *TextInput { + return &TextInput{ + cells: -1, + text: []rune{}, + } +} + +func (ti *TextInput) Prompt(prompt string) *TextInput { + ti.prompt = prompt + return ti +} + +func (ti *TextInput) String() string { + return string(ti.text) +} + +func (ti *TextInput) Invalidate() { + ti.DoInvalidate(ti) +} + +func (ti *TextInput) Draw(ctx *Context) { + ti.ctx = ctx // gross + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) + ctx.Printf(0, 0, tcell.StyleDefault, "%s%s", ti.prompt, string(ti.text)) + cells := runewidth.StringWidth(string(ti.text[:ti.index])) + if cells != ti.cells { + ctx.SetCursor(cells+1, 0) + } +} + +func (ti *TextInput) Focus(focus bool) { + ti.focus = focus + if focus && ti.ctx != nil { + cells := runewidth.StringWidth(string(ti.text[:ti.index])) + ti.ctx.SetCursor(cells+1, 0) + } +} + +func (ti *TextInput) insert(ch rune) { + left := ti.text[:ti.index] + right := ti.text[ti.index:] + ti.text = append(left, append([]rune{ch}, right...)...) + ti.index++ + ti.Invalidate() +} + +func (ti *TextInput) deleteWord() { + // TODO: Break on any of / " ' + if len(ti.text) == 0 { + return + } + i := ti.index - 1 + if ti.text[i] == ' ' { + i-- + } + for ; i >= 0; i-- { + if ti.text[i] == ' ' { + break + } + } + ti.text = append(ti.text[:i+1], ti.text[ti.index:]...) + ti.index = i + 1 + ti.Invalidate() +} + +func (ti *TextInput) deleteChar() { + if len(ti.text) > 0 && ti.index != len(ti.text) { + ti.text = append(ti.text[:ti.index], ti.text[ti.index+1:]...) + ti.Invalidate() + } +} + +func (ti *TextInput) backspace() { + if len(ti.text) > 0 && ti.index != 0 { + ti.text = append(ti.text[:ti.index-1], ti.text[ti.index:]...) + ti.index-- + ti.Invalidate() + } +} + +func (ti *TextInput) Event(event tcell.Event) bool { + switch event := event.(type) { + case *tcell.EventKey: + switch event.Key() { + case tcell.KeyBackspace, tcell.KeyBackspace2: + ti.backspace() + case tcell.KeyCtrlD, tcell.KeyDelete: + ti.deleteChar() + case tcell.KeyCtrlB, tcell.KeyLeft: + if ti.index > 0 { + ti.index-- + ti.Invalidate() + } + case tcell.KeyCtrlF, tcell.KeyRight: + if ti.index < len(ti.text) { + ti.index++ + ti.Invalidate() + } + case tcell.KeyCtrlA, tcell.KeyHome: + ti.index = 0 + ti.Invalidate() + case tcell.KeyCtrlE, tcell.KeyEnd: + ti.index = len(ti.text) + ti.Invalidate() + case tcell.KeyCtrlW: + ti.deleteWord() + case tcell.KeyRune: + ti.insert(event.Rune()) + } + } + return true +} diff --git a/widgets/dirlist.go b/widgets/dirlist.go index 374d142..faf73a1 100644 --- a/widgets/dirlist.go +++ b/widgets/dirlist.go @@ -13,13 +13,13 @@ import ( type DirectoryList struct { ui.Invalidatable - conf *config.AccountConfig - dirs []string - logger *log.Logger - selecting string - selected string - spinner *Spinner - worker *types.Worker + conf *config.AccountConfig + dirs []string + logger *log.Logger + selecting string + selected string + spinner *Spinner + worker *types.Worker } func NewDirectoryList(conf *config.AccountConfig, diff --git a/widgets/exline.go b/widgets/exline.go index 8b18736..c841802 100644 --- a/widgets/exline.go +++ b/widgets/exline.go @@ -2,36 +2,29 @@ package widgets import ( "github.com/gdamore/tcell" - "github.com/mattn/go-runewidth" "git.sr.ht/~sircmpwn/aerc2/lib/ui" ) -// TODO: history -// TODO: tab completion -// TODO: scrolling - type ExLine struct { ui.Invalidatable - command []rune - commit func(cmd string) - ctx *ui.Context - cancel func() - cells int - focus bool - index int - scroll int - - onInvalidate func(d ui.Drawable) + cancel func() + commit func(cmd string) + ctx *ui.Context + input *ui.TextInput } func NewExLine(commit func(cmd string), cancel func()) *ExLine { - return &ExLine{ - cancel: cancel, - cells: -1, - commit: commit, - command: []rune{}, + input := ui.NewTextInput().Prompt(":") + exline := &ExLine{ + cancel: cancel, + commit: commit, + input: input, } + input.OnInvalidate(func(d ui.Drawable) { + exline.Invalidate() + }) + return exline } func (ex *ExLine) Invalidate() { @@ -40,102 +33,29 @@ func (ex *ExLine) Invalidate() { func (ex *ExLine) Draw(ctx *ui.Context) { ex.ctx = ctx // gross - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault) - ctx.Printf(0, 0, tcell.StyleDefault, ":%s", string(ex.command)) - cells := runewidth.StringWidth(string(ex.command[:ex.index])) - if cells != ex.cells { - ctx.SetCursor(cells+1, 0) - } + ex.input.Draw(ctx) } func (ex *ExLine) Focus(focus bool) { - ex.focus = focus - if focus && ex.ctx != nil { - cells := runewidth.StringWidth(string(ex.command[:ex.index])) - ex.ctx.SetCursor(cells+1, 0) - } -} - -func (ex *ExLine) insert(ch rune) { - left := ex.command[:ex.index] - right := ex.command[ex.index:] - ex.command = append(left, append([]rune{ch}, right...)...) - ex.index++ - ex.Invalidate() -} - -func (ex *ExLine) deleteWord() { - // TODO: Break on any of / " ' - if len(ex.command) == 0 { - return - } - i := ex.index - 1 - if ex.command[i] == ' ' { - i-- - } - for ; i >= 0; i-- { - if ex.command[i] == ' ' { - break - } - } - ex.command = append(ex.command[:i+1], ex.command[ex.index:]...) - ex.index = i + 1 - ex.Invalidate() -} - -func (ex *ExLine) deleteChar() { - if len(ex.command) > 0 && ex.index != len(ex.command) { - ex.command = append(ex.command[:ex.index], ex.command[ex.index+1:]...) - ex.Invalidate() - } -} - -func (ex *ExLine) backspace() { - if len(ex.command) > 0 && ex.index != 0 { - ex.command = append(ex.command[:ex.index-1], ex.command[ex.index:]...) - ex.index-- - ex.Invalidate() - } + ex.input.Focus(focus) } func (ex *ExLine) Event(event tcell.Event) bool { switch event := event.(type) { case *tcell.EventKey: switch event.Key() { - case tcell.KeyBackspace, tcell.KeyBackspace2: - ex.backspace() - case tcell.KeyCtrlD, tcell.KeyDelete: - ex.deleteChar() - case tcell.KeyCtrlB, tcell.KeyLeft: - if ex.index > 0 { - ex.index-- - ex.Invalidate() - } - case tcell.KeyCtrlF, tcell.KeyRight: - if ex.index < len(ex.command) { - ex.index++ - ex.Invalidate() - } - case tcell.KeyCtrlA, tcell.KeyHome: - ex.index = 0 - ex.Invalidate() - case tcell.KeyCtrlE, tcell.KeyEnd: - ex.index = len(ex.command) - ex.Invalidate() - case tcell.KeyCtrlW: - ex.deleteWord() case tcell.KeyEnter: if ex.ctx != nil { ex.ctx.HideCursor() } - ex.commit(string(ex.command)) + ex.commit(ex.input.String()) case tcell.KeyEsc, tcell.KeyCtrlC: if ex.ctx != nil { ex.ctx.HideCursor() } ex.cancel() - case tcell.KeyRune: - ex.insert(event.Rune()) + default: + return ex.input.Event(event) } } return true diff --git a/widgets/spinner.go b/widgets/spinner.go index bb7dbe8..86158e6 100644 --- a/widgets/spinner.go +++ b/widgets/spinner.go @@ -24,8 +24,8 @@ var ( type Spinner struct { ui.Invalidatable - frame int64 // access via atomic - stop chan struct{} + frame int64 // access via atomic + stop chan struct{} } func NewSpinner() *Spinner {