Projects STRLCPY termdash Commits bf6b61a7
🤬
Revision indexing in progress... (symbol navigation in revisions will be accurate after indexed)
  • ■ ■ ■ ■ ■ ■
    CHANGELOG.md
    skipped 6 lines
    7 7   
    8 8  ## [Unreleased]
    9 9   
    10  -## [0.7.2] - 25-Feb-2019
    11  - 
    12 10  ### Added
    13 11   
    14 12  - New API for building layouts, a grid.Builder. Allows defining the layout
    15 13   iteratively as repetitive Elements, Rows and Columns.
    16  -- Test coverage for data only packages.
    17 14  - Containers now support margin around them and padding of their content.
     15 +- Container now supports dynamic layout changes via the new Update method.
    18 16   
    19 17  ### Changed
    20 18   
    21 19  - The Text widget now supports content wrapping on word boundaries.
    22 20  - The BarChart and SparkLine widgets now have a method that returns the
    23 21   observed value capacity the last time Draw was called.
    24  -- Refactoring packages that contained a mix of public and internal identifiers.
    25 22  - Moving widgetapi out of the internal directory to allow external users to
    26 23   develop their own widgets.
    27 24   
    skipped 6 lines
    34 31  - The BarChart, LineChart and SegmentDisplay widgets now protect against
    35 32   external mutation of the values passed into them by copying the data they
    36 33   receive.
     34 + 
     35 +## [0.7.2] - 25-Feb-2019
     36 + 
     37 +### Added
     38 + 
     39 +- Test coverage for data only packages.
     40 + 
     41 +### Changed
     42 + 
     43 +- Refactoring packages that contained a mix of public and internal identifiers.
    37 44   
    38 45  #### Breaking API changes
    39 46   
    skipped 199 lines
  • ■ ■ ■ ■ ■
    README.md
    skipped 9 lines
    10 10   
    11 11  Termdash is a cross-platform customizable terminal based dashboard.
    12 12   
    13  -[<img src="./doc/images/termdashdemo_0_7_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
     13 +[<img src="./doc/images/termdashdemo_0_8_0.gif" alt="termdashdemo" type="image/gif">](termdashdemo/termdashdemo.go)
    14 14   
    15 15  The feature set is inspired by the
    16 16  [gizak/termui](http://github.com/gizak/termui) project, which in turn was
    skipped 22 lines
    39 39   
    40 40  - Full support for terminal window resizing throughout the infrastructure.
    41 41  - Customizable layout, widget placement, borders, margins, padding, colors, etc.
     42 +- Dynamic layout changes at runtime.
    42 43  - Binary tree and Grid forms of setting up the layout.
    43 44  - Focusable containers and widgets.
    44 45  - Processing of keyboard and mouse events.
    skipped 144 lines
  • ■ ■ ■ ■ ■ ■
    container/container.go
    skipped 21 lines
    22 22  package container
    23 23   
    24 24  import (
     25 + "errors"
    25 26   "fmt"
    26 27   "image"
    27 28   "sync"
    skipped 25 lines
    53 54   focusTracker *focusTracker
    54 55   
    55 56   // area is the area of the terminal this container has access to.
     57 + // Initialized the first time Draw is called.
    56 58   area image.Rectangle
    57 59   
    58 60   // opts are the options provided to the container.
    59 61   opts *options
     62 + 
     63 + // clearNeeded indicates if the terminal needs to be cleared next time we
     64 + // are clearNeeded the container.
     65 + // This is required if the container was updated and thus the layout might
     66 + // have changed.
     67 + clearNeeded bool
    60 68   
    61 69   // mu protects the container tree.
    62 70   // All containers in the tree share the same lock.
    skipped 9 lines
    72 80  // New returns a new root container that will use the provided terminal and
    73 81  // applies the provided options.
    74 82  func New(t terminalapi.Terminal, opts ...Option) (*Container, error) {
    75  - size := t.Size()
    76 83   root := &Container{
    77 84   term: t,
    78  - // The root container has access to the entire terminal.
    79  - area: image.Rect(0, 0, size.X, size.Y),
    80 85   opts: newOptions( /* parent = */ nil),
    81 86   mu: &sync.Mutex{},
    82 87   }
    skipped 3 lines
    86 91   if err := applyOptions(root, opts...); err != nil {
    87 92   return nil, err
    88 93   }
    89  - 
    90  - ar, err := root.opts.margin.apply(root.area)
    91  - if err != nil {
     94 + if err := validateOptions(root); err != nil {
    92 95   return nil, err
    93 96   }
    94  - root.area = ar
    95 97   return root, nil
    96 98  }
    97 99   
    98 100  // newChild creates a new child container of the given parent.
    99  -func newChild(parent *Container, area image.Rectangle, opts []Option) (*Container, error) {
     101 +func newChild(parent *Container, opts []Option) (*Container, error) {
    100 102   child := &Container{
    101 103   parent: parent,
    102 104   term: parent.term,
    103 105   focusTracker: parent.focusTracker,
    104  - area: area,
    105 106   opts: newOptions(parent.opts),
    106 107   mu: parent.mu,
    107 108   }
    108 109   if err := applyOptions(child, opts...); err != nil {
    109 110   return nil, err
    110 111   }
    111  - 
    112  - ar, err := child.opts.margin.apply(child.area)
    113  - if err != nil {
    114  - return nil, err
    115  - }
    116  - child.area = ar
    117 112   return child, nil
    118 113  }
    119 114   
    skipped 64 lines
    184 179   
    185 180  // createFirst creates and returns the first sub container of this container.
    186 181  func (c *Container) createFirst(opts []Option) error {
    187  - ar, _, err := c.split()
    188  - if err != nil {
    189  - return err
    190  - }
    191  - first, err := newChild(c, ar, opts)
     182 + first, err := newChild(c, opts)
    192 183   if err != nil {
    193 184   return err
    194 185   }
    skipped 3 lines
    198 189   
    199 190  // createSecond creates and returns the second sub container of this container.
    200 191  func (c *Container) createSecond(opts []Option) error {
    201  - _, ar, err := c.split()
    202  - if err != nil {
    203  - return err
    204  - }
    205  - second, err := newChild(c, ar, opts)
     192 + second, err := newChild(c, opts)
    206 193   if err != nil {
    207 194   return err
    208 195   }
    skipped 6 lines
    215 202   c.mu.Lock()
    216 203   defer c.mu.Unlock()
    217 204   
     205 + if c.clearNeeded {
     206 + if err := c.term.Clear(); err != nil {
     207 + return fmt.Errorf("term.Clear => error: %v", err)
     208 + }
     209 + c.clearNeeded = false
     210 + }
     211 + 
    218 212   // Update the area we are tracking for focus in case the terminal size
    219 213   // changed.
    220 214   ar, err := area.FromSize(c.term.Size())
    skipped 4 lines
    225 219   return drawTree(c)
    226 220  }
    227 221   
    228  -// updateFocus processes the mouse event and determines if it changes the
    229  -// focused container.
    230  -func (c *Container) updateFocus(m *terminalapi.Mouse) {
     222 +// Update updates container with the specified id by setting the provided
     223 +// options. This can be used to perform dynamic layout changes, i.e. anything
     224 +// between replacing the widget in the container and completely changing the
     225 +// layout and splits.
     226 +// The argument id must match exactly one container with that was created with
     227 +// matching ID() option. The argument id must not be an empty string.
     228 +func (c *Container) Update(id string, opts ...Option) error {
    231 229   c.mu.Lock()
    232 230   defer c.mu.Unlock()
    233 231   
     232 + target, err := findID(c, id)
     233 + if err != nil {
     234 + return err
     235 + }
     236 + c.clearNeeded = true
     237 + 
     238 + if err := applyOptions(target, opts...); err != nil {
     239 + return err
     240 + }
     241 + if err := validateOptions(c); err != nil {
     242 + return err
     243 + }
     244 + 
     245 + // The currently focused container might not be reachable anymore, because
     246 + // it was under the target. If that is so, move the focus up to the target.
     247 + if !c.focusTracker.reachableFrom(c) {
     248 + c.focusTracker.setActive(target)
     249 + }
     250 + return nil
     251 +}
     252 + 
     253 +// updateFocus processes the mouse event and determines if it changes the
     254 +// focused container.
     255 +// Caller must hold c.mu.
     256 +func (c *Container) updateFocus(m *terminalapi.Mouse) {
    234 257   target := pointCont(c, m.Position)
    235 258   if target == nil { // Ignore mouse clicks where no containers are.
    236 259   return
    skipped 1 lines
    238 261   c.focusTracker.mouse(target, m)
    239 262  }
    240 263   
    241  -// keyboardToWidget forwards the keyboard event to the widget unconditionally.
    242  -func (c *Container) keyboardToWidget(k *terminalapi.Keyboard, scope widgetapi.KeyScope) error {
     264 +// processEvent processes events delivered to the container.
     265 +func (c *Container) processEvent(ev terminalapi.Event) error {
     266 + // This is done in two stages.
     267 + // 1) under lock we traverse the container and identify all targets
     268 + // (widgets) that should receive the event.
     269 + // 2) lock is released and events are delivered to the widgets. Widgets
     270 + // themselves are thread-safe. Lock must be releases when delivering,
     271 + // because some widgets might try to mutate the container when they
     272 + // receive the event, like dynamically change the layout.
    243 273   c.mu.Lock()
    244  - defer c.mu.Unlock()
    245  - 
    246  - if scope == widgetapi.KeyScopeFocused && !c.focusTracker.isActive(c) {
    247  - return nil
     274 + sendFn, err := c.prepareEvTargets(ev)
     275 + c.mu.Unlock()
     276 + if err != nil {
     277 + return err
    248 278   }
    249  - return c.opts.widget.Keyboard(k)
     279 + return sendFn()
    250 280  }
    251 281   
    252  -// mouseToWidget forwards the mouse event to the widget.
    253  -func (c *Container) mouseToWidget(m *terminalapi.Mouse, scope widgetapi.MouseScope) error {
    254  - c.mu.Lock()
    255  - defer c.mu.Unlock()
     282 +// prepareEvTargets returns a closure, that when called delivers the event to
     283 +// widgets that registered for it.
     284 +// Also processes the event on behalf of the container (tracks keyboard focus).
     285 +// Caller must hold c.mu.
     286 +func (c *Container) prepareEvTargets(ev terminalapi.Event) (func() error, error) {
     287 + switch e := ev.(type) {
     288 + case *terminalapi.Mouse:
     289 + c.updateFocus(ev.(*terminalapi.Mouse))
    256 290   
    257  - target := pointCont(c, m.Position)
    258  - if target == nil { // Ignore mouse clicks where no containers are.
    259  - return nil
    260  - }
     291 + targets, err := c.mouseEvTargets(e)
     292 + if err != nil {
     293 + return nil, err
     294 + }
     295 + return func() error {
     296 + for _, mt := range targets {
     297 + if err := mt.widget.Mouse(mt.ev); err != nil {
     298 + return err
     299 + }
     300 + }
     301 + return nil
     302 + }, nil
    261 303   
    262  - // Ignore clicks falling outside of the container.
    263  - if scope != widgetapi.MouseScopeGlobal && !m.Position.In(c.area) {
    264  - return nil
    265  - }
     304 + case *terminalapi.Keyboard:
     305 + targets := c.keyEvTargets()
     306 + return func() error {
     307 + for _, w := range targets {
     308 + if err := w.Keyboard(e); err != nil {
     309 + return err
     310 + }
     311 + }
     312 + return nil
     313 + }, nil
    266 314   
    267  - // Ignore clicks falling outside of the widget's canvas.
    268  - wa, err := c.widgetArea()
    269  - if err != nil {
    270  - return err
     315 + default:
     316 + return nil, fmt.Errorf("container received an unsupported event type %T", ev)
    271 317   }
    272  - if scope == widgetapi.MouseScopeWidget && !m.Position.In(wa) {
     318 +}
     319 + 
     320 +// keyEvTargets returns those widgets found in the container that should
     321 +// receive this keyboard event.
     322 +// Caller must hold c.mu.
     323 +func (c *Container) keyEvTargets() []widgetapi.Widget {
     324 + var (
     325 + errStr string
     326 + widgets []widgetapi.Widget
     327 + )
     328 + 
     329 + // All the widgets that should receive this event.
     330 + // For now stable ordering (preOrder).
     331 + preOrder(c, &errStr, visitFunc(func(cur *Container) error {
     332 + if !cur.hasWidget() {
     333 + return nil
     334 + }
     335 + 
     336 + wOpt := cur.opts.widget.Options()
     337 + switch wOpt.WantKeyboard {
     338 + case widgetapi.KeyScopeNone:
     339 + // Widget doesn't want any keyboard events.
     340 + return nil
     341 + 
     342 + case widgetapi.KeyScopeFocused:
     343 + if cur.focusTracker.isActive(cur) {
     344 + widgets = append(widgets, cur.opts.widget)
     345 + }
     346 + 
     347 + case widgetapi.KeyScopeGlobal:
     348 + widgets = append(widgets, cur.opts.widget)
     349 + }
    273 350   return nil
     351 + }))
     352 + return widgets
     353 +}
     354 + 
     355 +// mouseEvTarget contains a mouse event adjusted relative to the widget's area
     356 +// and the widget that should receive it.
     357 +type mouseEvTarget struct {
     358 + // widget is the widget that should receive the mouse event.
     359 + widget widgetapi.Widget
     360 + // ev is the adjusted mouse event.
     361 + ev *terminalapi.Mouse
     362 +}
     363 + 
     364 +// newMouseEvTarget returns a new newMouseEvTarget.
     365 +func newMouseEvTarget(w widgetapi.Widget, wArea image.Rectangle, ev *terminalapi.Mouse) *mouseEvTarget {
     366 + return &mouseEvTarget{
     367 + widget: w,
     368 + ev: adjustMouseEv(ev, wArea),
    274 369   }
     370 +}
    275 371   
    276  - // The sent mouse coordinate is relative to the widget canvas, i.e. zero
    277  - // based, even though the widget might not be in the top left corner on the
    278  - // terminal.
    279  - offset := wa.Min
    280  - var wm *terminalapi.Mouse
    281  - if m.Position.In(wa) {
    282  - wm = &terminalapi.Mouse{
    283  - Position: m.Position.Sub(offset),
    284  - Button: m.Button,
     372 +// mouseEvTargets returns those widgets found in the container that should
     373 +// receive this mouse event.
     374 +// Caller must hold c.mu.
     375 +func (c *Container) mouseEvTargets(m *terminalapi.Mouse) ([]*mouseEvTarget, error) {
     376 + var (
     377 + errStr string
     378 + widgets []*mouseEvTarget
     379 + )
     380 + 
     381 + // All the widgets that should receive this event.
     382 + // For now stable ordering (preOrder).
     383 + preOrder(c, &errStr, visitFunc(func(cur *Container) error {
     384 + if !cur.hasWidget() {
     385 + return nil
    285 386   }
    286  - } else {
    287  - wm = &terminalapi.Mouse{
    288  - Position: image.Point{-1, -1},
    289  - Button: m.Button,
     387 + 
     388 + wOpts := cur.opts.widget.Options()
     389 + wa, err := cur.widgetArea()
     390 + if err != nil {
     391 + return err
    290 392   }
     393 + 
     394 + switch wOpts.WantMouse {
     395 + case widgetapi.MouseScopeNone:
     396 + // Widget doesn't want any mouse events.
     397 + return nil
     398 + 
     399 + case widgetapi.MouseScopeWidget:
     400 + // Only if the event falls inside of the widget's canvas.
     401 + if m.Position.In(wa) {
     402 + widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
     403 + }
     404 + 
     405 + case widgetapi.MouseScopeContainer:
     406 + // Only if the event falls inside the widget's parent container.
     407 + if m.Position.In(cur.area) {
     408 + widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
     409 + }
     410 + 
     411 + case widgetapi.MouseScopeGlobal:
     412 + // Widget wants all mouse events.
     413 + widgets = append(widgets, newMouseEvTarget(cur.opts.widget, wa, m))
     414 + }
     415 + return nil
     416 + }))
     417 + 
     418 + if errStr != "" {
     419 + return nil, errors.New(errStr)
    291 420   }
    292  - return c.opts.widget.Mouse(wm)
     421 + return widgets, nil
    293 422  }
    294 423   
    295 424  // Subscribe tells the container to subscribe itself and widgets to the
    skipped 8 lines
    304 433   // before we throttle them.
    305 434   const maxReps = 10
    306 435   
    307  - root := rootCont(c)
    308 436   // Subscriber the container itself in order to track keyboard focus.
    309  - eds.Subscribe([]terminalapi.Event{&terminalapi.Mouse{}}, func(ev terminalapi.Event) {
    310  - root.updateFocus(ev.(*terminalapi.Mouse))
    311  - }, event.MaxRepetitive(0)) // One event is enough to change the focus.
    312  - 
    313  - // Subscribe any widgets that specify Keyboard or Mouse in their options.
    314  - var errStr string
    315  - preOrder(root, &errStr, visitFunc(func(c *Container) error {
    316  - if c.hasWidget() {
    317  - wOpt := c.opts.widget.Options()
    318  - switch scope := wOpt.WantKeyboard; scope {
    319  - case widgetapi.KeyScopeNone:
    320  - // Widget doesn't want any keyboard events.
    321  - 
    322  - default:
    323  - eds.Subscribe([]terminalapi.Event{&terminalapi.Keyboard{}}, func(ev terminalapi.Event) {
    324  - if err := c.keyboardToWidget(ev.(*terminalapi.Keyboard), scope); err != nil {
    325  - eds.Event(terminalapi.NewErrorf("failed to send global keyboard event %v to widget %T: %v", ev, c.opts.widget, err))
    326  - }
    327  - }, event.MaxRepetitive(maxReps))
    328  - }
    329  - 
    330  - switch scope := wOpt.WantMouse; scope {
    331  - case widgetapi.MouseScopeNone:
    332  - // Widget doesn't want any mouse events.
     437 + want := []terminalapi.Event{
     438 + &terminalapi.Keyboard{},
     439 + &terminalapi.Mouse{},
     440 + }
     441 + eds.Subscribe(want, func(ev terminalapi.Event) {
     442 + if err := c.processEvent(ev); err != nil {
     443 + eds.Event(terminalapi.NewErrorf("failed to process event %v: %v", ev, err))
     444 + }
     445 + }, event.MaxRepetitive(maxReps))
     446 +}
    333 447   
    334  - default:
    335  - eds.Subscribe([]terminalapi.Event{&terminalapi.Mouse{}}, func(ev terminalapi.Event) {
    336  - if err := c.mouseToWidget(ev.(*terminalapi.Mouse), scope); err != nil {
    337  - eds.Event(terminalapi.NewErrorf("failed to send mouse event %v to widget %T: %v", ev, c.opts.widget, err))
    338  - }
    339  - }, event.MaxRepetitive(maxReps))
    340  - }
     448 +// adjustMouseEv adjusts the mouse event relative to the widget area.
     449 +func adjustMouseEv(m *terminalapi.Mouse, wArea image.Rectangle) *terminalapi.Mouse {
     450 + // The sent mouse coordinate is relative to the widget canvas, i.e. zero
     451 + // based, even though the widget might not be in the top left corner on the
     452 + // terminal.
     453 + offset := wArea.Min
     454 + if m.Position.In(wArea) {
     455 + return &terminalapi.Mouse{
     456 + Position: m.Position.Sub(offset),
     457 + Button: m.Button,
    341 458   }
    342  - return nil
    343  - }))
     459 + }
     460 + return &terminalapi.Mouse{
     461 + Position: image.Point{-1, -1},
     462 + Button: m.Button,
     463 + }
    344 464  }
    345 465   
  • ■ ■ ■ ■ ■
    container/container_test.go
    skipped 91 lines
    92 92   wantContainerErr: true,
    93 93   },
    94 94   {
     95 + desc: "fails on invalid option on the first vertical child container",
     96 + termSize: image.Point{10, 10},
     97 + container: func(ft *faketerm.Terminal) (*Container, error) {
     98 + return New(
     99 + ft,
     100 + SplitVertical(
     101 + Left(
     102 + MarginTop(-1),
     103 + ),
     104 + Right(),
     105 + ),
     106 + )
     107 + },
     108 + wantContainerErr: true,
     109 + },
     110 + {
     111 + desc: "fails on invalid option on the second vertical child container",
     112 + termSize: image.Point{10, 10},
     113 + container: func(ft *faketerm.Terminal) (*Container, error) {
     114 + return New(
     115 + ft,
     116 + SplitVertical(
     117 + Left(),
     118 + Right(
     119 + MarginTop(-1),
     120 + ),
     121 + ),
     122 + )
     123 + },
     124 + wantContainerErr: true,
     125 + },
     126 + {
     127 + desc: "fails on invalid option on the first horizontal child container",
     128 + termSize: image.Point{10, 10},
     129 + container: func(ft *faketerm.Terminal) (*Container, error) {
     130 + return New(
     131 + ft,
     132 + SplitHorizontal(
     133 + Top(
     134 + MarginTop(-1),
     135 + ),
     136 + Bottom(),
     137 + ),
     138 + )
     139 + },
     140 + wantContainerErr: true,
     141 + },
     142 + {
     143 + desc: "fails on invalid option on the second horizontal child container",
     144 + termSize: image.Point{10, 10},
     145 + container: func(ft *faketerm.Terminal) (*Container, error) {
     146 + return New(
     147 + ft,
     148 + SplitHorizontal(
     149 + Top(),
     150 + Bottom(
     151 + MarginTop(-1),
     152 + ),
     153 + ),
     154 + )
     155 + },
     156 + wantContainerErr: true,
     157 + },
     158 + {
    95 159   desc: "fails on MarginTopPercent too low",
    96 160   termSize: image.Point{10, 10},
    97 161   container: func(ft *faketerm.Terminal) (*Container, error) {
    skipped 302 lines
    400 464   termSize: image.Point{10, 10},
    401 465   container: func(ft *faketerm.Terminal) (*Container, error) {
    402 466   return New(ft, PaddingLeftPercent(1), PaddingLeft(1))
     467 + },
     468 + wantContainerErr: true,
     469 + },
     470 + {
     471 + desc: "fails on empty ID specified",
     472 + termSize: image.Point{10, 10},
     473 + container: func(ft *faketerm.Terminal) (*Container, error) {
     474 + return New(ft, ID(""))
     475 + },
     476 + wantContainerErr: true,
     477 + },
     478 + {
     479 + desc: "fails on empty duplicate ID specified",
     480 + termSize: image.Point{10, 10},
     481 + container: func(ft *faketerm.Terminal) (*Container, error) {
     482 + return New(
     483 + ft,
     484 + ID("0"),
     485 + SplitHorizontal(
     486 + Top(ID("1")),
     487 + Bottom(ID("1")),
     488 + ),
     489 + )
    403 490   },
    404 491   wantContainerErr: true,
    405 492   },
    skipped 436 lines
    842 929   if err != nil {
    843 930   return
    844 931   }
     932 + contStr := cont.String()
     933 + t.Logf("For container: %v", contStr)
    845 934   if err := cont.Draw(); err != nil {
    846 935   t.Fatalf("Draw => unexpected error: %v", err)
    847 936   }
    skipped 16 lines
    864 953   
    865 954  }
    866 955   
    867  -// eventGroup is a group of events to be delivered with synchronization.
    868  -// I.e. the test execution waits until the specified number is processed before
    869  -// proceeding with test execution.
    870  -type eventGroup struct {
    871  - events []terminalapi.Event
    872  - wantProcessed int
    873  -}
    874  - 
    875 956  // errorHandler just stores the last error received.
    876 957  type errorHandler struct {
    877 958   err error
    skipped 14 lines
    892 973   
    893 974  func TestKeyboard(t *testing.T) {
    894 975   tests := []struct {
    895  - desc string
    896  - termSize image.Point
    897  - container func(ft *faketerm.Terminal) (*Container, error)
    898  - eventGroups []*eventGroup
    899  - want func(size image.Point) *faketerm.Terminal
    900  - wantErr bool
     976 + desc string
     977 + termSize image.Point
     978 + container func(ft *faketerm.Terminal) (*Container, error)
     979 + events []terminalapi.Event
     980 + // If specified, waits for this number of events.
     981 + // Otherwise waits for len(events).
     982 + wantProcessed int
     983 + want func(size image.Point) *faketerm.Terminal
     984 + wantErr bool
    901 985   }{
    902 986   {
    903 987   desc: "event not forwarded if container has no widget",
    skipped 1 lines
    905 989   container: func(ft *faketerm.Terminal) (*Container, error) {
    906 990   return New(ft)
    907 991   },
    908  - eventGroups: []*eventGroup{
    909  - {
    910  - events: []terminalapi.Event{
    911  - &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    912  - },
    913  - wantProcessed: 0,
    914  - },
     992 + events: []terminalapi.Event{
     993 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    915 994   },
    916 995   want: func(size image.Point) *faketerm.Terminal {
    917 996   return faketerm.MustNew(size)
    skipped 22 lines
    940 1019   ),
    941 1020   )
    942 1021   },
    943  - eventGroups: []*eventGroup{
     1022 + events: []terminalapi.Event{
    944 1023   // Move focus to the target container.
    945  - {
    946  - events: []terminalapi.Event{
    947  - &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonLeft},
    948  - &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonRelease},
    949  - },
    950  - wantProcessed: 2,
    951  - },
     1024 + &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonLeft},
     1025 + &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonRelease},
    952 1026   // Send the keyboard event.
    953  - {
    954  - events: []terminalapi.Event{
    955  - &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    956  - },
    957  - wantProcessed: 5,
    958  - },
     1027 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    959 1028   },
    960  - 
    961 1029   want: func(size image.Point) *faketerm.Terminal {
    962 1030   ft := faketerm.MustNew(size)
    963 1031   
    skipped 42 lines
    1006 1074   ),
    1007 1075   )
    1008 1076   },
    1009  - eventGroups: []*eventGroup{
     1077 + events: []terminalapi.Event{
    1010 1078   // Move focus to the target container.
    1011  - {
    1012  - events: []terminalapi.Event{
    1013  - &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonLeft},
    1014  - &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonRelease},
    1015  - },
    1016  - wantProcessed: 2,
    1017  - },
     1079 + &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonLeft},
     1080 + &terminalapi.Mouse{Position: image.Point{39, 19}, Button: mouse.ButtonRelease},
    1018 1081   // Send the keyboard event.
    1019  - {
    1020  - events: []terminalapi.Event{
    1021  - &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    1022  - },
    1023  - wantProcessed: 5,
    1024  - },
     1082 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    1025 1083   },
    1026  - 
    1027 1084   want: func(size image.Point) *faketerm.Terminal {
    1028 1085   ft := faketerm.MustNew(size)
    1029 1086   
    skipped 32 lines
    1062 1119   PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeNone})),
    1063 1120   )
    1064 1121   },
    1065  - eventGroups: []*eventGroup{
    1066  - {
    1067  - events: []terminalapi.Event{
    1068  - &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    1069  - },
    1070  - wantProcessed: 0,
    1071  - },
     1122 + events: []terminalapi.Event{
     1123 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
    1072 1124   },
    1073 1125   want: func(size image.Point) *faketerm.Terminal {
    1074 1126   ft := faketerm.MustNew(size)
    skipped 15 lines
    1090 1142   PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
    1091 1143   )
    1092 1144   },
    1093  - eventGroups: []*eventGroup{
    1094  - {
    1095  - events: []terminalapi.Event{
    1096  - &terminalapi.Keyboard{Key: keyboard.KeyEsc},
    1097  - },
    1098  - wantProcessed: 2,
    1099  - },
     1145 + events: []terminalapi.Event{
     1146 + &terminalapi.Keyboard{Key: keyboard.KeyEsc},
    1100 1147   },
     1148 + wantProcessed: 2, // The error is also an event.
    1101 1149   want: func(size image.Point) *faketerm.Terminal {
    1102 1150   ft := faketerm.MustNew(size)
    1103 1151   
    skipped 28 lines
    1132 1180   })
    1133 1181   
    1134 1182   c.Subscribe(eds)
    1135  - for _, eg := range tc.eventGroups {
    1136  - for _, ev := range eg.events {
    1137  - eds.Event(ev)
     1183 + // Initial draw to determine sizes of containers.
     1184 + if err := c.Draw(); err != nil {
     1185 + t.Fatalf("Draw => unexpected error: %v", err)
     1186 + }
     1187 + for _, ev := range tc.events {
     1188 + eds.Event(ev)
     1189 + }
     1190 + var wantEv int
     1191 + if tc.wantProcessed != 0 {
     1192 + wantEv = tc.wantProcessed
     1193 + } else {
     1194 + wantEv = len(tc.events)
     1195 + }
     1196 + 
     1197 + if err := testevent.WaitFor(5*time.Second, func() error {
     1198 + if got, want := eds.Processed(), wantEv; got != want {
     1199 + return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
    1138 1200   }
    1139  - if err := testevent.WaitFor(5*time.Second, func() error {
    1140  - if got, want := eds.Processed(), eg.wantProcessed; got != want {
    1141  - return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
    1142  - }
    1143  - return nil
    1144  - }); err != nil {
    1145  - t.Fatalf("testevent.WaitFor => %v", err)
    1146  - }
     1201 + return nil
     1202 + }); err != nil {
     1203 + t.Fatalf("testevent.WaitFor => %v", err)
    1147 1204   }
    1148 1205   
    1149 1206   if err := c.Draw(); err != nil {
    skipped 13 lines
    1163 1220   
    1164 1221  func TestMouse(t *testing.T) {
    1165 1222   tests := []struct {
    1166  - desc string
    1167  - termSize image.Point
    1168  - container func(ft *faketerm.Terminal) (*Container, error)
    1169  - events []terminalapi.Event
    1170  - want func(size image.Point) *faketerm.Terminal
     1223 + desc string
     1224 + termSize image.Point
     1225 + container func(ft *faketerm.Terminal) (*Container, error)
     1226 + events []terminalapi.Event
     1227 + // If specified, waits for this number of events.
     1228 + // Otherwise waits for len(events).
    1171 1229   wantProcessed int
     1230 + want func(size image.Point) *faketerm.Terminal
    1172 1231   wantErr bool
    1173 1232   }{
    1174 1233   {
    skipped 19 lines
    1194 1253   )
    1195 1254   return ft
    1196 1255   },
    1197  - wantProcessed: 4,
    1198 1256   },
    1199 1257   {
    1200 1258   desc: "event not forwarded if container has no widget",
    skipped 8 lines
    1209 1267   want: func(size image.Point) *faketerm.Terminal {
    1210 1268   return faketerm.MustNew(size)
    1211 1269   },
    1212  - wantProcessed: 2,
    1213 1270   },
    1214 1271   {
    1215 1272   desc: "event forwarded to container at that point",
    skipped 47 lines
    1263 1320   )
    1264 1321   return ft
    1265 1322   },
    1266  - wantProcessed: 8,
    1267 1323   },
    1268 1324   {
    1269 1325   desc: "event focuses the target container after terminal resize (falls onto the new area), regression for #169",
    skipped 74 lines
    1344 1400   )
    1345 1401   return ft
    1346 1402   },
    1347  - wantProcessed: 8,
    1348 1403   },
    1349 1404   {
    1350 1405   desc: "event not forwarded if the widget didn't request it",
    skipped 17 lines
    1368 1423   )
    1369 1424   return ft
    1370 1425   },
    1371  - wantProcessed: 1,
    1372 1426   },
    1373 1427   {
    1374 1428   desc: "MouseScopeWidget, event not forwarded if it falls on the container's border",
    skipped 28 lines
    1403 1457   )
    1404 1458   return ft
    1405 1459   },
    1406  - wantProcessed: 2,
    1407 1460   },
    1408 1461   {
    1409 1462   desc: "MouseScopeContainer, event forwarded if it falls on the container's border",
    skipped 29 lines
    1439 1492   )
    1440 1493   return ft
    1441 1494   },
    1442  - wantProcessed: 2,
    1443 1495   },
    1444 1496   {
    1445 1497   desc: "MouseScopeGlobal, event forwarded if it falls on the container's border",
    skipped 29 lines
    1475 1527   )
    1476 1528   return ft
    1477 1529   },
    1478  - wantProcessed: 2,
    1479 1530   },
    1480 1531   {
    1481 1532   desc: "MouseScopeWidget event not forwarded if it falls outside of widget's canvas",
    skipped 24 lines
    1506 1557   )
    1507 1558   return ft
    1508 1559   },
    1509  - wantProcessed: 2,
    1510 1560   },
    1511 1561   {
    1512 1562   desc: "MouseScopeContainer event forwarded if it falls outside of widget's canvas",
    skipped 25 lines
    1538 1588   )
    1539 1589   return ft
    1540 1590   },
    1541  - wantProcessed: 2,
    1542 1591   },
    1543 1592   {
    1544 1593   desc: "MouseScopeGlobal event forwarded if it falls outside of widget's canvas",
    skipped 25 lines
    1570 1619   )
    1571 1620   return ft
    1572 1621   },
    1573  - wantProcessed: 2,
    1574 1622   },
    1575 1623   {
    1576 1624   desc: "MouseScopeWidget event not forwarded if it falls to another container",
    skipped 29 lines
    1606 1654   )
    1607 1655   return ft
    1608 1656   },
    1609  - wantProcessed: 2,
    1610 1657   },
    1611 1658   {
    1612 1659   desc: "MouseScopeContainer event not forwarded if it falls to another container",
    skipped 29 lines
    1642 1689   )
    1643 1690   return ft
    1644 1691   },
    1645  - wantProcessed: 2,
    1646 1692   },
    1647 1693   {
    1648 1694   desc: "MouseScopeGlobal event forwarded if it falls to another container",
    skipped 30 lines
    1679 1725   )
    1680 1726   return ft
    1681 1727   },
    1682  - wantProcessed: 2,
    1683 1728   },
    1684 1729   {
    1685 1730   desc: "mouse position adjusted relative to widget's canvas, vertical offset",
    skipped 25 lines
    1711 1756   )
    1712 1757   return ft
    1713 1758   },
    1714  - wantProcessed: 2,
    1715 1759   },
    1716 1760   {
    1717  - desc: "mouse poisition adjusted relative to widget's canvas, horizontal offset",
     1761 + desc: "mouse position adjusted relative to widget's canvas, horizontal offset",
    1718 1762   termSize: image.Point{30, 20},
    1719 1763   container: func(ft *faketerm.Terminal) (*Container, error) {
    1720 1764   return New(
    skipped 22 lines
    1743 1787   )
    1744 1788   return ft
    1745 1789   },
    1746  - wantProcessed: 2,
    1747 1790   },
    1748 1791   {
    1749 1792   desc: "widget returns an error when processing the event",
    skipped 7 lines
    1757 1800   events: []terminalapi.Event{
    1758 1801   &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRight},
    1759 1802   },
     1803 + wantProcessed: 2, // The error is also an event.
    1760 1804   want: func(size image.Point) *faketerm.Terminal {
    1761 1805   ft := faketerm.MustNew(size)
    1762 1806   
    skipped 4 lines
    1767 1811   )
    1768 1812   return ft
    1769 1813   },
    1770  - wantProcessed: 3,
    1771  - wantErr: true,
     1814 + wantErr: true,
    1772 1815   },
    1773 1816   }
    1774 1817   
    skipped 16 lines
    1791 1834   eh.handle(ev.(*terminalapi.Error).Error())
    1792 1835   })
    1793 1836   c.Subscribe(eds)
     1837 + // Initial draw to determine sizes of containers.
     1838 + if err := c.Draw(); err != nil {
     1839 + t.Fatalf("Draw => unexpected error: %v", err)
     1840 + }
    1794 1841   for _, ev := range tc.events {
    1795 1842   eds.Event(ev)
    1796 1843   }
     1844 + var wantEv int
     1845 + if tc.wantProcessed != 0 {
     1846 + wantEv = tc.wantProcessed
     1847 + } else {
     1848 + wantEv = len(tc.events)
     1849 + }
     1850 + 
    1797 1851   if err := testevent.WaitFor(5*time.Second, func() error {
    1798  - if got, want := eds.Processed(), tc.wantProcessed; got != want {
     1852 + if got, want := eds.Processed(), wantEv; got != want {
    1799 1853   return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
    1800 1854   }
    1801 1855   return nil
    skipped 16 lines
    1818 1872   }
    1819 1873  }
    1820 1874   
     1875 +func TestUpdate(t *testing.T) {
     1876 + tests := []struct {
     1877 + desc string
     1878 + termSize image.Point
     1879 + container func(ft *faketerm.Terminal) (*Container, error)
     1880 + updateID string
     1881 + updateOpts []Option
     1882 + // events are events delivered before the update.
     1883 + beforeEvents []terminalapi.Event
     1884 + // events are events delivered after the update.
     1885 + afterEvents []terminalapi.Event
     1886 + wantUpdateErr bool
     1887 + want func(size image.Point) *faketerm.Terminal
     1888 + }{
     1889 + {
     1890 + desc: "fails on empty updateID",
     1891 + termSize: image.Point{10, 10},
     1892 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1893 + return New(ft)
     1894 + },
     1895 + wantUpdateErr: true,
     1896 + },
     1897 + {
     1898 + desc: "fails when no container with the ID is found",
     1899 + termSize: image.Point{10, 10},
     1900 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1901 + return New(ft)
     1902 + },
     1903 + updateID: "myID",
     1904 + wantUpdateErr: true,
     1905 + },
     1906 + {
     1907 + desc: "no changes when no options are provided",
     1908 + termSize: image.Point{10, 10},
     1909 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1910 + return New(
     1911 + ft,
     1912 + ID("myID"),
     1913 + Border(linestyle.Light),
     1914 + )
     1915 + },
     1916 + updateID: "myID",
     1917 + want: func(size image.Point) *faketerm.Terminal {
     1918 + ft := faketerm.MustNew(size)
     1919 + cvs := testcanvas.MustNew(ft.Area())
     1920 + testdraw.MustBorder(
     1921 + cvs,
     1922 + image.Rect(0, 0, 10, 10),
     1923 + draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
     1924 + )
     1925 + testcanvas.MustApply(cvs, ft)
     1926 + return ft
     1927 + },
     1928 + },
     1929 + {
     1930 + desc: "fails on invalid options",
     1931 + termSize: image.Point{10, 10},
     1932 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1933 + return New(
     1934 + ft,
     1935 + ID("myID"),
     1936 + Border(linestyle.Light),
     1937 + )
     1938 + },
     1939 + updateID: "myID",
     1940 + updateOpts: []Option{
     1941 + MarginTop(-1),
     1942 + },
     1943 + wantUpdateErr: true,
     1944 + },
     1945 + {
     1946 + desc: "fails when update introduces a duplicate ID",
     1947 + termSize: image.Point{10, 10},
     1948 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1949 + return New(
     1950 + ft,
     1951 + ID("myID"),
     1952 + Border(linestyle.Light),
     1953 + )
     1954 + },
     1955 + updateID: "myID",
     1956 + updateOpts: []Option{
     1957 + SplitVertical(
     1958 + Left(
     1959 + ID("left"),
     1960 + ),
     1961 + Right(
     1962 + ID("myID"),
     1963 + ),
     1964 + ),
     1965 + },
     1966 + wantUpdateErr: true,
     1967 + },
     1968 + {
     1969 + desc: "removes border from the container",
     1970 + termSize: image.Point{10, 10},
     1971 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1972 + return New(
     1973 + ft,
     1974 + ID("myID"),
     1975 + Border(linestyle.Light),
     1976 + )
     1977 + },
     1978 + updateID: "myID",
     1979 + updateOpts: []Option{
     1980 + Border(linestyle.None),
     1981 + },
     1982 + want: func(size image.Point) *faketerm.Terminal {
     1983 + return faketerm.MustNew(size)
     1984 + },
     1985 + },
     1986 + {
     1987 + desc: "places widget into a sub-container container",
     1988 + termSize: image.Point{20, 10},
     1989 + container: func(ft *faketerm.Terminal) (*Container, error) {
     1990 + return New(
     1991 + ft,
     1992 + ID("myRoot"),
     1993 + SplitVertical(
     1994 + Left(
     1995 + ID("left"),
     1996 + ),
     1997 + Right(
     1998 + ID("right"),
     1999 + ),
     2000 + ),
     2001 + )
     2002 + },
     2003 + updateID: "right",
     2004 + updateOpts: []Option{
     2005 + PlaceWidget(fakewidget.New(widgetapi.Options{})),
     2006 + },
     2007 + want: func(size image.Point) *faketerm.Terminal {
     2008 + ft := faketerm.MustNew(size)
     2009 + cvs := testcanvas.MustNew(ft.Area())
     2010 + wAr := image.Rect(10, 0, 20, 10)
     2011 + wCvs := testcanvas.MustNew(wAr)
     2012 + fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
     2013 + testcanvas.MustCopyTo(wCvs, cvs)
     2014 + testcanvas.MustApply(cvs, ft)
     2015 + return ft
     2016 + },
     2017 + },
     2018 + {
     2019 + desc: "places widget into root which removes children",
     2020 + termSize: image.Point{20, 10},
     2021 + container: func(ft *faketerm.Terminal) (*Container, error) {
     2022 + return New(
     2023 + ft,
     2024 + ID("myRoot"),
     2025 + SplitVertical(
     2026 + Left(
     2027 + ID("left"),
     2028 + Border(linestyle.Light),
     2029 + ),
     2030 + Right(
     2031 + ID("right"),
     2032 + Border(linestyle.Light),
     2033 + ),
     2034 + ),
     2035 + )
     2036 + },
     2037 + updateID: "myRoot",
     2038 + updateOpts: []Option{
     2039 + PlaceWidget(fakewidget.New(widgetapi.Options{})),
     2040 + },
     2041 + want: func(size image.Point) *faketerm.Terminal {
     2042 + ft := faketerm.MustNew(size)
     2043 + cvs := testcanvas.MustNew(ft.Area())
     2044 + fakewidget.MustDraw(ft, cvs, widgetapi.Options{})
     2045 + testcanvas.MustApply(cvs, ft)
     2046 + return ft
     2047 + },
     2048 + },
     2049 + {
     2050 + desc: "changes container splits",
     2051 + termSize: image.Point{10, 10},
     2052 + container: func(ft *faketerm.Terminal) (*Container, error) {
     2053 + return New(
     2054 + ft,
     2055 + ID("myRoot"),
     2056 + SplitVertical(
     2057 + Left(
     2058 + ID("left"),
     2059 + Border(linestyle.Light),
     2060 + ),
     2061 + Right(
     2062 + ID("right"),
     2063 + Border(linestyle.Light),
     2064 + ),
     2065 + ),
     2066 + )
     2067 + },
     2068 + updateID: "myRoot",
     2069 + updateOpts: []Option{
     2070 + SplitHorizontal(
     2071 + Top(
     2072 + ID("left"),
     2073 + Border(linestyle.Light),
     2074 + ),
     2075 + Bottom(
     2076 + ID("right"),
     2077 + Border(linestyle.Light),
     2078 + ),
     2079 + ),
     2080 + },
     2081 + want: func(size image.Point) *faketerm.Terminal {
     2082 + ft := faketerm.MustNew(size)
     2083 + cvs := testcanvas.MustNew(ft.Area())
     2084 + testdraw.MustBorder(cvs, image.Rect(0, 0, 10, 5))
     2085 + testdraw.MustBorder(cvs, image.Rect(0, 5, 10, 10))
     2086 + testcanvas.MustApply(cvs, ft)
     2087 + return ft
     2088 + },
     2089 + },
     2090 + {
     2091 + desc: "update retains original focused container if it still exists",
     2092 + termSize: image.Point{10, 10},
     2093 + container: func(ft *faketerm.Terminal) (*Container, error) {
     2094 + return New(
     2095 + ft,
     2096 + ID("myRoot"),
     2097 + SplitVertical(
     2098 + Left(
     2099 + ID("left"),
     2100 + Border(linestyle.Light),
     2101 + ),
     2102 + Right(
     2103 + ID("right"),
     2104 + Border(linestyle.Light),
     2105 + SplitHorizontal(
     2106 + Top(
     2107 + ID("rightTop"),
     2108 + Border(linestyle.Light),
     2109 + ),
     2110 + Bottom(
     2111 + ID("rightBottom"),
     2112 + Border(linestyle.Light),
     2113 + ),
     2114 + ),
     2115 + ),
     2116 + ),
     2117 + )
     2118 + },
     2119 + beforeEvents: []terminalapi.Event{
     2120 + // Move focus to container with ID "right".
     2121 + // It will continue to exist after the update.
     2122 + &terminalapi.Mouse{Position: image.Point{5, 0}, Button: mouse.ButtonLeft},
     2123 + &terminalapi.Mouse{Position: image.Point{5, 0}, Button: mouse.ButtonRelease},
     2124 + },
     2125 + updateID: "right",
     2126 + updateOpts: []Option{
     2127 + Clear(),
     2128 + },
     2129 + want: func(size image.Point) *faketerm.Terminal {
     2130 + ft := faketerm.MustNew(size)
     2131 + cvs := testcanvas.MustNew(ft.Area())
     2132 + testdraw.MustBorder(cvs, image.Rect(0, 0, 5, 10))
     2133 + testdraw.MustBorder(cvs, image.Rect(5, 0, 10, 10), draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)))
     2134 + testcanvas.MustApply(cvs, ft)
     2135 + return ft
     2136 + },
     2137 + },
     2138 + {
     2139 + desc: "update moves focus to the nearest parent when focused container is destroyed",
     2140 + termSize: image.Point{10, 10},
     2141 + container: func(ft *faketerm.Terminal) (*Container, error) {
     2142 + return New(
     2143 + ft,
     2144 + ID("myRoot"),
     2145 + SplitVertical(
     2146 + Left(
     2147 + ID("left"),
     2148 + Border(linestyle.Light),
     2149 + ),
     2150 + Right(
     2151 + ID("right"),
     2152 + Border(linestyle.Light),
     2153 + SplitHorizontal(
     2154 + Top(
     2155 + ID("rightTop"),
     2156 + Border(linestyle.Light),
     2157 + ),
     2158 + Bottom(
     2159 + ID("rightBottom"),
     2160 + Border(linestyle.Light),
     2161 + ),
     2162 + ),
     2163 + ),
     2164 + ),
     2165 + )
     2166 + },
     2167 + beforeEvents: []terminalapi.Event{
     2168 + // Move focus to container with ID "rightTop".
     2169 + // It will be destroyed by calling update.
     2170 + &terminalapi.Mouse{Position: image.Point{6, 1}, Button: mouse.ButtonLeft},
     2171 + &terminalapi.Mouse{Position: image.Point{6, 1}, Button: mouse.ButtonRelease},
     2172 + },
     2173 + updateID: "right",
     2174 + updateOpts: []Option{
     2175 + Clear(),
     2176 + },
     2177 + want: func(size image.Point) *faketerm.Terminal {
     2178 + ft := faketerm.MustNew(size)
     2179 + cvs := testcanvas.MustNew(ft.Area())
     2180 + testdraw.MustBorder(cvs, image.Rect(0, 0, 5, 10))
     2181 + testdraw.MustBorder(cvs, image.Rect(5, 0, 10, 10), draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)))
     2182 + testcanvas.MustApply(cvs, ft)
     2183 + return ft
     2184 + },
     2185 + },
     2186 + {
     2187 + desc: "newly placed widget gets keyboard events",
     2188 + termSize: image.Point{10, 10},
     2189 + container: func(ft *faketerm.Terminal) (*Container, error) {
     2190 + return New(
     2191 + ft,
     2192 + ID("myRoot"),
     2193 + PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
     2194 + )
     2195 + },
     2196 + beforeEvents: []terminalapi.Event{
     2197 + // Move focus to the target container.
     2198 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
     2199 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease},
     2200 + },
     2201 + afterEvents: []terminalapi.Event{
     2202 + // Send the keyboard event.
     2203 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
     2204 + },
     2205 + updateID: "myRoot",
     2206 + updateOpts: []Option{
     2207 + PlaceWidget(fakewidget.New(widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused})),
     2208 + },
     2209 + want: func(size image.Point) *faketerm.Terminal {
     2210 + ft := faketerm.MustNew(size)
     2211 + cvs := testcanvas.MustNew(ft.Area())
     2212 + fakewidget.MustDraw(
     2213 + ft,
     2214 + cvs,
     2215 + widgetapi.Options{WantKeyboard: widgetapi.KeyScopeFocused},
     2216 + &terminalapi.Keyboard{Key: keyboard.KeyEnter},
     2217 + )
     2218 + testcanvas.MustApply(cvs, ft)
     2219 + return ft
     2220 + },
     2221 + },
     2222 + {
     2223 + desc: "newly placed widget gets mouse events",
     2224 + termSize: image.Point{20, 10},
     2225 + container: func(ft *faketerm.Terminal) (*Container, error) {
     2226 + return New(
     2227 + ft,
     2228 + ID("myRoot"),
     2229 + PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
     2230 + )
     2231 + },
     2232 + afterEvents: []terminalapi.Event{
     2233 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
     2234 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease},
     2235 + },
     2236 + updateID: "myRoot",
     2237 + updateOpts: []Option{
     2238 + PlaceWidget(fakewidget.New(widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget})),
     2239 + },
     2240 + want: func(size image.Point) *faketerm.Terminal {
     2241 + ft := faketerm.MustNew(size)
     2242 + cvs := testcanvas.MustNew(ft.Area())
     2243 + fakewidget.MustDraw(
     2244 + ft,
     2245 + cvs,
     2246 + widgetapi.Options{WantMouse: widgetapi.MouseScopeWidget},
     2247 + &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonRelease},
     2248 + )
     2249 + testcanvas.MustApply(cvs, ft)
     2250 + return ft
     2251 + },
     2252 + },
     2253 + }
     2254 + 
     2255 + for _, tc := range tests {
     2256 + t.Run(tc.desc, func(t *testing.T) {
     2257 + got, err := faketerm.New(tc.termSize)
     2258 + if err != nil {
     2259 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     2260 + }
     2261 + 
     2262 + cont, err := tc.container(got)
     2263 + if err != nil {
     2264 + t.Fatalf("tc.container => unexpected error: %v", err)
     2265 + }
     2266 + 
     2267 + eds := event.NewDistributionSystem()
     2268 + eh := &errorHandler{}
     2269 + // Subscribe to receive errors.
     2270 + eds.Subscribe([]terminalapi.Event{terminalapi.NewError("")}, func(ev terminalapi.Event) {
     2271 + eh.handle(ev.(*terminalapi.Error).Error())
     2272 + })
     2273 + cont.Subscribe(eds)
     2274 + // Initial draw to determine sizes of containers.
     2275 + if err := cont.Draw(); err != nil {
     2276 + t.Fatalf("Draw => unexpected error: %v", err)
     2277 + }
     2278 + 
     2279 + // Deliver the before events.
     2280 + for _, ev := range tc.beforeEvents {
     2281 + eds.Event(ev)
     2282 + }
     2283 + if err := testevent.WaitFor(5*time.Second, func() error {
     2284 + if got, want := eds.Processed(), len(tc.beforeEvents); got != want {
     2285 + return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
     2286 + }
     2287 + return nil
     2288 + }); err != nil {
     2289 + t.Fatalf("testevent.WaitFor => %v", err)
     2290 + }
     2291 + 
     2292 + {
     2293 + err := cont.Update(tc.updateID, tc.updateOpts...)
     2294 + if (err != nil) != tc.wantUpdateErr {
     2295 + t.Errorf("Update => unexpected error:%v, wantErr:%v", err, tc.wantUpdateErr)
     2296 + }
     2297 + if err != nil {
     2298 + return
     2299 + }
     2300 + }
     2301 + 
     2302 + // Deliver the after events.
     2303 + for _, ev := range tc.afterEvents {
     2304 + eds.Event(ev)
     2305 + }
     2306 + wantEv := len(tc.beforeEvents) + len(tc.afterEvents)
     2307 + if err := testevent.WaitFor(5*time.Second, func() error {
     2308 + if got, want := eds.Processed(), wantEv; got != want {
     2309 + return fmt.Errorf("the event distribution system processed %d events, want %d", got, want)
     2310 + }
     2311 + return nil
     2312 + }); err != nil {
     2313 + t.Fatalf("testevent.WaitFor => %v", err)
     2314 + }
     2315 + 
     2316 + if err := cont.Draw(); err != nil {
     2317 + t.Fatalf("Draw => unexpected error: %v", err)
     2318 + }
     2319 + 
     2320 + var want *faketerm.Terminal
     2321 + if tc.want != nil {
     2322 + want = tc.want(tc.termSize)
     2323 + } else {
     2324 + w, err := faketerm.New(tc.termSize)
     2325 + if err != nil {
     2326 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     2327 + }
     2328 + want = w
     2329 + }
     2330 + if diff := faketerm.Diff(want, got); diff != "" {
     2331 + t.Errorf("Draw => %v", diff)
     2332 + }
     2333 + })
     2334 + }
     2335 + 
     2336 +}
     2337 + 
  • ■ ■ ■ ■ ■
    container/focus.go
    skipped 72 lines
    73 73   return ft.container == c
    74 74  }
    75 75   
    76  -// active returns the currently focused container.
    77  -func (ft *focusTracker) active() *Container {
    78  - return ft.container
     76 +// setActive sets the currently active container to the one provided.
     77 +func (ft *focusTracker) setActive(c *Container) {
     78 + ft.container = c
    79 79  }
    80 80   
    81 81  // mouse identifies mouse events that change the focused container and track
    skipped 17 lines
    99 99   ft.buttonFSM.UpdateArea(ar)
    100 100  }
    101 101   
     102 +// reachableFrom asserts whether the currently focused container is reachable
     103 +// from the provided node in the tree.
     104 +func (ft *focusTracker) reachableFrom(node *Container) bool {
     105 + var (
     106 + errStr string
     107 + reachable bool
     108 + )
     109 + preOrder(node, &errStr, visitFunc(func(c *Container) error {
     110 + if c == ft.container {
     111 + reachable = true
     112 + }
     113 + return nil
     114 + }))
     115 + return reachable
     116 +}
     117 + 
  • ■ ■ ■ ■ ■ ■
    container/focus_test.go
    skipped 236 lines
    237 237   if err != nil {
    238 238   t.Fatalf("tc.container => unexpected error: %v", err)
    239 239   }
     240 + // Initial draw to determine sizes of containers.
     241 + if err := cont.Draw(); err != nil {
     242 + t.Fatalf("Draw => unexpected error: %v", err)
     243 + }
    240 244   for _, pc := range tc.cases {
    241 245   gotCont := pointCont(cont, pc.point)
    242 246   if (gotCont == nil) != pc.wantNil {
    skipped 167 lines
    410 414   
    411 415   eds := event.NewDistributionSystem()
    412 416   root.Subscribe(eds)
     417 + // Initial draw to determine sizes of containers.
     418 + if err := root.Draw(); err != nil {
     419 + t.Fatalf("Draw => unexpected error: %v", err)
     420 + }
    413 421   for _, ev := range tc.events {
    414 422   eds.Event(ev)
    415 423   }
    skipped 33 lines
  • ■ ■ ■ ■ ■ ■
    container/options.go
    skipped 16 lines
    17 17  // options.go defines container options.
    18 18   
    19 19  import (
     20 + "errors"
    20 21   "fmt"
    21 22   "image"
    22 23   
    skipped 4 lines
    27 28   "github.com/mum4k/termdash/widgetapi"
    28 29  )
    29 30   
    30  -// applyOptions applies the options to the container.
     31 +// applyOptions applies the options to the container and validates them.
    31 32  func applyOptions(c *Container, opts ...Option) error {
    32 33   for _, opt := range opts {
    33 34   if err := opt.set(c); err != nil {
    skipped 3 lines
    37 38   return nil
    38 39  }
    39 40   
     41 +// validateOptions validates options set in the container tree.
     42 +func validateOptions(c *Container) error {
     43 + // ensure all the container identifiers are either empty or unique.
     44 + var errStr string
     45 + seenID := map[string]bool{}
     46 + preOrder(c, &errStr, func(c *Container) error {
     47 + if c.opts.id == "" {
     48 + return nil
     49 + }
     50 + 
     51 + if seenID[c.opts.id] {
     52 + return fmt.Errorf("duplicate container ID %q", c.opts.id)
     53 + }
     54 + seenID[c.opts.id] = true
     55 + return nil
     56 + })
     57 + if errStr != "" {
     58 + return errors.New(errStr)
     59 + }
     60 + return nil
     61 +}
     62 + 
    40 63  // Option is used to provide options to a container.
    41 64  type Option interface {
    42 65   // set sets the provided option.
    skipped 2 lines
    45 68   
    46 69  // options stores the options provided to the container.
    47 70  type options struct {
     71 + // id is the identifier provided by the user.
     72 + id string
     73 + 
    48 74   // inherited are options that are inherited by child containers.
    49 75   inherited inherited
    50 76   
    skipped 177 lines
    228 254   }
    229 255   
    230 256   return c.createSecond(b.bOpts())
     257 + })
     258 +}
     259 + 
     260 +// ID sets an identifier for this container.
     261 +// This ID can be later used to perform dynamic layout changes by passing new
     262 +// options to this container. When provided, it must be a non-empty string that
     263 +// is unique among all the containers.
     264 +func ID(id string) Option {
     265 + return option(func(c *Container) error {
     266 + if id == "" {
     267 + return errors.New("the ID cannot be an empty string")
     268 + }
     269 + c.opts.id = id
     270 + return nil
     271 + })
     272 +}
     273 + 
     274 +// Clear clears this container.
     275 +// If the container contains a widget, the widget is removed.
     276 +// If the container had any sub containers or splits, they are removed.
     277 +func Clear() Option {
     278 + return option(func(c *Container) error {
     279 + c.opts.widget = nil
     280 + c.first = nil
     281 + c.second = nil
     282 + return nil
    231 283   })
    232 284  }
    233 285   
    skipped 485 lines
  • ■ ■ ■ ■ ■ ■
    container/traversal.go
    skipped 13 lines
    14 14   
    15 15  package container
    16 16   
     17 +import (
     18 + "errors"
     19 + "fmt"
     20 +)
     21 + 
    17 22  // traversal.go provides functions that navigate the container tree.
    18 23   
    19 24  // rootCont returns the root container.
    skipped 37 lines
    57 62   }
    58 63  }
    59 64   
     65 +// findID finds container with the provided ID.
     66 +// Returns an error of there is no container with the specified ID.
     67 +func findID(root *Container, id string) (*Container, error) {
     68 + if id == "" {
     69 + return nil, errors.New("the container ID must not be empty")
     70 + }
     71 + 
     72 + var (
     73 + errStr string
     74 + cont *Container
     75 + )
     76 + preOrder(root, &errStr, visitFunc(func(c *Container) error {
     77 + if c.opts.id == id {
     78 + cont = c
     79 + }
     80 + return nil
     81 + }))
     82 + if cont == nil {
     83 + return nil, fmt.Errorf("cannot find container with ID %q", id)
     84 + }
     85 + return cont, nil
     86 +}
     87 + 
  • ■ ■ ■ ■ ■ ■
    container/traversal_test.go
    skipped 165 lines
    166 166   }
    167 167  }
    168 168   
     169 +func TestFindID(t *testing.T) {
     170 + tests := []struct {
     171 + desc string
     172 + container func(ft *faketerm.Terminal) (*Container, error)
     173 + id string
     174 + wantFound bool
     175 + wantErr bool
     176 + }{
     177 + {
     178 + desc: "fails when searching with empty ID",
     179 + container: func(ft *faketerm.Terminal) (*Container, error) {
     180 + return New(ft)
     181 + },
     182 + wantErr: true,
     183 + },
     184 + {
     185 + desc: "no container with the specified ID",
     186 + container: func(ft *faketerm.Terminal) (*Container, error) {
     187 + return New(ft)
     188 + },
     189 + id: "mycont",
     190 + wantErr: true,
     191 + },
     192 + {
     193 + desc: "finds the container",
     194 + container: func(ft *faketerm.Terminal) (*Container, error) {
     195 + return New(ft, ID("mycont"))
     196 + },
     197 + id: "mycont",
     198 + wantFound: true,
     199 + },
     200 + }
     201 + 
     202 + for _, tc := range tests {
     203 + t.Run(tc.desc, func(t *testing.T) {
     204 + ft, err := faketerm.New(image.Point{10, 10})
     205 + if err != nil {
     206 + t.Fatalf("faketerm.New => unexpected error: %v", err)
     207 + }
     208 + 
     209 + cont, err := tc.container(ft)
     210 + if err != nil {
     211 + t.Fatalf("tc.container => unexpected error: %v", err)
     212 + }
     213 + 
     214 + got, err := findID(cont, tc.id)
     215 + if (err != nil) != tc.wantErr {
     216 + t.Errorf("findID => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     217 + }
     218 + if err != nil {
     219 + return
     220 + }
     221 + 
     222 + if (got != nil) != tc.wantFound {
     223 + t.Errorf("findID returned %v, wantFound: %v", got, tc.wantFound)
     224 + }
     225 + })
     226 + }
     227 +}
     228 + 
  • doc/images/termdashdemo_0_8_0.gif
  • ■ ■ ■ ■ ■ ■
    termdash.go
    skipped 322 lines
    323 323  // start starts the terminal dashboard. Blocks until the context expires or
    324 324  // until stop() is called.
    325 325  func (td *termdash) start(ctx context.Context) error {
     326 + // Redraw once to initialize the container sizes.
     327 + if err := td.periodicRedraw(); err != nil {
     328 + close(td.exitCh)
     329 + return err
     330 + }
     331 + 
    326 332   redrawTimer := time.NewTicker(td.redrawInterval)
    327 333   defer redrawTimer.Stop()
    328 334   
    skipped 29 lines
  • ■ ■ ■ ■ ■ ■
    termdash_test.go
    skipped 240 lines
    241 241   events: []terminalapi.Event{
    242 242   &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonLeft},
    243 243   },
    244  - wantProcessed: 3,
     244 + wantProcessed: 2,
    245 245   want: func(size image.Point) *faketerm.Terminal {
    246 246   ft := faketerm.MustNew(size)
    247 247   
    skipped 111 lines
    359 359   events: []terminalapi.Event{
    360 360   &terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonWheelUp},
    361 361   },
    362  - wantProcessed: 4,
     362 + wantProcessed: 3,
    363 363   after: func(eh *eventHandlers) error {
    364 364   want := terminalapi.Mouse{Position: image.Point{0, 0}, Button: mouse.ButtonWheelUp}
    365 365   if diff := pretty.Compare(want, eh.mouseSub.get()); diff != "" {
    skipped 288 lines
  • ■ ■ ■ ■ ■ ■
    termdashdemo/termdashdemo.go
    skipped 44 lines
    45 45  // redrawInterval is how often termdash redraws the screen.
    46 46  const redrawInterval = 250 * time.Millisecond
    47 47   
    48  -// gridLayout prepares the screen layout by creating the container and placing
    49  -// widgets.
    50  -// This function demonstrates the use of the grid builder.
    51  -// gridLayout() and contLayout() demonstrate the two available layout APIs and
    52  -// both produce equivalent layouts.
    53  -func gridLayout(ctx context.Context, t terminalapi.Terminal) (*container.Container, error) {
     48 +// widgets holds the widgets used by this demo.
     49 +type widgets struct {
     50 + segDist *segmentdisplay.SegmentDisplay
     51 + rollT *text.Text
     52 + spGreen *sparkline.SparkLine
     53 + spRed *sparkline.SparkLine
     54 + gauge *gauge.Gauge
     55 + heartLC *linechart.LineChart
     56 + barChart *barchart.BarChart
     57 + donut *donut.Donut
     58 + leftB *button.Button
     59 + rightB *button.Button
     60 + sineLC *linechart.LineChart
     61 + 
     62 + buttons *layoutButtons
     63 +}
     64 + 
     65 +// newWidgets creates all widgets used by this demo.
     66 +func newWidgets(ctx context.Context, c *container.Container) (*widgets, error) {
    54 67   sd, err := newSegmentDisplay(ctx)
    55 68   if err != nil {
    56 69   return nil, err
    skipped 15 lines
    72 85   if err != nil {
    73 86   return nil, err
    74 87   }
     88 + 
    75 89   bc, err := newBarChart(ctx)
    76 90   if err != nil {
    77 91   return nil, err
    skipped 8 lines
    86 100   if err != nil {
    87 101   return nil, err
    88 102   }
     103 + return &widgets{
     104 + segDist: sd,
     105 + rollT: rollT,
     106 + spGreen: spGreen,
     107 + spRed: spRed,
     108 + gauge: g,
     109 + heartLC: heartLC,
     110 + barChart: bc,
     111 + donut: don,
     112 + leftB: leftB,
     113 + rightB: rightB,
     114 + sineLC: sineLC,
     115 + }, nil
     116 +}
    89 117   
    90  - builder := grid.New()
    91  - builder.Add(
    92  - grid.ColWidthPerc(70,
    93  - grid.RowHeightPerc(25,
    94  - grid.Widget(sd,
    95  - container.Border(linestyle.Light),
    96  - container.BorderTitle("Press Q to quit"),
    97  - ),
     118 +// layoutType represents the possible layouts the buttons switch between.
     119 +type layoutType int
     120 + 
     121 +const (
     122 + // layoutAll displays all the widgets.
     123 + layoutAll layoutType = iota
     124 + // layoutText focuses onto the text widget.
     125 + layoutText
     126 + // layoutSparkLines focuses onto the sparklines.
     127 + layoutSparkLines
     128 + // layoutLineChart focuses onto the linechart.
     129 + layoutLineChart
     130 +)
     131 + 
     132 +// gridLayout prepares container options that represent the desired screen layout.
     133 +// This function demonstrates the use of the grid builder.
     134 +// gridLayout() and contLayout() demonstrate the two available layout APIs and
     135 +// both produce equivalent layouts for layoutType layoutAll.
     136 +func gridLayout(w *widgets, lt layoutType) ([]container.Option, error) {
     137 + leftRows := []grid.Element{
     138 + grid.RowHeightPerc(25,
     139 + grid.Widget(w.segDist,
     140 + container.Border(linestyle.Light),
     141 + container.BorderTitle("Press Q to quit"),
     142 + ),
     143 + ),
     144 + grid.RowHeightPerc(5,
     145 + grid.ColWidthPerc(25,
     146 + grid.Widget(w.buttons.allB),
     147 + ),
     148 + grid.ColWidthPerc(25,
     149 + grid.Widget(w.buttons.textB),
     150 + ),
     151 + grid.ColWidthPerc(25,
     152 + grid.Widget(w.buttons.spB),
     153 + ),
     154 + grid.ColWidthPerc(25,
     155 + grid.Widget(w.buttons.lcB),
    98 156   ),
    99  - grid.RowHeightPerc(26,
     157 + ),
     158 + }
     159 + switch lt {
     160 + case layoutAll:
     161 + leftRows = append(leftRows,
     162 + grid.RowHeightPerc(23,
    100 163   grid.ColWidthPerc(50,
    101  - grid.Widget(rollT,
     164 + grid.Widget(w.rollT,
    102 165   container.Border(linestyle.Light),
    103 166   container.BorderTitle("A rolling text"),
    104 167   ),
    105 168   ),
    106 169   grid.RowHeightPerc(50,
    107  - grid.Widget(spGreen,
     170 + grid.Widget(w.spGreen,
    108 171   container.Border(linestyle.Light),
    109 172   container.BorderTitle("Green SparkLine"),
    110 173   ),
    111 174   ),
    112 175   grid.RowHeightPerc(50,
    113  - grid.Widget(spRed,
     176 + grid.Widget(w.spRed,
    114 177   container.Border(linestyle.Light),
    115 178   container.BorderTitle("Red SparkLine"),
    116 179   ),
    117 180   ),
    118 181   ),
    119  - grid.RowHeightPerc(10,
    120  - grid.Widget(g,
     182 + grid.RowHeightPerc(7,
     183 + grid.Widget(w.gauge,
    121 184   container.Border(linestyle.Light),
    122 185   container.BorderTitle("A Gauge"),
    123 186   container.BorderColor(cell.ColorNumber(39)),
    124 187   ),
    125 188   ),
    126  - grid.RowHeightPerc(39,
    127  - grid.Widget(heartLC,
     189 + grid.RowHeightPerc(35,
     190 + grid.Widget(w.heartLC,
    128 191   container.Border(linestyle.Light),
    129 192   container.BorderTitle("A LineChart"),
    130 193   ),
    131 194   ),
    132  - ),
     195 + )
     196 + case layoutText:
     197 + leftRows = append(leftRows,
     198 + grid.RowHeightPerc(65,
     199 + grid.Widget(w.rollT,
     200 + container.Border(linestyle.Light),
     201 + container.BorderTitle("A rolling text"),
     202 + ),
     203 + ),
     204 + )
     205 + 
     206 + case layoutSparkLines:
     207 + leftRows = append(leftRows,
     208 + grid.RowHeightPerc(32,
     209 + grid.Widget(w.spGreen,
     210 + container.Border(linestyle.Light),
     211 + container.BorderTitle("Green SparkLine"),
     212 + ),
     213 + ),
     214 + grid.RowHeightPerc(33,
     215 + grid.Widget(w.spRed,
     216 + container.Border(linestyle.Light),
     217 + container.BorderTitle("Red SparkLine"),
     218 + ),
     219 + ),
     220 + )
     221 + 
     222 + case layoutLineChart:
     223 + leftRows = append(leftRows,
     224 + grid.RowHeightPerc(65,
     225 + grid.Widget(w.heartLC,
     226 + container.Border(linestyle.Light),
     227 + container.BorderTitle("A LineChart"),
     228 + ),
     229 + ),
     230 + )
     231 + }
     232 + 
     233 + builder := grid.New()
     234 + builder.Add(
     235 + grid.ColWidthPerc(70, leftRows...),
    133 236   )
     237 + 
    134 238   builder.Add(
    135 239   grid.ColWidthPerc(30,
    136 240   grid.RowHeightPerc(30,
    137  - grid.Widget(bc,
     241 + grid.Widget(w.barChart,
    138 242   container.Border(linestyle.Light),
    139 243   container.BorderTitle("BarChart"),
    140 244   container.BorderTitleAlignRight(),
    141 245   ),
    142 246   ),
    143 247   grid.RowHeightPerc(21,
    144  - grid.Widget(don,
     248 + grid.Widget(w.donut,
    145 249   container.Border(linestyle.Light),
    146 250   container.BorderTitle("A Donut"),
    147 251   container.BorderTitleAlignRight(),
    148 252   ),
    149 253   ),
    150 254   grid.RowHeightPerc(40,
    151  - grid.Widget(sineLC,
     255 + grid.Widget(w.sineLC,
    152 256   container.Border(linestyle.Light),
    153 257   container.BorderTitle("Multiple series"),
    154 258   container.BorderTitleAlignRight(),
    skipped 1 lines
    156 260   ),
    157 261   grid.RowHeightPerc(9,
    158 262   grid.ColWidthPerc(50,
    159  - grid.Widget(leftB,
     263 + grid.Widget(w.leftB,
    160 264   container.AlignHorizontal(align.HorizontalRight),
    161 265   container.PaddingRight(1),
    162 266   ),
    163 267   ),
    164 268   grid.ColWidthPerc(50,
    165  - grid.Widget(rightB,
     269 + grid.Widget(w.rightB,
    166 270   container.AlignHorizontal(align.HorizontalLeft),
    167 271   container.PaddingLeft(1),
    168 272   ),
    skipped 6 lines
    175 279   if err != nil {
    176 280   return nil, err
    177 281   }
    178  - c, err := container.New(t, gridOpts...)
    179  - if err != nil {
    180  - return nil, err
    181  - }
    182  - return c, nil
     282 + return gridOpts, nil
    183 283  }
    184 284   
    185  -// contLayout prepares the screen layout by creating the container and placing
    186  -// widgets.
     285 +// contLayout prepares container options that represent the desired screen layout.
    187 286  // This function demonstrates the direct use of the container API.
    188 287  // gridLayout() and contLayout() demonstrate the two available layout APIs and
    189  -// both produce equivalent layouts.
    190  -func contLayout(ctx context.Context, t terminalapi.Terminal) (*container.Container, error) {
    191  - sd, err := newSegmentDisplay(ctx)
    192  - if err != nil {
    193  - return nil, err
    194  - }
    195  - rollT, err := newRollText(ctx)
    196  - if err != nil {
    197  - return nil, err
    198  - }
    199  - spGreen, spRed, err := newSparkLines(ctx)
    200  - if err != nil {
    201  - return nil, err
    202  - }
    203  - g, err := newGauge(ctx)
    204  - if err != nil {
    205  - return nil, err
    206  - }
    207  - 
    208  - heartLC, err := newHeartbeat(ctx)
    209  - if err != nil {
    210  - return nil, err
    211  - }
    212  - bc, err := newBarChart(ctx)
    213  - if err != nil {
    214  - return nil, err
    215  - }
    216  - 
    217  - don, err := newDonut(ctx)
    218  - if err != nil {
    219  - return nil, err
    220  - }
    221  - 
    222  - leftB, rightB, sineLC, err := newSines(ctx)
    223  - if err != nil {
    224  - return nil, err
     288 +// both produce equivalent layouts for layoutType layoutAll.
     289 +// contLayout only produces layoutAll.
     290 +func contLayout(w *widgets) ([]container.Option, error) {
     291 + buttonRow := []container.Option{
     292 + container.SplitVertical(
     293 + container.Left(
     294 + container.SplitVertical(
     295 + container.Left(
     296 + container.PlaceWidget(w.buttons.allB),
     297 + ),
     298 + container.Right(
     299 + container.PlaceWidget(w.buttons.textB),
     300 + ),
     301 + ),
     302 + ),
     303 + container.Right(
     304 + container.SplitVertical(
     305 + container.Left(
     306 + container.PlaceWidget(w.buttons.spB),
     307 + ),
     308 + container.Right(
     309 + container.PlaceWidget(w.buttons.lcB),
     310 + ),
     311 + ),
     312 + ),
     313 + ),
    225 314   }
    226 315   
    227 316   segmentTextSpark := []container.Option{
    skipped 1 lines
    229 318   container.Top(
    230 319   container.Border(linestyle.Light),
    231 320   container.BorderTitle("Press Q to quit"),
    232  - container.PlaceWidget(sd),
     321 + container.PlaceWidget(w.segDist),
    233 322   ),
    234 323   container.Bottom(
    235  - container.SplitVertical(
    236  - container.Left(
    237  - container.Border(linestyle.Light),
    238  - container.BorderTitle("A rolling text"),
    239  - container.PlaceWidget(rollT),
    240  - ),
    241  - container.Right(
    242  - container.SplitHorizontal(
    243  - container.Top(
     324 + container.SplitHorizontal(
     325 + container.Top(buttonRow...),
     326 + container.Bottom(
     327 + container.SplitVertical(
     328 + container.Left(
    244 329   container.Border(linestyle.Light),
    245  - container.BorderTitle("Green SparkLine"),
    246  - container.PlaceWidget(spGreen),
     330 + container.BorderTitle("A rolling text"),
     331 + container.PlaceWidget(w.rollT),
    247 332   ),
    248  - container.Bottom(
    249  - container.Border(linestyle.Light),
    250  - container.BorderTitle("Red SparkLine"),
    251  - container.PlaceWidget(spRed),
     333 + container.Right(
     334 + container.SplitHorizontal(
     335 + container.Top(
     336 + container.Border(linestyle.Light),
     337 + container.BorderTitle("Green SparkLine"),
     338 + container.PlaceWidget(w.spGreen),
     339 + ),
     340 + container.Bottom(
     341 + container.Border(linestyle.Light),
     342 + container.BorderTitle("Red SparkLine"),
     343 + container.PlaceWidget(w.spRed),
     344 + ),
     345 + ),
    252 346   ),
    253 347   ),
    254 348   ),
     349 + container.SplitPercent(20),
    255 350   ),
    256 351   ),
    257 352   container.SplitPercent(50),
    skipped 6 lines
    264 359   container.Border(linestyle.Light),
    265 360   container.BorderTitle("A Gauge"),
    266 361   container.BorderColor(cell.ColorNumber(39)),
    267  - container.PlaceWidget(g),
     362 + container.PlaceWidget(w.gauge),
    268 363   ),
    269 364   container.Bottom(
    270 365   container.Border(linestyle.Light),
    271 366   container.BorderTitle("A LineChart"),
    272  - container.PlaceWidget(heartLC),
     367 + container.PlaceWidget(w.heartLC),
    273 368   ),
    274 369   container.SplitPercent(20),
    275 370   ),
    skipped 13 lines
    289 384   container.Border(linestyle.Light),
    290 385   container.BorderTitle("Multiple series"),
    291 386   container.BorderTitleAlignRight(),
    292  - container.PlaceWidget(sineLC),
     387 + container.PlaceWidget(w.sineLC),
    293 388   ),
    294 389   container.Bottom(
    295 390   container.SplitVertical(
    296 391   container.Left(
    297  - container.PlaceWidget(leftB),
     392 + container.PlaceWidget(w.leftB),
    298 393   container.AlignHorizontal(align.HorizontalRight),
    299 394   container.PaddingRight(1),
    300 395   ),
    301 396   container.Right(
    302  - container.PlaceWidget(rightB),
     397 + container.PlaceWidget(w.rightB),
    303 398   container.AlignHorizontal(align.HorizontalLeft),
    304 399   container.PaddingLeft(1),
    305 400   ),
    skipped 8 lines
    314 409   container.Top(
    315 410   container.Border(linestyle.Light),
    316 411   container.BorderTitle("BarChart"),
    317  - container.PlaceWidget(bc),
     412 + container.PlaceWidget(w.barChart),
    318 413   container.BorderTitleAlignRight(),
    319 414   ),
    320 415   container.Bottom(
    skipped 2 lines
    323 418   container.Border(linestyle.Light),
    324 419   container.BorderTitle("A Donut"),
    325 420   container.BorderTitleAlignRight(),
    326  - container.PlaceWidget(don),
     421 + container.PlaceWidget(w.donut),
    327 422   ),
    328 423   container.Bottom(lcAndButtons...),
    329 424   container.SplitPercent(30),
    skipped 3 lines
    333 428   ),
    334 429   }
    335 430   
    336  - c, err := container.New(
    337  - t,
     431 + return []container.Option{
    338 432   container.SplitVertical(
    339 433   container.Left(leftSide...),
    340 434   container.Right(rightSide...),
    341 435   container.SplitPercent(70),
    342 436   ),
    343  - )
    344  - if err != nil {
    345  - return nil, err
    346  - }
    347  - return c, nil
     437 + }, nil
    348 438  }
    349 439   
     440 +// rootID is the ID assigned to the root container.
     441 +const rootID = "root"
     442 + 
    350 443  func main() {
    351 444   t, err := termbox.New(termbox.ColorMode(terminalapi.ColorMode256))
    352 445   if err != nil {
    skipped 1 lines
    354 447   }
    355 448   defer t.Close()
    356 449   
     450 + c, err := container.New(t, container.ID(rootID))
     451 + if err != nil {
     452 + panic(err)
     453 + }
     454 + 
    357 455   ctx, cancel := context.WithCancel(context.Background())
     456 + w, err := newWidgets(ctx, c)
     457 + if err != nil {
     458 + panic(err)
     459 + }
     460 + lb, err := newLayoutButtons(c, w)
     461 + if err != nil {
     462 + panic(err)
     463 + }
     464 + w.buttons = lb
    358 465   
    359  - c, err := gridLayout(ctx, t) // equivalent to contLayout(ctx, t)
     466 + //gridOpts, err := gridLayout(w, layoutAll) // equivalent to contLayout(w)
     467 + gridOpts, err := contLayout(w) // equivalent to contLayout(w)
    360 468   if err != nil {
     469 + panic(err)
     470 + }
     471 + 
     472 + if err := c.Update(rootID, gridOpts...); err != nil {
    361 473   panic(err)
    362 474   }
    363 475   
    skipped 308 lines
    672 784   return nil, nil, nil, err
    673 785   }
    674 786   return leftB, rightB, sineLc, nil
     787 +}
     788 + 
     789 +// setLayout sets the specified layout.
     790 +func setLayout(c *container.Container, w *widgets, lt layoutType) error {
     791 + gridOpts, err := gridLayout(w, lt)
     792 + if err != nil {
     793 + return err
     794 + }
     795 + return c.Update(rootID, gridOpts...)
     796 +}
     797 + 
     798 +// layoutButtons are buttons that change the layout.
     799 +type layoutButtons struct {
     800 + allB *button.Button
     801 + textB *button.Button
     802 + spB *button.Button
     803 + lcB *button.Button
     804 +}
     805 + 
     806 +// newLayoutButtons returns buttons that dynamically switch the layouts.
     807 +func newLayoutButtons(c *container.Container, w *widgets) (*layoutButtons, error) {
     808 + opts := []button.Option{
     809 + button.WidthFor("sparklines"),
     810 + button.FillColor(cell.ColorNumber(220)),
     811 + button.Height(1),
     812 + }
     813 + 
     814 + allB, err := button.New("all", func() error {
     815 + return setLayout(c, w, layoutAll)
     816 + }, opts...)
     817 + if err != nil {
     818 + return nil, err
     819 + }
     820 + 
     821 + textB, err := button.New("text", func() error {
     822 + return setLayout(c, w, layoutText)
     823 + }, opts...)
     824 + if err != nil {
     825 + return nil, err
     826 + }
     827 + 
     828 + spB, err := button.New("sparklines", func() error {
     829 + return setLayout(c, w, layoutSparkLines)
     830 + }, opts...)
     831 + if err != nil {
     832 + return nil, err
     833 + }
     834 + 
     835 + lcB, err := button.New("linechart", func() error {
     836 + return setLayout(c, w, layoutLineChart)
     837 + }, opts...)
     838 + if err != nil {
     839 + return nil, err
     840 + }
     841 + 
     842 + return &layoutButtons{
     843 + allB: allB,
     844 + textB: textB,
     845 + spB: spB,
     846 + lcB: lcB,
     847 + }, nil
    675 848  }
    676 849   
    677 850  // rotateFloats returns a new slice with inputs rotated by step.
    skipped 17 lines
Please wait...
Page is in error, reload to recover