Skip to content

Commit

Permalink
Merge 6a2d2bb into 3cfeb8a
Browse files Browse the repository at this point in the history
  • Loading branch information
mum4k committed Apr 3, 2021
2 parents 3cfeb8a + 6a2d2bb commit 112c195
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 3 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- The `Text` widget has a new option `MaxTextCells` which can be used to limit
the maximum number of cells the widget keeps in memory.

### Changed

- Bump github.com/mattn/go-runewidth from 0.0.10 to 0.0.12.
Expand Down
2 changes: 1 addition & 1 deletion private/runewidth/runewidth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestRuneWidth(t *testing.T) {
},
{
desc: "non-printable characters from mattn/runewidth/runewidth_test",
runes: []rune{'\x00', '\x01', '\u0300', '\u2028', '\u2029'},
runes: []rune{'\x00', '\x01', '\u0300', '\u2028', '\u2029', '\n'},
want: 0,
},
{
Expand Down
25 changes: 25 additions & 0 deletions widgets/text/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type options struct {
scrollDown rune
wrapMode wrap.Mode
rollContent bool
maxTextCells int
disableScrolling bool
mouseUpButton mouse.Button
mouseDownButton mouse.Button
Expand All @@ -56,6 +57,7 @@ func newOptions(opts ...Option) *options {
keyDown: DefaultScrollKeyDown,
keyPgUp: DefaultScrollKeyPageUp,
keyPgDown: DefaultScrollKeyPageDown,
maxTextCells: DefaultMaxTextCells,
}
for _, o := range opts {
o.set(opt)
Expand All @@ -77,6 +79,9 @@ func (o *options) validate() error {
if o.mouseUpButton == o.mouseDownButton {
return fmt.Errorf("invalid ScrollMouseButtons(up:%v, down:%v), the buttons must be unique", o.mouseUpButton, o.mouseDownButton)
}
if o.maxTextCells < 0 {
return fmt.Errorf("invalid MaxTextCells(%d), must be zero or a positive integer", o.maxTextCells)
}
return nil
}

Expand Down Expand Up @@ -174,3 +179,23 @@ func ScrollKeys(up, down, pageUp, pageDown keyboard.Key) Option {
opts.keyPgDown = pageDown
})
}

// The default value for the MaxTextCells option.
// Use zero as no limit, for logs you may wish to try 10,000 or higher.
const (
DefaultMaxTextCells = 0
)

// MaxTextCells limits the text content to this number of terminal cells.
// This is useful when sending large amounts of text to the Text widget, e.g.
// when tailing logs as it will limit the memory usage.
// When the newly added content goes over this number of cells, the Text widget
// behaves as a circular buffer and drops earlier content to accommodate the
// new one.
// Note the count is in cells, not runes, some wide runes can take multiple
// terminal cells.
func MaxTextCells(max int) Option {
return option(func(opts *options) {
opts.maxTextCells = max
})
}
68 changes: 67 additions & 1 deletion widgets/text/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ package text
import (
"fmt"
"image"
"strings"
"sync"

"github.com/mum4k/termdash/private/canvas"
"github.com/mum4k/termdash/private/canvas/buffer"
"github.com/mum4k/termdash/private/runewidth"
"github.com/mum4k/termdash/private/wrap"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
Expand Down Expand Up @@ -90,6 +92,16 @@ func (t *Text) reset() {
t.contentChanged = true
}

// contentCells calculates the number of cells the content takes to display on
// terminal.
func (t *Text) contentCells() int {
cells := 0
for _, c := range t.content {
cells += runeWidth(c.Rune)
}
return cells
}

// Write writes text for the widget to display. Multiple calls append
// additional text. The text contain cannot control characters
// (unicode.IsControl) or space character (unicode.IsSpace) other than:
Expand All @@ -108,7 +120,17 @@ func (t *Text) Write(text string, wOpts ...WriteOption) error {
if opts.replace {
t.reset()
}
for _, r := range text {

truncated := truncateToCells(text, t.opts.maxTextCells)
textCells := stringWidth(truncated)
contentCells := t.contentCells()
// If MaxTextCells has been set, limit the content if needed.
if t.opts.maxTextCells > 0 && contentCells+textCells > t.opts.maxTextCells {
diff := contentCells + textCells - t.opts.maxTextCells
t.content = t.content[diff:]
}

for _, r := range truncated {
t.content = append(t.content, buffer.NewCell(r, opts.cellOpts))
}
t.contentChanged = true
Expand Down Expand Up @@ -284,3 +306,47 @@ func (t *Text) Options() widgetapi.Options {
WantKeyboard: ks,
}
}

// runeWidth is line runewidth.RuneWidth, but assumes newline characters take 1
// cell.
func runeWidth(r rune) int {
if r == '\n' {
return 1
}
return runewidth.RuneWidth(r)
}

// stringWidth is like runewidth.StringWidth, but assumes newline characters
// take 1 cell.
func stringWidth(s string) int {
var width int
for _, r := range []rune(s) {
width += runeWidth(r)
}
return width
}

// truncateToCells truncates the beginning of text, so that it can be displayed
// in at most maxCells. Setting maxCells to zero disables truncating.
func truncateToCells(text string, maxCells int) string {
textCells := stringWidth(text)
if maxCells == 0 || textCells <= maxCells {
return text
}

haveCells := 0
textRunes := []rune(text)
i := len(textRunes) - 1
for ; i >= 0; i-- {
haveCells += runeWidth(textRunes[i])
if haveCells > maxCells {
break
}
}

var b strings.Builder
for j := i + 1; j < len(textRunes); j++ {
b.WriteRune(textRunes[j])
}
return b.String()
}
Loading

0 comments on commit 112c195

Please sign in to comment.