From 1418e1b9dc41d8f69bccb8de0fe0f1fb6835ce11 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Mon, 26 Feb 2018 22:54:39 -0500 Subject: Split UI library and widgets --- cmd/aerc/main.go | 44 ++++++------ lib/ui/borders.go | 73 +++++++++++++++++++ lib/ui/context.go | 109 ++++++++++++++++++++++++++++ lib/ui/drawable.go | 10 +++ lib/ui/grid.go | 191 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/ui/interactive.go | 15 ++++ lib/ui/tab.go | 115 +++++++++++++++++++++++++++++ lib/ui/text.go | 71 ++++++++++++++++++ lib/ui/ui.go | 79 ++++++++++++++++++++ ui/account.go.old | 97 ------------------------- ui/borders.go | 73 ------------------- ui/context.go | 109 ---------------------------- ui/drawable.go | 10 --- ui/exline.go | 127 -------------------------------- ui/grid.go | 191 ------------------------------------------------- ui/interactive.go | 10 --- ui/tab.go | 115 ----------------------------- ui/text.go | 71 ------------------ ui/ui.go | 79 -------------------- widgets/account.go.old | 97 +++++++++++++++++++++++++ widgets/exline.go | 129 +++++++++++++++++++++++++++++++++ 21 files changed, 912 insertions(+), 903 deletions(-) create mode 100644 lib/ui/borders.go create mode 100644 lib/ui/context.go create mode 100644 lib/ui/drawable.go create mode 100644 lib/ui/grid.go create mode 100644 lib/ui/interactive.go create mode 100644 lib/ui/tab.go create mode 100644 lib/ui/text.go create mode 100644 lib/ui/ui.go delete mode 100644 ui/account.go.old delete mode 100644 ui/borders.go delete mode 100644 ui/context.go delete mode 100644 ui/drawable.go delete mode 100644 ui/exline.go delete mode 100644 ui/grid.go delete mode 100644 ui/interactive.go delete mode 100644 ui/tab.go delete mode 100644 ui/text.go delete mode 100644 ui/ui.go create mode 100644 widgets/account.go.old create mode 100644 widgets/exline.go diff --git a/cmd/aerc/main.go b/cmd/aerc/main.go index 3ba9d1a..2985e03 100644 --- a/cmd/aerc/main.go +++ b/cmd/aerc/main.go @@ -11,12 +11,13 @@ import ( tb "github.com/nsf/termbox-go" "git.sr.ht/~sircmpwn/aerc2/config" - "git.sr.ht/~sircmpwn/aerc2/ui" + libui "git.sr.ht/~sircmpwn/aerc2/lib/ui" + "git.sr.ht/~sircmpwn/aerc2/widgets" ) type fill rune -func (f fill) Draw(ctx *ui.Context) { +func (f fill) Draw(ctx *libui.Context) { for x := 0; x < ctx.Width(); x += 1 { for y := 0; y < ctx.Height(); y += 1 { ctx.SetCell(x, y, rune(f), tb.ColorDefault, tb.ColorDefault) @@ -24,7 +25,7 @@ func (f fill) Draw(ctx *ui.Context) { } } -func (f fill) OnInvalidate(callback func(d ui.Drawable)) { +func (f fill) OnInvalidate(callback func(d libui.Drawable)) { // no-op } @@ -48,38 +49,39 @@ func main() { panic(err) } - tabs := ui.NewTabs() + tabs := libui.NewTabs() tabs.Add(fill('★'), "白い星") tabs.Add(fill('☆'), "empty stars") - grid := ui.NewGrid().Rows([]ui.GridSpec{ - ui.GridSpec{ui.SIZE_EXACT, 1}, - ui.GridSpec{ui.SIZE_WEIGHT, 1}, - ui.GridSpec{ui.SIZE_EXACT, 1}, - }).Columns([]ui.GridSpec{ - ui.GridSpec{ui.SIZE_EXACT, 20}, - ui.GridSpec{ui.SIZE_WEIGHT, 1}, + grid := libui.NewGrid().Rows([]libui.GridSpec{ + libui.GridSpec{libui.SIZE_EXACT, 1}, + libui.GridSpec{libui.SIZE_WEIGHT, 1}, + libui.GridSpec{libui.SIZE_EXACT, 1}, + }).Columns([]libui.GridSpec{ + libui.GridSpec{libui.SIZE_EXACT, 20}, + libui.GridSpec{libui.SIZE_WEIGHT, 1}, }) // TODO: move sidebar into tab content, probably - grid.AddChild(ui.NewText("aerc"). - Strategy(ui.TEXT_CENTER). + grid.AddChild(libui.NewText("aerc"). + Strategy(libui.TEXT_CENTER). Color(tb.ColorBlack, tb.ColorWhite)) // sidebar placeholder: - grid.AddChild(ui.NewBordered( - fill('.'), ui.BORDER_RIGHT)).At(1, 0).Span(2, 1) + grid.AddChild(libui.NewBordered( + fill('.'), libui.BORDER_RIGHT)).At(1, 0).Span(2, 1) grid.AddChild(tabs.TabStrip).At(0, 1) grid.AddChild(tabs.TabContent).At(1, 1) - exline := ui.NewExLine() + exline := widgets.NewExLine() grid.AddChild(exline).At(2, 1) - _ui, err := ui.Initialize(conf, grid) + ui, err := libui.Initialize(conf, grid) if err != nil { panic(err) } - defer _ui.Close() + defer ui.Close() - _ui.AddInteractive(exline) + // TODO: this should be a stack + ui.AddInteractive(exline) go (func() { for { @@ -88,8 +90,8 @@ func main() { } })() - for !_ui.Exit { - if !_ui.Tick() { + for !ui.Exit { + if !ui.Tick() { // ~60 FPS time.Sleep(16 * time.Millisecond) } diff --git a/lib/ui/borders.go b/lib/ui/borders.go new file mode 100644 index 0000000..08071ad --- /dev/null +++ b/lib/ui/borders.go @@ -0,0 +1,73 @@ +package ui + +import ( + tb "github.com/nsf/termbox-go" +) + +const ( + BORDER_LEFT = 1 << iota + BORDER_TOP = 1 << iota + BORDER_RIGHT = 1 << iota + BORDER_BOTTOM = 1 << iota +) + +type Bordered struct { + borders uint + content Drawable + onInvalidate func(d Drawable) +} + +func NewBordered(content Drawable, borders uint) *Bordered { + b := &Bordered{ + borders: borders, + content: content, + } + content.OnInvalidate(b.contentInvalidated) + return b +} + +func (bordered *Bordered) contentInvalidated(d Drawable) { + bordered.Invalidate() +} + +func (bordered *Bordered) Invalidate() { + if bordered.onInvalidate != nil { + bordered.onInvalidate(bordered) + } +} + +func (bordered *Bordered) OnInvalidate(onInvalidate func(d Drawable)) { + bordered.onInvalidate = onInvalidate +} + +func (bordered *Bordered) Draw(ctx *Context) { + x := 0 + y := 0 + width := ctx.Width() + height := ctx.Height() + cell := tb.Cell{ + Ch: ' ', + Fg: tb.ColorBlack, + Bg: tb.ColorWhite, + } + if bordered.borders&BORDER_LEFT != 0 { + ctx.Fill(0, 0, 1, ctx.Height(), cell) + x += 1 + width -= 1 + } + if bordered.borders&BORDER_TOP != 0 { + ctx.Fill(0, 0, ctx.Width(), 1, cell) + y += 1 + height -= 1 + } + if bordered.borders&BORDER_RIGHT != 0 { + ctx.Fill(ctx.Width()-1, 0, 1, ctx.Height(), cell) + width -= 1 + } + if bordered.borders&BORDER_BOTTOM != 0 { + ctx.Fill(0, ctx.Height()-1, ctx.Width(), 1, cell) + height -= 1 + } + subctx := ctx.Subcontext(x, y, width, height) + bordered.content.Draw(subctx) +} diff --git a/lib/ui/context.go b/lib/ui/context.go new file mode 100644 index 0000000..ca3f452 --- /dev/null +++ b/lib/ui/context.go @@ -0,0 +1,109 @@ +package ui + +import ( + "fmt" + + "github.com/mattn/go-runewidth" + tb "github.com/nsf/termbox-go" +) + +// A context allows you to draw in a sub-region of the terminal +type Context struct { + x int + y int + width int + height int +} + +func (ctx *Context) X() int { + return ctx.x +} + +func (ctx *Context) Y() int { + return ctx.y +} + +func (ctx *Context) Width() int { + return ctx.width +} + +func (ctx *Context) Height() int { + return ctx.height +} + +func NewContext(width, height int) *Context { + return &Context{0, 0, width, height} +} + +func (ctx *Context) Subcontext(x, y, width, height int) *Context { + if x+width > ctx.width || y+height > ctx.height { + panic(fmt.Errorf("Attempted to create context larger than parent")) + } + return &Context{ + x: ctx.x + x, + y: ctx.y + y, + width: width, + height: height, + } +} + +func (ctx *Context) SetCell(x, y int, ch rune, fg, bg tb.Attribute) { + if x >= ctx.width || y >= ctx.height { + panic(fmt.Errorf("Attempted to draw outside of context")) + } + tb.SetCell(ctx.x+x, ctx.y+y, ch, fg, bg) +} + +func (ctx *Context) Printf(x, y int, ref tb.Cell, + format string, a ...interface{}) int { + + if x >= ctx.width || y >= ctx.height { + panic(fmt.Errorf("Attempted to draw outside of context")) + } + + str := fmt.Sprintf(format, a...) + + x += ctx.x + y += ctx.y + old_x := x + + newline := func() bool { + x = old_x + y++ + return y < ctx.height + } + for _, ch := range str { + if str == " こんにちは " { + fmt.Printf("%c\n", ch) + } + switch ch { + case '\n': + if !newline() { + return runewidth.StringWidth(str) + } + case '\r': + x = old_x + default: + tb.SetCell(x, y, ch, ref.Fg, ref.Bg) + x += runewidth.RuneWidth(ch) + if x == old_x+ctx.width { + if !newline() { + return runewidth.StringWidth(str) + } + } + } + } + + return runewidth.StringWidth(str) +} + +func (ctx *Context) Fill(x, y, width, height int, ref tb.Cell) { + _x := x + _y := y + for ; y < _y+height && y < ctx.height; y++ { + for ; x < _x+width && x < ctx.width; x++ { + ctx.SetCell(x, y, ref.Ch, ref.Fg, ref.Bg) + } + x = _x + } +} diff --git a/lib/ui/drawable.go b/lib/ui/drawable.go new file mode 100644 index 0000000..ef09451 --- /dev/null +++ b/lib/ui/drawable.go @@ -0,0 +1,10 @@ +package ui + +type Drawable interface { + // Called when this renderable should draw itself + Draw(ctx *Context) + // Specifies a function to call when this cell needs to be redrawn + OnInvalidate(callback func(d Drawable)) + // Invalidates the drawable + Invalidate() +} diff --git a/lib/ui/grid.go b/lib/ui/grid.go new file mode 100644 index 0000000..ede7d0c --- /dev/null +++ b/lib/ui/grid.go @@ -0,0 +1,191 @@ +package ui + +import ( + "fmt" + "math" +) + +type Grid struct { + rows []GridSpec + rowLayout []gridLayout + columns []GridSpec + columnLayout []gridLayout + Cells []*GridCell + onInvalidate func(d Drawable) + invalid bool +} + +const ( + SIZE_EXACT = iota + SIZE_WEIGHT = iota +) + +// Specifies the layout of a single row or column +type GridSpec struct { + // One of SIZE_EXACT or SIZE_WEIGHT + Strategy int + // If Strategy = SIZE_EXACT, this is the number of cells this row/col shall + // occupy. If SIZE_WEIGHT, the space left after all exact rows/cols are + // measured is distributed amonst the remainder weighted by this value. + Size int +} + +// Used to cache layout of each row/column +type gridLayout struct { + Offset int + Size int +} + +type GridCell struct { + Row int + Column int + RowSpan int + ColSpan int + Content Drawable + invalid bool +} + +func NewGrid() *Grid { + return &Grid{invalid: true} +} + +func (cell *GridCell) At(row, col int) *GridCell { + cell.Row = row + cell.Column = col + return cell +} + +func (cell *GridCell) Span(rows, cols int) *GridCell { + cell.RowSpan = rows + cell.ColSpan = cols + return cell +} + +func (grid *Grid) Rows(spec []GridSpec) *Grid { + grid.rows = spec + return grid +} + +func (grid *Grid) Columns(spec []GridSpec) *Grid { + grid.columns = spec + return grid +} + +func (grid *Grid) Draw(ctx *Context) { + invalid := grid.invalid + if invalid { + grid.reflow(ctx) + } + for _, cell := range grid.Cells { + if !cell.invalid && !invalid { + continue + } + rows := grid.rowLayout[cell.Row : cell.Row+cell.RowSpan] + cols := grid.columnLayout[cell.Column : cell.Column+cell.ColSpan] + x := cols[0].Offset + y := rows[0].Offset + width := 0 + height := 0 + for _, col := range cols { + width += col.Size + } + for _, row := range rows { + height += row.Size + } + subctx := ctx.Subcontext(x, y, width, height) + cell.Content.Draw(subctx) + } +} + +func (grid *Grid) reflow(ctx *Context) { + grid.rowLayout = nil + grid.columnLayout = nil + flow := func(specs *[]GridSpec, layouts *[]gridLayout, extent int) { + exact := 0 + weight := 0 + nweights := 0 + for _, spec := range *specs { + if spec.Strategy == SIZE_EXACT { + exact += spec.Size + } else if spec.Strategy == SIZE_WEIGHT { + nweights += 1 + weight += spec.Size + } + } + offset := 0 + for _, spec := range *specs { + layout := gridLayout{Offset: offset} + if spec.Strategy == SIZE_EXACT { + layout.Size = spec.Size + } else if spec.Strategy == SIZE_WEIGHT { + size := float64(spec.Size) / float64(weight) + size *= float64(extent - exact) + layout.Size = int(math.Floor(size)) + } + offset += layout.Size + *layouts = append(*layouts, layout) + } + } + flow(&grid.rows, &grid.rowLayout, ctx.Height()) + flow(&grid.columns, &grid.columnLayout, ctx.Width()) + grid.invalid = false +} + +func (grid *Grid) invalidateLayout() { + grid.invalid = true + if grid.onInvalidate != nil { + grid.onInvalidate(grid) + } +} + +func (grid *Grid) Invalidate() { + grid.invalidateLayout() + for _, cell := range grid.Cells { + cell.Content.Invalidate() + } +} + +func (grid *Grid) OnInvalidate(onInvalidate func(d Drawable)) { + grid.onInvalidate = onInvalidate +} + +func (grid *Grid) AddChild(content Drawable) *GridCell { + cell := &GridCell{ + RowSpan: 1, + ColSpan: 1, + Content: content, + invalid: true, + } + grid.Cells = append(grid.Cells, cell) + cell.Content.OnInvalidate(grid.cellInvalidated) + cell.invalid = true + grid.invalidateLayout() + return cell +} + +func (grid *Grid) RemoveChild(cell *GridCell) { + for i, _cell := range grid.Cells { + if _cell == cell { + grid.Cells = append(grid.Cells[:i], grid.Cells[i+1:]...) + break + } + } + grid.invalidateLayout() +} + +func (grid *Grid) cellInvalidated(drawable Drawable) { + var cell *GridCell + for _, cell = range grid.Cells { + if cell.Content == drawable { + break + } + cell = nil + } + if cell == nil { + panic(fmt.Errorf("Attempted to invalidate unknown cell")) + } + cell.invalid = true + if grid.onInvalidate != nil { + grid.onInvalidate(grid) + } +} diff --git a/lib/ui/interactive.go b/lib/ui/interactive.go new file mode 100644 index 0000000..8bdf592 --- /dev/null +++ b/lib/ui/interactive.go @@ -0,0 +1,15 @@ +package ui + +import ( + tb "github.com/nsf/termbox-go" +) + +type Interactive interface { + // Returns true if the event was handled by this component + Event(event tb.Event) bool +} + +type Simulator interface { + // Queues up the given input events for simulation + Simulate(events []tb.Event) +} diff --git a/lib/ui/tab.go b/lib/ui/tab.go new file mode 100644 index 0000000..e6a8aa5 --- /dev/null +++ b/lib/ui/tab.go @@ -0,0 +1,115 @@ +package ui + +import ( + tb "github.com/nsf/termbox-go" +) + +type Tabs struct { + Tabs []*Tab + TabStrip *TabStrip + TabContent *TabContent + Selected int + + onInvalidateStrip func(d Drawable) + onInvalidateContent func(d Drawable) +} + +type Tab struct { + Content Drawable + Name string + invalid bool +} + +type TabStrip Tabs +type TabContent Tabs + +func NewTabs() *Tabs { + tabs := &Tabs{} + tabs.TabStrip = (*TabStrip)(tabs) + tabs.TabContent = (*TabContent)(tabs) + return tabs +} + +func (tabs *Tabs) Add(content Drawable, name string) { + tabs.Tabs = append(tabs.Tabs, &Tab{ + Content: content, + Name: name, + }) + tabs.TabStrip.Invalidate() + content.OnInvalidate(tabs.invalidateChild) +} + +func (tabs *Tabs) invalidateChild(d Drawable) { + for i, tab := range tabs.Tabs { + if tab.Content == d { + if i == tabs.Selected { + tabs.TabContent.Invalidate() + } + return + } + } +} + +func (tabs *Tabs) Remove(content Drawable) { + for i, tab := range tabs.Tabs { + if tab.Content == content { + tabs.Tabs = append(tabs.Tabs[:i], tabs.Tabs[i+1:]...) + break + } + } + tabs.TabStrip.Invalidate() +} + +func (tabs *Tabs) Select(index int) { + if tabs.Selected != index { + tabs.Selected = index + tabs.TabStrip.Invalidate() + tabs.TabContent.Invalidate() + } +} + +// TODO: Color repository +func (strip *TabStrip) Draw(ctx *Context) { + x := 0 + for i, tab := range strip.Tabs { + cell := tb.Cell{ + Fg: tb.ColorBlack, + Bg: tb.ColorWhite, + } + if strip.Selected == i { + cell.Fg = tb.ColorDefault + cell.Bg = tb.ColorDefault + } + x += ctx.Printf(x, 0, cell, " %s ", tab.Name) + } + cell := tb.Cell{ + Fg: tb.ColorBlack, + Bg: tb.ColorWhite, + } + ctx.Fill(x, 0, ctx.Width()-x, 1, cell) +} + +func (strip *TabStrip) Invalidate() { + if strip.onInvalidateStrip != nil { + strip.onInvalidateStrip(strip) + } +} + +func (strip *TabStrip) OnInvalidate(onInvalidate func(d Drawable)) { + strip.onInvalidateStrip = onInvalidate +} + +func (content *TabContent) Draw(ctx *Context) { + tab := content.Tabs[content.Selected] + tab.Content.Draw(ctx) +} + +func (content *TabContent) Invalidate() { + if content.onInvalidateContent != nil { + content.onInvalidateContent(content) + } +} + +func (content *TabContent) OnInvalidate(onInvalidate func(d Drawable)) { + content.onInvalidateContent = onInvalidate +} diff --git a/lib/ui/text.go b/lib/ui/text.go new file mode 100644 index 0000000..6164837 --- /dev/null +++ b/lib/ui/text.go @@ -0,0 +1,71 @@ +package ui + +import ( + "github.com/mattn/go-runewidth" + tb "github.com/nsf/termbox-go" +) + +const ( + TEXT_LEFT = iota + TEXT_CENTER = iota + TEXT_RIGHT = iota +) + +type Text struct { + text string + strategy uint + fg tb.Attribute + bg tb.Attribute + onInvalidate func(d Drawable) +} + +func NewText(text string) *Text { + return &Text{text: text} +} + +func (t *Text) Text(text string) *Text { + t.text = text + t.Invalidate() + return t +} + +func (t *Text) Strategy(strategy uint) *Text { + t.strategy = strategy + t.Invalidate() + return t +} + +func (t *Text) Color(fg tb.Attribute, bg tb.Attribute) *Text { + t.fg = fg + t.bg = bg + t.Invalidate() + return t +} + +func (t *Text) Draw(ctx *Context) { + size := runewidth.StringWidth(t.text) + cell := tb.Cell{ + Ch: ' ', + Fg: t.fg, + Bg: t.bg, + } + x := 0 + if t.strategy == TEXT_CENTER { + x = (ctx.Width() - size) / 2 + } + if t.strategy == TEXT_RIGHT { + x = ctx.Width() - size + } + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), cell) + ctx.Printf(x, 0, cell, "%s", t.text) +} + +func (t *Text) OnInvalidate(onInvalidate func(d Drawable)) { + t.onInvalidate = onInvalidate +} + +func (t *Text) Invalidate() { + if t.onInvalidate != nil { + t.onInvalidate(t) + } +} diff --git a/lib/ui/ui.go b/lib/ui/ui.go new file mode 100644 index 0000000..9ea037c --- /dev/null +++ b/lib/ui/ui.go @@ -0,0 +1,79 @@ +package ui + +import ( + tb "github.com/nsf/termbox-go" + + "git.sr.ht/~sircmpwn/aerc2/config" +) + +type UI struct { + Exit bool + Content Drawable + ctx *Context + + interactive []Interactive + + tbEvents chan tb.Event + invalidations chan interface{} +} + +func Initialize(conf *config.AercConfig, content Drawable) (*UI, error) { + if err := tb.Init(); err != nil { + return nil, err + } + width, height := tb.Size() + state := UI{ + Content: content, + ctx: NewContext(width, height), + + tbEvents: make(chan tb.Event, 10), + invalidations: make(chan interface{}), + } + tb.SetInputMode(tb.InputEsc | tb.InputMouse) + tb.SetOutputMode(tb.Output256) + go (func() { + for !state.Exit { + state.tbEvents <- tb.PollEvent() + } + })() + go (func() { state.invalidations <- nil })() + content.OnInvalidate(func(_ Drawable) { + go (func() { state.invalidations <- nil })() + }) + return &state, nil +} + +func (state *UI) Close() { + tb.Close() +} + +func (state *UI) Tick() bool { + select { + case event := <-state.tbEvents: + switch event.Type { + case tb.EventKey: + if event.Key == tb.KeyEsc { + state.Exit = true + } + case tb.EventResize: + tb.Clear(tb.ColorDefault, tb.ColorDefault) + state.ctx = NewContext(event.Width, event.Height) + state.Content.Invalidate() + } + if state.interactive != nil { + for _, i := range state.interactive { + i.Event(event) + } + } + case <-state.invalidations: + state.Content.Draw(state.ctx) + tb.Flush() + default: + return false + } + return true +} + +func (state *UI) AddInteractive(i Interactive) { + state.interactive = append(state.interactive, i) +} diff --git a/ui/account.go.old b/ui/account.go.old deleted file mode 100644 index 393a47a..0000000 --- a/ui/account.go.old +++ /dev/null @@ -1,97 +0,0 @@ -package ui - -import ( - "log" - - tb "github.com/nsf/termbox-go" - - "git.sr.ht/~sircmpwn/aerc2/config" - "git.sr.ht/~sircmpwn/aerc2/worker" - "git.sr.ht/~sircmpwn/aerc2/worker/types" -) - -type AccountTab struct { - Config *config.AccountConfig - Worker *types.Worker - Parent *UIState - logger *log.Logger - counter int -} - -func NewAccountTab(conf *config.AccountConfig, - logger *log.Logger) (*AccountTab, error) { - - work, err := worker.NewWorker(conf.Source, logger) - if err != nil { - return nil, err - } - go work.Backend.Run() - acc := &AccountTab{ - Config: conf, - Worker: work, - logger: logger, - } - acc.Worker.PostAction(&types.Configure{Config: conf}, nil) - acc.Worker.PostAction(&types.Connect{}, func(msg types.WorkerMessage) { - switch msg := msg.(type) { - case *types.Done: - acc.logger.Println("Connected.") - acc.Worker.PostAction(&types.ListDirectories{}, nil) - case *types.CertificateApprovalRequest: - // TODO: Ask the user - acc.logger.Println("Approving certificate") - acc.Worker.PostAction(&types.ApproveCertificate{ - Message: types.RespondTo(msg), - Approved: true, - }, nil) - default: - acc.logger.Println("Connection failed.") - } - }) - return acc, nil -} - -func (acc *AccountTab) Name() string { - return acc.Config.Name -} - -func (acc *AccountTab) SetParent(parent *UIState) { - acc.Parent = parent -} - -func (acc *AccountTab) Render(at Geometry) { - cell := tb.Cell{ - Ch: ' ', - Fg: tb.ColorDefault, - Bg: tb.ColorDefault, - } - TFill(at, cell) - TPrintf(&at, cell, "%s %d\n", acc.Name(), acc.counter) - acc.counter++ - if acc.counter%10000 == 0 { - acc.counter = 0 - } - acc.Parent.InvalidateFrom(acc) -} - -func (acc *AccountTab) GetChannel() chan types.WorkerMessage { - return acc.Worker.Messages -} - -func (acc *AccountTab) HandleMessage(msg types.WorkerMessage) { - msg = acc.Worker.ProcessMessage(msg) - switch msg := msg.(type) { - case *types.Done: - case *types.CertificateApprovalRequest: - case *types.Unsupported: - // no-op - case *types.Error: - acc.logger.Printf("Error: %v\n", msg.Error) - case *types.Directory: - acc.logger.Printf("Directory: %s\n", msg.Name) - default: - acc.Worker.PostAction(&types.Unsupported{ - Message: types.RespondTo(msg), - }, nil) - } -} diff --git a/ui/borders.go b/ui/borders.go deleted file mode 100644 index 08071ad..0000000 --- a/ui/borders.go +++ /dev/null @@ -1,73 +0,0 @@ -package ui - -import ( - tb "github.com/nsf/termbox-go" -) - -const ( - BORDER_LEFT = 1 << iota - BORDER_TOP = 1 << iota - BORDER_RIGHT = 1 << iota - BORDER_BOTTOM = 1 << iota -) - -type Bordered struct { - borders uint - content Drawable - onInvalidate func(d Drawable) -} - -func NewBordered(content Drawable, borders uint) *Bordered { - b := &Bordered{ - borders: borders, - content: content, - } - content.OnInvalidate(b.contentInvalidated) - return b -} - -func (bordered *Bordered) contentInvalidated(d Drawable) { - bordered.Invalidate() -} - -func (bordered *Bordered) Invalidate() { - if bordered.onInvalidate != nil { - bordered.onInvalidate(bordered) - } -} - -func (bordered *Bordered) OnInvalidate(onInvalidate func(d Drawable)) { - bordered.onInvalidate = onInvalidate -} - -func (bordered *Bordered) Draw(ctx *Context) { - x := 0 - y := 0 - width := ctx.Width() - height := ctx.Height() - cell := tb.Cell{ - Ch: ' ', - Fg: tb.ColorBlack, - Bg: tb.ColorWhite, - } - if bordered.borders&BORDER_LEFT != 0 { - ctx.Fill(0, 0, 1, ctx.Height(), cell) - x += 1 - width -= 1 - } - if bordered.borders&BORDER_TOP != 0 { - ctx.Fill(0, 0, ctx.Width(), 1, cell) - y += 1 - height -= 1 - } - if bordered.borders&BORDER_RIGHT != 0 { - ctx.Fill(ctx.Width()-1, 0, 1, ctx.Height(), cell) - width -= 1 - } - if bordered.borders&BORDER_BOTTOM != 0 { - ctx.Fill(0, ctx.Height()-1, ctx.Width(), 1, cell) - height -= 1 - } - subctx := ctx.Subcontext(x, y, width, height) - bordered.content.Draw(subctx) -} diff --git a/ui/context.go b/ui/context.go deleted file mode 100644 index ca3f452..0000000 --- a/ui/context.go +++ /dev/null @@ -1,109 +0,0 @@ -package ui - -import ( - "fmt" - - "github.com/mattn/go-runewidth" - tb "github.com/nsf/termbox-go" -) - -// A context allows you to draw in a sub-region of the terminal -type Context struct { - x int - y int - width int - height int -} - -func (ctx *Context) X() int { - return ctx.x -} - -func (ctx *Context) Y() int { - return ctx.y -} - -func (ctx *Context) Width() int { - return ctx.width -} - -func (ctx *Context) Height() int { - return ctx.height -} - -func NewContext(width, height int) *Context { - return &Context{0, 0, width, height} -} - -func (ctx *Context) Subcontext(x, y, width, height int) *Context { - if x+width > ctx.width || y+height > ctx.height { - panic(fmt.Errorf("Attempted to create context larger than parent")) - } - return &Context{ - x: ctx.x + x, - y: ctx.y + y, - width: width, - height: height, - } -} - -func (ctx *Context) SetCell(x, y int, ch rune, fg, bg tb.Attribute) { - if x >= ctx.width || y >= ctx.height { - panic(fmt.Errorf("Attempted to draw outside of context")) - } - tb.SetCell(ctx.x+x, ctx.y+y, ch, fg, bg) -} - -func (ctx *Context) Printf(x, y int, ref tb.Cell, - format string, a ...interface{}) int { - - if x >= ctx.width || y >= ctx.height { - panic(fmt.Errorf("Attempted to draw outside of context")) - } - - str := fmt.Sprintf(format, a...) - - x += ctx.x - y += ctx.y - old_x := x - - newline := func() bool { - x = old_x - y++ - return y < ctx.height - } - for _, ch := range str { - if str == " こんにちは " { - fmt.Printf("%c\n", ch) - } - switch ch { - case '\n': - if !newline() { - return runewidth.StringWidth(str) - } - case '\r': - x = old_x - default: - tb.SetCell(x, y, ch, ref.Fg, ref.Bg) - x += runewidth.RuneWidth(ch) - if x == old_x+ctx.width { - if !newline() { - return runewidth.StringWidth(str) - } - } - } - } - - return runewidth.StringWidth(str) -} - -func (ctx *Context) Fill(x, y, width, height int, ref tb.Cell) { - _x := x - _y := y - for ; y < _y+height && y < ctx.height; y++ { - for ; x < _x+width && x < ctx.width; x++ { - ctx.SetCell(x, y, ref.Ch, ref.Fg, ref.Bg) - } - x = _x - } -} diff --git a/ui/drawable.go b/ui/drawable.go deleted file mode 100644 index ef09451..0000000 --- a/ui/drawable.go +++ /dev/null @@ -1,10 +0,0 @@ -package ui - -type Drawable interface { - // Called when this renderable should draw itself - Draw(ctx *Context) - // Specifies a function to call when this cell needs to be redrawn - OnInvalidate(callback func(d Drawable)) - // Invalidates the drawable - Invalidate() -} diff --git a/ui/exline.go b/ui/exline.go deleted file mode 100644 index a377cd7..0000000 --- a/ui/exline.go +++ /dev/null @@ -1,127 +0,0 @@ -package ui - -import ( - tb "github.com/nsf/termbox-go" -) - -// TODO: history -// TODO: tab completion -// TODO: commit -// TODO: cancel (via esc/ctrl+c) -// TODO: scrolling - -type ExLine struct { - command *string - commit func(cmd *string) - index int - scroll int - - onInvalidate func(d Drawable) -} - -func NewExLine() *ExLine { - cmd := "" - return &ExLine{command: &cmd} -} - -func (ex *ExLine) OnInvalidate(onInvalidate func(d Drawable)) { - ex.onInvalidate = onInvalidate -} - -func (ex *ExLine) Invalidate() { - if ex.onInvalidate != nil { - ex.onInvalidate(ex) - } -} - -func (ex *ExLine) Draw(ctx *Context) { - cell := tb.Cell{ - Fg: tb.ColorDefault, - Bg: tb.ColorDefault, - Ch: ' ', - } - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), cell) - ctx.Printf(0, 0, cell, ":%s", *ex.command) - tb.SetCursor(ctx.X()+ex.index-ex.scroll+1, ctx.Y()) -} - -func (ex *ExLine) insert(ch rune) { - newCmd := (*ex.command)[:ex.index] + string(ch) + (*ex.command)[ex.index:] - ex.command = &newCmd - 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 - } - } - newCmd := (*ex.command)[:i+1] + (*ex.command)[ex.index:] - ex.command = &newCmd - ex.index = i + 1 - ex.Invalidate() -} - -func (ex *ExLine) deleteChar() { - if len(*ex.command) > 0 && ex.index != len(*ex.command) { - newCmd := (*ex.command)[:ex.index] + (*ex.command)[ex.index+1:] - ex.command = &newCmd - ex.Invalidate() - } -} - -func (ex *ExLine) backspace() { - if len(*ex.command) > 0 && ex.index != 0 { - newCmd := (*ex.command)[:ex.index-1] + (*ex.command)[ex.index:] - ex.command = &newCmd - ex.index-- - ex.Invalidate() - } -} - -func (ex *ExLine) Event(event tb.Event) bool { - switch event.Type { - case tb.EventKey: - switch event.Key { - case tb.KeySpace: - ex.insert(' ') - case tb.KeyBackspace, tb.KeyBackspace2: - ex.backspace() - case tb.KeyCtrlD, tb.KeyDelete: - ex.deleteChar() - case tb.KeyCtrlB, tb.KeyArrowLeft: - if ex.index > 0 { - ex.index-- - ex.Invalidate() - } - case tb.KeyCtrlF, tb.KeyArrowRight: - if ex.index < len(*ex.command) { - ex.index++ - ex.Invalidate() - } - case tb.KeyCtrlA, tb.KeyHome: - ex.index = 0 - ex.Invalidate() - case tb.KeyCtrlE, tb.KeyEnd: - ex.index = len(*ex.command) - ex.Invalidate() - case tb.KeyCtrlW: - ex.deleteWord() - default: - if event.Ch != 0 { - ex.insert(event.Ch) - } - } - } - return true -} diff --git a/ui/grid.go b/ui/grid.go deleted file mode 100644 index ede7d0c..0000000 --- a/ui/grid.go +++ /dev/null @@ -1,191 +0,0 @@ -package ui - -import ( - "fmt" - "math" -) - -type Grid struct { - rows []GridSpec - rowLayout []gridLayout - columns []GridSpec - columnLayout []gridLayout - Cells []*GridCell - onInvalidate func(d Drawable) - invalid bool -} - -const ( - SIZE_EXACT = iota - SIZE_WEIGHT = iota -) - -// Specifies the layout of a single row or column -type GridSpec struct { - // One of SIZE_EXACT or SIZE_WEIGHT - Strategy int - // If Strategy = SIZE_EXACT, this is the number of cells this row/col shall - // occupy. If SIZE_WEIGHT, the space left after all exact rows/cols are - // measured is distributed amonst the remainder weighted by this value. - Size int -} - -// Used to cache layout of each row/column -type gridLayout struct { - Offset int - Size int -} - -type GridCell struct { - Row int - Column int - RowSpan int - ColSpan int - Content Drawable - invalid bool -} - -func NewGrid() *Grid { - return &Grid{invalid: true} -} - -func (cell *GridCell) At(row, col int) *GridCell { - cell.Row = row - cell.Column = col - return cell -} - -func (cell *GridCell) Span(rows, cols int) *GridCell { - cell.RowSpan = rows - cell.ColSpan = cols - return cell -} - -func (grid *Grid) Rows(spec []GridSpec) *Grid { - grid.rows = spec - return grid -} - -func (grid *Grid) Columns(spec []GridSpec) *Grid { - grid.columns = spec - return grid -} - -func (grid *Grid) Draw(ctx *Context) { - invalid := grid.invalid - if invalid { - grid.reflow(ctx) - } - for _, cell := range grid.Cells { - if !cell.invalid && !invalid { - continue - } - rows := grid.rowLayout[cell.Row : cell.Row+cell.RowSpan] - cols := grid.columnLayout[cell.Column : cell.Column+cell.ColSpan] - x := cols[0].Offset - y := rows[0].Offset - width := 0 - height := 0 - for _, col := range cols { - width += col.Size - } - for _, row := range rows { - height += row.Size - } - subctx := ctx.Subcontext(x, y, width, height) - cell.Content.Draw(subctx) - } -} - -func (grid *Grid) reflow(ctx *Context) { - grid.rowLayout = nil - grid.columnLayout = nil - flow := func(specs *[]GridSpec, layouts *[]gridLayout, extent int) { - exact := 0 - weight := 0 - nweights := 0 - for _, spec := range *specs { - if spec.Strategy == SIZE_EXACT { - exact += spec.Size - } else if spec.Strategy == SIZE_WEIGHT { - nweights += 1 - weight += spec.Size - } - } - offset := 0 - for _, spec := range *specs { - layout := gridLayout{Offset: offset} - if spec.Strategy == SIZE_EXACT { - layout.Size = spec.Size - } else if spec.Strategy == SIZE_WEIGHT { - size := float64(spec.Size) / float64(weight) - size *= float64(extent - exact) - layout.Size = int(math.Floor(size)) - } - offset += layout.Size - *layouts = append(*layouts, layout) - } - } - flow(&grid.rows, &grid.rowLayout, ctx.Height()) - flow(&grid.columns, &grid.columnLayout, ctx.Width()) - grid.invalid = false -} - -func (grid *Grid) invalidateLayout() { - grid.invalid = true - if grid.onInvalidate != nil { - grid.onInvalidate(grid) - } -} - -func (grid *Grid) Invalidate() { - grid.invalidateLayout() - for _, cell := range grid.Cells { - cell.Content.Invalidate() - } -} - -func (grid *Grid) OnInvalidate(onInvalidate func(d Drawable)) { - grid.onInvalidate = onInvalidate -} - -func (grid *Grid) AddChild(content Drawable) *GridCell { - cell := &GridCell{ - RowSpan: 1, - ColSpan: 1, - Content: content, - invalid: true, - } - grid.Cells = append(grid.Cells, cell) - cell.Content.OnInvalidate(grid.cellInvalidated) - cell.invalid = true - grid.invalidateLayout() - return cell -} - -func (grid *Grid) RemoveChild(cell *GridCell) { - for i, _cell := range grid.Cells { - if _cell == cell { - grid.Cells = append(grid.Cells[:i], grid.Cells[i+1:]...) - break - } - } - grid.invalidateLayout() -} - -func (grid *Grid) cellInvalidated(drawable Drawable) { - var cell *GridCell - for _, cell = range grid.Cells { - if cell.Content == drawable { - break - } - cell = nil - } - if cell == nil { - panic(fmt.Errorf("Attempted to invalidate unknown cell")) - } - cell.invalid = true - if grid.onInvalidate != nil { - grid.onInvalidate(grid) - } -} diff --git a/ui/interactive.go b/ui/interactive.go deleted file mode 100644 index 5dd5fef..0000000 --- a/ui/interactive.go +++ /dev/null @@ -1,10 +0,0 @@ -package ui - -import ( - tb "github.com/nsf/termbox-go" -) - -type Interactive interface { - // Returns true if the event was handled by this component - Event(event tb.Event) bool -} diff --git a/ui/tab.go b/ui/tab.go deleted file mode 100644 index e6a8aa5..0000000 --- a/ui/tab.go +++ /dev/null @@ -1,115 +0,0 @@ -package ui - -import ( - tb "github.com/nsf/termbox-go" -) - -type Tabs struct { - Tabs []*Tab - TabStrip *TabStrip - TabContent *TabContent - Selected int - - onInvalidateStrip func(d Drawable) - onInvalidateContent func(d Drawable) -} - -type Tab struct { - Content Drawable - Name string - invalid bool -} - -type TabStrip Tabs -type TabContent Tabs - -func NewTabs() *Tabs { - tabs := &Tabs{} - tabs.TabStrip = (*TabStrip)(tabs) - tabs.TabContent = (*TabContent)(tabs) - return tabs -} - -func (tabs *Tabs) Add(content Drawable, name string) { - tabs.Tabs = append(tabs.Tabs, &Tab{ - Content: content, - Name: name, - }) - tabs.TabStrip.Invalidate() - content.OnInvalidate(tabs.invalidateChild) -} - -func (tabs *Tabs) invalidateChild(d Drawable) { - for i, tab := range tabs.Tabs { - if tab.Content == d { - if i == tabs.Selected { - tabs.TabContent.Invalidate() - } - return - } - } -} - -func (tabs *Tabs) Remove(content Drawable) { - for i, tab := range tabs.Tabs { - if tab.Content == content { - tabs.Tabs = append(tabs.Tabs[:i], tabs.Tabs[i+1:]...) - break - } - } - tabs.TabStrip.Invalidate() -} - -func (tabs *Tabs) Select(index int) { - if tabs.Selected != index { - tabs.Selected = index - tabs.TabStrip.Invalidate() - tabs.TabContent.Invalidate() - } -} - -// TODO: Color repository -func (strip *TabStrip) Draw(ctx *Context) { - x := 0 - for i, tab := range strip.Tabs { - cell := tb.Cell{ - Fg: tb.ColorBlack, - Bg: tb.ColorWhite, - } - if strip.Selected == i { - cell.Fg = tb.ColorDefault - cell.Bg = tb.ColorDefault - } - x += ctx.Printf(x, 0, cell, " %s ", tab.Name) - } - cell := tb.Cell{ - Fg: tb.ColorBlack, - Bg: tb.ColorWhite, - } - ctx.Fill(x, 0, ctx.Width()-x, 1, cell) -} - -func (strip *TabStrip) Invalidate() { - if strip.onInvalidateStrip != nil { - strip.onInvalidateStrip(strip) - } -} - -func (strip *TabStrip) OnInvalidate(onInvalidate func(d Drawable)) { - strip.onInvalidateStrip = onInvalidate -} - -func (content *TabContent) Draw(ctx *Context) { - tab := content.Tabs[content.Selected] - tab.Content.Draw(ctx) -} - -func (content *TabContent) Invalidate() { - if content.onInvalidateContent != nil { - content.onInvalidateContent(content) - } -} - -func (content *TabContent) OnInvalidate(onInvalidate func(d Drawable)) { - content.onInvalidateContent = onInvalidate -} diff --git a/ui/text.go b/ui/text.go deleted file mode 100644 index 6164837..0000000 --- a/ui/text.go +++ /dev/null @@ -1,71 +0,0 @@ -package ui - -import ( - "github.com/mattn/go-runewidth" - tb "github.com/nsf/termbox-go" -) - -const ( - TEXT_LEFT = iota - TEXT_CENTER = iota - TEXT_RIGHT = iota -) - -type Text struct { - text string - strategy uint - fg tb.Attribute - bg tb.Attribute - onInvalidate func(d Drawable) -} - -func NewText(text string) *Text { - return &Text{text: text} -} - -func (t *Text) Text(text string) *Text { - t.text = text - t.Invalidate() - return t -} - -func (t *Text) Strategy(strategy uint) *Text { - t.strategy = strategy - t.Invalidate() - return t -} - -func (t *Text) Color(fg tb.Attribute, bg tb.Attribute) *Text { - t.fg = fg - t.bg = bg - t.Invalidate() - return t -} - -func (t *Text) Draw(ctx *Context) { - size := runewidth.StringWidth(t.text) - cell := tb.Cell{ - Ch: ' ', - Fg: t.fg, - Bg: t.bg, - } - x := 0 - if t.strategy == TEXT_CENTER { - x = (ctx.Width() - size) / 2 - } - if t.strategy == TEXT_RIGHT { - x = ctx.Width() - size - } - ctx.Fill(0, 0, ctx.Width(), ctx.Height(), cell) - ctx.Printf(x, 0, cell, "%s", t.text) -} - -func (t *Text) OnInvalidate(onInvalidate func(d Drawable)) { - t.onInvalidate = onInvalidate -} - -func (t *Text) Invalidate() { - if t.onInvalidate != nil { - t.onInvalidate(t) - } -} diff --git a/ui/ui.go b/ui/ui.go deleted file mode 100644 index 9ea037c..0000000 --- a/ui/ui.go +++ /dev/null @@ -1,79 +0,0 @@ -package ui - -import ( - tb "github.com/nsf/termbox-go" - - "git.sr.ht/~sircmpwn/aerc2/config" -) - -type UI struct { - Exit bool - Content Drawable - ctx *Context - - interactive []Interactive - - tbEvents chan tb.Event - invalidations chan interface{} -} - -func Initialize(conf *config.AercConfig, content Drawable) (*UI, error) { - if err := tb.Init(); err != nil { - return nil, err - } - width, height := tb.Size() - state := UI{ - Content: content, - ctx: NewContext(width, height), - - tbEvents: make(chan tb.Event, 10), - invalidations: make(chan interface{}), - } - tb.SetInputMode(tb.InputEsc | tb.InputMouse) - tb.SetOutputMode(tb.Output256) - go (func() { - for !state.Exit { - state.tbEvents <- tb.PollEvent() - } - })() - go (func() { state.invalidations <- nil })() - content.OnInvalidate(func(_ Drawable) { - go (func() { state.invalidations <- nil })() - }) - return &state, nil -} - -func (state *UI) Close() { - tb.Close() -} - -func (state *UI) Tick() bool { - select { - case event := <-state.tbEvents: - switch event.Type { - case tb.EventKey: - if event.Key == tb.KeyEsc { - state.Exit = true - } - case tb.EventResize: - tb.Clear(tb.ColorDefault, tb.ColorDefault) - state.ctx = NewContext(event.Width, event.Height) - state.Content.Invalidate() - } - if state.interactive != nil { - for _, i := range state.interactive { - i.Event(event) - } - } - case <-state.invalidations: - state.Content.Draw(state.ctx) - tb.Flush() - default: - return false - } - return true -} - -func (state *UI) AddInteractive(i Interactive) { - state.interactive = append(state.interactive, i) -} diff --git a/widgets/account.go.old b/widgets/account.go.old new file mode 100644 index 0000000..393a47a --- /dev/null +++ b/widgets/account.go.old @@ -0,0 +1,97 @@ +package ui + +import ( + "log" + + tb "github.com/nsf/termbox-go" + + "git.sr.ht/~sircmpwn/aerc2/config" + "git.sr.ht/~sircmpwn/aerc2/worker" + "git.sr.ht/~sircmpwn/aerc2/worker/types" +) + +type AccountTab struct { + Config *config.AccountConfig + Worker *types.Worker + Parent *UIState + logger *log.Logger + counter int +} + +func NewAccountTab(conf *config.AccountConfig, + logger *log.Logger) (*AccountTab, error) { + + work, err := worker.NewWorker(conf.Source, logger) + if err != nil { + return nil, err + } + go work.Backend.Run() + acc := &AccountTab{ + Config: conf, + Worker: work, + logger: logger, + } + acc.Worker.PostAction(&types.Configure{Config: conf}, nil) + acc.Worker.PostAction(&types.Connect{}, func(msg types.WorkerMessage) { + switch msg := msg.(type) { + case *types.Done: + acc.logger.Println("Connected.") + acc.Worker.PostAction(&types.ListDirectories{}, nil) + case *types.CertificateApprovalRequest: + // TODO: Ask the user + acc.logger.Println("Approving certificate") + acc.Worker.PostAction(&types.ApproveCertificate{ + Message: types.RespondTo(msg), + Approved: true, + }, nil) + default: + acc.logger.Println("Connection failed.") + } + }) + return acc, nil +} + +func (acc *AccountTab) Name() string { + return acc.Config.Name +} + +func (acc *AccountTab) SetParent(parent *UIState) { + acc.Parent = parent +} + +func (acc *AccountTab) Render(at Geometry) { + cell := tb.Cell{ + Ch: ' ', + Fg: tb.ColorDefault, + Bg: tb.ColorDefault, + } + TFill(at, cell) + TPrintf(&at, cell, "%s %d\n", acc.Name(), acc.counter) + acc.counter++ + if acc.counter%10000 == 0 { + acc.counter = 0 + } + acc.Parent.InvalidateFrom(acc) +} + +func (acc *AccountTab) GetChannel() chan types.WorkerMessage { + return acc.Worker.Messages +} + +func (acc *AccountTab) HandleMessage(msg types.WorkerMessage) { + msg = acc.Worker.ProcessMessage(msg) + switch msg := msg.(type) { + case *types.Done: + case *types.CertificateApprovalRequest: + case *types.Unsupported: + // no-op + case *types.Error: + acc.logger.Printf("Error: %v\n", msg.Error) + case *types.Directory: + acc.logger.Printf("Directory: %s\n", msg.Name) + default: + acc.Worker.PostAction(&types.Unsupported{ + Message: types.RespondTo(msg), + }, nil) + } +} diff --git a/widgets/exline.go b/widgets/exline.go new file mode 100644 index 0000000..092d6eb --- /dev/null +++ b/widgets/exline.go @@ -0,0 +1,129 @@ +package widgets + +import ( + tb "github.com/nsf/termbox-go" + + "git.sr.ht/~sircmpwn/aerc2/lib/ui" +) + +// TODO: history +// TODO: tab completion +// TODO: commit +// TODO: cancel (via esc/ctrl+c) +// TODO: scrolling + +type ExLine struct { + command *string + commit func(cmd *string) + index int + scroll int + + onInvalidate func(d ui.Drawable) +} + +func NewExLine() *ExLine { + cmd := "" + return &ExLine{command: &cmd} +} + +func (ex *ExLine) OnInvalidate(onInvalidate func(d ui.Drawable)) { + ex.onInvalidate = onInvalidate +} + +func (ex *ExLine) Invalidate() { + if ex.onInvalidate != nil { + ex.onInvalidate(ex) + } +} + +func (ex *ExLine) Draw(ctx *ui.Context) { + cell := tb.Cell{ + Fg: tb.ColorDefault, + Bg: tb.ColorDefault, + Ch: ' ', + } + ctx.Fill(0, 0, ctx.Width(), ctx.Height(), cell) + ctx.Printf(0, 0, cell, ":%s", *ex.command) + tb.SetCursor(ctx.X()+ex.index-ex.scroll+1, ctx.Y()) +} + +func (ex *ExLine) insert(ch rune) { + newCmd := (*ex.command)[:ex.index] + string(ch) + (*ex.command)[ex.index:] + ex.command = &newCmd + 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 + } + } + newCmd := (*ex.command)[:i+1] + (*ex.command)[ex.index:] + ex.command = &newCmd + ex.index = i + 1 + ex.Invalidate() +} + +func (ex *ExLine) deleteChar() { + if len(*ex.command) > 0 && ex.index != len(*ex.command) { + newCmd := (*ex.command)[:ex.index] + (*ex.command)[ex.index+1:] + ex.command = &newCmd + ex.Invalidate() + } +} + +func (ex *ExLine) backspace() { + if len(*ex.command) > 0 && ex.index != 0 { + newCmd := (*ex.command)[:ex.index-1] + (*ex.command)[ex.index:] + ex.command = &newCmd + ex.index-- + ex.Invalidate() + } +} + +func (ex *ExLine) Event(event tb.Event) bool { + switch event.Type { + case tb.EventKey: + switch event.Key { + case tb.KeySpace: + ex.insert(' ') + case tb.KeyBackspace, tb.KeyBackspace2: + ex.backspace() + case tb.KeyCtrlD, tb.KeyDelete: + ex.deleteChar() + case tb.KeyCtrlB, tb.KeyArrowLeft: + if ex.index > 0 { + ex.index-- + ex.Invalidate() + } + case tb.KeyCtrlF, tb.KeyArrowRight: + if ex.index < len(*ex.command) { + ex.index++ + ex.Invalidate() + } + case tb.KeyCtrlA, tb.KeyHome: + ex.index = 0 + ex.Invalidate() + case tb.KeyCtrlE, tb.KeyEnd: + ex.index = len(*ex.command) + ex.Invalidate() + case tb.KeyCtrlW: + ex.deleteWord() + default: + if event.Ch != 0 { + ex.insert(event.Ch) + } + } + } + return true +} -- cgit v1.2.3