1// Copyright 2018 Google Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package text
16
17import (
18	"fmt"
19
20	"github.com/mum4k/termdash/keyboard"
21	"github.com/mum4k/termdash/mouse"
22	"github.com/mum4k/termdash/private/wrap"
23)
24
25// options.go contains configurable options for Text.
26
27// Option is used to provide options to New().
28type Option interface {
29	// set sets the provided option.
30	set(*options)
31}
32
33// options stores the provided options.
34type options struct {
35	scrollUp         rune
36	scrollDown       rune
37	wrapMode         wrap.Mode
38	rollContent      bool
39	maxTextCells     int
40	disableScrolling bool
41	mouseUpButton    mouse.Button
42	mouseDownButton  mouse.Button
43	keyUp            keyboard.Key
44	keyDown          keyboard.Key
45	keyPgUp          keyboard.Key
46	keyPgDown        keyboard.Key
47}
48
49// newOptions returns a new options instance.
50func newOptions(opts ...Option) *options {
51	opt := &options{
52		scrollUp:        DefaultScrollUpRune,
53		scrollDown:      DefaultScrollDownRune,
54		mouseUpButton:   DefaultScrollMouseButtonUp,
55		mouseDownButton: DefaultScrollMouseButtonDown,
56		keyUp:           DefaultScrollKeyUp,
57		keyDown:         DefaultScrollKeyDown,
58		keyPgUp:         DefaultScrollKeyPageUp,
59		keyPgDown:       DefaultScrollKeyPageDown,
60		maxTextCells:    DefaultMaxTextCells,
61	}
62	for _, o := range opts {
63		o.set(opt)
64	}
65	return opt
66}
67
68// validate validates the provided options.
69func (o *options) validate() error {
70	keys := map[keyboard.Key]bool{
71		o.keyUp:     true,
72		o.keyDown:   true,
73		o.keyPgUp:   true,
74		o.keyPgDown: true,
75	}
76	if len(keys) != 4 {
77		return fmt.Errorf("invalid ScrollKeys(up:%v, down:%v, pageUp:%v, pageDown:%v), the keys must be unique", o.keyUp, o.keyDown, o.keyPgUp, o.keyPgDown)
78	}
79	if o.mouseUpButton == o.mouseDownButton {
80		return fmt.Errorf("invalid ScrollMouseButtons(up:%v, down:%v), the buttons must be unique", o.mouseUpButton, o.mouseDownButton)
81	}
82	if o.maxTextCells < 0 {
83		return fmt.Errorf("invalid MaxTextCells(%d), must be zero or a positive integer", o.maxTextCells)
84	}
85	return nil
86}
87
88// option implements Option.
89type option func(*options)
90
91// set implements Option.set.
92func (o option) set(opts *options) {
93	o(opts)
94}
95
96// ScrollRunes configures the text widgets scroll runes, shown at the top and
97// bottom of a scrollable text widget. If not provided, the default scroll
98// runes will be used.
99func ScrollRunes(up, down rune) Option {
100	return option(func(opts *options) {
101		opts.scrollUp = up
102		opts.scrollDown = down
103	})
104}
105
106// The default scroll runes for content scrolling
107const (
108	DefaultScrollUpRune   = '⇧'
109	DefaultScrollDownRune = '⇩'
110)
111
112// WrapAtWords configures the text widget so that it automatically wraps lines
113// that are longer than the width of the widget at word boundaries. If not
114// provided, long lines are trimmed instead.
115func WrapAtWords() Option {
116	return option(func(opts *options) {
117		opts.wrapMode = wrap.AtWords
118	})
119}
120
121// WrapAtRunes configures the text widget so that it automatically wraps lines
122// that are longer than the width of the widget at rune boundaries. If not
123// provided, long lines are trimmed instead.
124func WrapAtRunes() Option {
125	return option(func(opts *options) {
126		opts.wrapMode = wrap.AtRunes
127	})
128}
129
130// RollContent configures the text widget so that it rolls the text content up
131// if more text than the size of the container is added. If not provided, the
132// content is trimmed instead.
133func RollContent() Option {
134	return option(func(opts *options) {
135		opts.rollContent = true
136	})
137}
138
139// DisableScrolling disables the scrolling of the content using keyboard and
140// mouse.
141func DisableScrolling() Option {
142	return option(func(opts *options) {
143		opts.disableScrolling = true
144	})
145}
146
147// The default mouse buttons for content scrolling.
148const (
149	DefaultScrollMouseButtonUp   = mouse.ButtonWheelUp
150	DefaultScrollMouseButtonDown = mouse.ButtonWheelDown
151)
152
153// ScrollMouseButtons configures the mouse buttons that scroll the content.
154// The provided buttons must be unique, e.g. the same button cannot be both up
155// and down.
156func ScrollMouseButtons(up, down mouse.Button) Option {
157	return option(func(opts *options) {
158		opts.mouseUpButton = up
159		opts.mouseDownButton = down
160	})
161}
162
163// The default keys for content scrolling.
164const (
165	DefaultScrollKeyUp       = keyboard.KeyArrowUp
166	DefaultScrollKeyDown     = keyboard.KeyArrowDown
167	DefaultScrollKeyPageUp   = keyboard.KeyPgUp
168	DefaultScrollKeyPageDown = keyboard.KeyPgDn
169)
170
171// ScrollKeys configures the keyboard keys that scroll the content.
172// The provided keys must be unique, e.g. the same key cannot be both up and
173// down.
174func ScrollKeys(up, down, pageUp, pageDown keyboard.Key) Option {
175	return option(func(opts *options) {
176		opts.keyUp = up
177		opts.keyDown = down
178		opts.keyPgUp = pageUp
179		opts.keyPgDown = pageDown
180	})
181}
182
183// The default value for the MaxTextCells option.
184// Use zero as no limit, for logs you may wish to try 10,000 or higher.
185const (
186	DefaultMaxTextCells = 0
187)
188
189// MaxTextCells limits the text content to this number of terminal cells.
190// This is useful when sending large amounts of text to the Text widget, e.g.
191// when tailing logs as it will limit the memory usage.
192// When the newly added content goes over this number of cells, the Text widget
193// behaves as a circular buffer and drops earlier content to accommodate the
194// new one.
195// Note the count is in cells, not runes, some wide runes can take multiple
196// terminal cells.
197func MaxTextCells(max int) Option {
198	return option(func(opts *options) {
199		opts.maxTextCells = max
200	})
201}
202