19 Commits

Author SHA1 Message Date
Nicolas Martin
7d1c04164e Return the value of paginator.TotalPages in SetTotalPages...
when the given number of items is below 1.
2020-10-28 10:40:23 -04:00
Christian Muehlhaeuser
97020cd0d2 Support Alt-Backspace to delete previous word 2020-10-27 08:50:22 +01:00
Christian Rocha
5dbcf95877 Fix situation where short viewports would panic (closes #20) 2020-10-25 15:21:49 -04:00
Christian Muehlhaeuser
f58fead10d Support Ctrl-W and Alt-D to delete words left/right of cursor
Ctrl-W: deletes word left of cursor
Alt-D: deletes word right of cursor
2020-10-25 14:52:16 -04:00
Christian Rocha
703de11da4 Fix regression where cursor was misplaced after a paste 2020-10-24 19:51:13 -04:00
Christian Rocha
03461d6804 Fix panic when pasting into a textinput with no char limit 2020-10-24 19:51:13 -04:00
Christian Muehlhaeuser
4445acbace Use the same badge order as in our other repos 2020-10-24 08:43:30 +02:00
Christian Muehlhaeuser
bd161e8ded Add release badge to README 2020-10-24 08:39:00 +02:00
Christian Muehlhaeuser
d9716a97f6 Fix link to example in README 2020-10-24 08:36:29 +02:00
Christian Muehlhaeuser
a0fe547fdb Make textinput cursor visible whenever it changes its position
This improves the UX because you never lose track of the cursor
moving around while it's currently hidden.
2020-10-24 00:12:27 -04:00
Christian Muehlhaeuser
1cb36774ed Split up workflows and automatically pick latest Go version 2020-10-23 13:18:52 +02:00
Christian Rocha
bdd909a5d7 Textinput: bind home/end and fix behavior of delete key 2020-10-22 15:36:54 -04:00
Will Bradley
b08b3efa02 add a couple alternate key bindings to make the scrolling slightly more intuitive 2020-10-21 22:54:58 -04:00
Christian Muehlhaeuser
83b6a2205f Add an EchoMode and EchoCharacter property to textinputs (#11)
EchoModes are:

- EchoNormal, displays text as is
- EchoPassword, displays EchoCharacter mask instead of actual characters
- EchoNone, displays nothing

Note that EchoOnEdit, which should displays character as they are entered and is otherwise identical to EchoPassword, is not yet implemented.
2020-10-21 18:17:41 -04:00
Christian Rocha
c06af8962d Update footer in README 2020-10-20 10:17:26 -04:00
Christian Muehlhaeuser
3a65be950a Remove release badge from README 2020-10-19 06:20:37 +02:00
Christian Muehlhaeuser
2dd6e0c80b Update README badges 2020-10-19 06:19:24 +02:00
Christian Muehlhaeuser
1e50e6d291 Add GitHub workflows 2020-10-19 06:16:01 +02:00
John Diego
573f90a876 Fix typo 2020-10-19 06:02:37 +02:00
10 changed files with 301 additions and 79 deletions

28
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: build
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
go-version: [~1.13, ^1]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Download Go modules
run: go mod download
- name: Build
run: go build -v ./...
- name: Test
run: go test ./...

28
.github/workflows/coverage.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: coverage
on: [push, pull_request]
jobs:
coverage:
strategy:
matrix:
go-version: [^1]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Coverage
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
go test -race -covermode atomic -coverprofile=profile.cov ./...
GO111MODULE=off go get github.com/mattn/goveralls
$(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github

20
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: lint
on: [push, pull_request]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.30
# Optional: golangci-lint command line arguments.
args: --issues-exit-code=0
# Optional: working directory, useful for monorepos
# working-directory: somedir
# Optional: show only new issues if it's a pull request. The default value is `false`.
only-new-issues: true

26
.golangci.yml Normal file
View File

@@ -0,0 +1,26 @@
run:
tests: false
issues:
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- dupl
- exportloopref
- goconst
- godot
- godox
- goimports
- gomnd
- goprintffuncname
- gosec
- misspell
- prealloc
- rowserrcheck
- sqlclosecheck
- unconvert
- unparam
- whitespace

View File

@@ -1,10 +1,10 @@
Bubbles
=======
<p>
<a href="https://pkg.go.dev/github.com/charmbracelet/bubbles?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
<a href="https://github.com/charmbracelet/bubbles/actions"><img src="https://github.com/charmbracelet/glow/workflows/build/badge.svg" alt="Build Status"></a>
</p>
[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)
[![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions)
[![Go ReportCard](http://goreportcard.com/badge/charmbracelet/bubbles)](http://goreportcard.com/report/charmbracelet/bubbles)
Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea) applications.
@@ -33,7 +33,7 @@ pasting, in-place scrolling when the value exceeds the width of the element and
the common, and many customization options.
* [Example code, one field](https://github.com/charmbracelet/tea/tree/master/examples/textinput/main.go)
* [Example code, many fields](https://github.com/charmbracelet/tea/tree/master/examples/textinput/main.go)
* [Example code, many fields](https://github.com/charmbracelet/tea/tree/master/examples/textinputs/main.go)
## Paginator
@@ -59,7 +59,7 @@ for applications which make use of the alterate screen buffer.
* [Example code](https://github.com/charmbracelet/tea/tree/master/examples/pager/main.go)
This compoent is well complimented with [Reflow][reflow] for ANSI-aware
This component is well complimented with [Reflow][reflow] for ANSI-aware
indenting and text wrapping.
[reflow]: https://github.com/muesli/reflow
@@ -72,8 +72,9 @@ indenting and text wrapping.
***
A [Charm](https://charm.sh) project.
Part of [Charm](https://charm.sh).
<img alt="the Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400">
<a href="https://charm.sh/"><img alt="the Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
Charm热爱开源! / Charm loves open source!
Charm热爱开源!

5
go.mod
View File

@@ -4,8 +4,9 @@ go 1.13
require (
github.com/atotto/clipboard v0.1.2
github.com/charmbracelet/bubbletea v0.12.0
github.com/charmbracelet/bubbletea v0.12.1
github.com/mattn/go-runewidth v0.0.9
github.com/muesli/termenv v0.7.4
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 // indirect
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a // indirect
)

10
go.sum
View File

@@ -1,7 +1,7 @@
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbletea v0.12.0 h1:/pUHp1GWRDyK1TJAWkXrnRH1u8Xc5076oH/J0NHxH+M=
github.com/charmbracelet/bubbletea v0.12.0/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=
github.com/charmbracelet/bubbletea v0.12.1 h1:t21pkG2IDBRduPbt2J64Dx5yt8yIidAkXwhhrc11SzY=
github.com/charmbracelet/bubbletea v0.12.1/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=
github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc=
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
@@ -21,6 +21,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -28,6 +30,6 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a h1:e3IU37lwO4aq3uoRKINC7JikojFmE5gO7xhfxs8VC34=
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -40,8 +40,8 @@ type Model struct {
// used for other things beyond navigating sets. Note that it both returns the
// number of total pages and alters the model.
func (m *Model) SetTotalPages(items int) int {
if items == 0 {
return 0
if items < 1 {
return m.TotalPages
}
n := items / m.PerPage
if items%m.PerPage > 0 {

View File

@@ -11,27 +11,49 @@ import (
"github.com/muesli/termenv"
)
const defaultBlinkSpeed = time.Millisecond * 530
const (
defaultBlinkSpeed = time.Millisecond * 600
// EchoNormal displays text as is. This is the default behavior.
EchoNormal EchoMode = iota
// EchoPassword displays the EchoCharacter mask instead of actual
// characters. This is commonly used for password fields.
EchoPassword
// EchoNone displays nothing as characters are entered. This is commonly
// seen for password fields on the command line.
EchoNone
// EchoOnEdit
)
// EchoMode sets the input behavior of the text input field.
type EchoMode int
var (
// color is a helper for returning colors.
color func(s string) termenv.Color = termenv.ColorProfile().Color
)
// Model is the Tea model for this text input element.
// Model is the Bubble Tea model for this text input element.
type Model struct {
Err error
Prompt string
Placeholder string
Cursor string
BlinkSpeed time.Duration
Placeholder string
TextColor string
BackgroundColor string
PlaceholderColor string
CursorColor string
EchoMode EchoMode
EchoCharacter rune
// CharLimit is the maximum amount of characters this input element will
// accept. If 0 or less, there's no limit.
CharLimit int
@@ -41,21 +63,21 @@ type Model struct {
// viewport. If 0 or less this setting is ignored.
Width int
// Underlying text value
// Underlying text value.
value []rune
// Focus indicates whether user input focus should be on this input
// component. When false, don't blink and ignore keyboard input.
focus bool
// Cursor blink state
// Cursor blink state.
blink bool
// Cursor position
// Cursor position.
pos int
// Used to emulate a viewport when width is set and the content is
// overflowing
// overflowing.
offset int
offsetRight int
}
@@ -69,7 +91,7 @@ func (m *Model) SetValue(s string) {
m.value = runes
}
if m.pos > len(m.value) {
m.pos = len(m.value)
m.SetCursor(len(m.value))
}
m.handleOverflow()
}
@@ -79,23 +101,22 @@ func (m Model) Value() string {
return string(m.value)
}
// Cursor start moves the cursor to the given position. If the position is out
// of bounds the cursor will be moved to the start or end accordingly.
// SetCursor start moves the cursor to the given position. If the position is
// out of bounds the cursor will be moved to the start or end accordingly.
func (m *Model) SetCursor(pos int) {
m.pos = clamp(pos, 0, len(m.value))
m.blink = false
m.handleOverflow()
}
// CursorStart moves the cursor to the start of the field.
func (m *Model) CursorStart() {
m.pos = 0
m.handleOverflow()
m.SetCursor(0)
}
// CursorEnd moves the cursor to the end of the field.
func (m *Model) CursorEnd() {
m.pos = len(m.value)
m.handleOverflow()
m.SetCursor(len(m.value))
}
// Focused returns the focus state on the model.
@@ -118,8 +139,7 @@ func (m *Model) Blur() {
// Reset sets the input to its default state with no input.
func (m *Model) Reset() {
m.value = nil
m.pos = 0
m.blink = false
m.SetCursor(0)
}
// Paste pastes the contents of the clipboard into the text area (if supported).
@@ -130,7 +150,10 @@ func (m *Model) Paste() {
}
paste := []rune(pasteString)
availSpace := m.CharLimit - len(m.value)
var availSpace int
if m.CharLimit > 0 {
availSpace = m.CharLimit - len(m.value)
}
// If the char limit's been reached cancel
if m.CharLimit > 0 && availSpace <= 0 {
@@ -139,7 +162,7 @@ func (m *Model) Paste() {
// If there's not enough space to paste the whole thing cut the pasted
// runes down so they'll fit
if availSpace < len(paste) {
if m.CharLimit > 0 && availSpace < len(paste) {
paste = paste[:len(paste)-availSpace]
}
@@ -152,15 +175,20 @@ func (m *Model) Paste() {
// Insert pasted runes
for _, r := range paste {
head = append(head, r)
availSpace--
m.pos++
if m.CharLimit > 0 && availSpace <= 0 {
if m.CharLimit > 0 {
availSpace--
if availSpace <= 0 {
break
}
}
}
// Put it all back together
m.value = append(head, tail...)
// Reset blink state and run overflow checks
m.SetCursor(m.pos)
}
// If a max width is defined, perform some logic to treat the visible area
@@ -228,6 +256,67 @@ func (m *Model) colorPlaceholder(s string) string {
String()
}
// deleteWordLeft deletes the word left to the cursor.
func (m *Model) deleteWordLeft() {
if m.pos == 0 || len(m.value) == 0 {
return
}
i := m.pos
m.SetCursor(m.pos - 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace before cursor
m.SetCursor(m.pos - 1)
}
for m.pos > 0 {
if !unicode.IsSpace(m.value[m.pos]) {
m.SetCursor(m.pos - 1)
} else {
if m.pos > 0 {
// keep the previous space
m.SetCursor(m.pos + 1)
}
break
}
}
if i > len(m.value) {
m.value = m.value[:m.pos]
} else {
m.value = append(m.value[:m.pos], m.value[i:]...)
}
}
// deleteWordRight deletes the word right to the cursor.
func (m *Model) deleteWordRight() {
if m.pos >= len(m.value) || len(m.value) == 0 {
return
}
i := m.pos
m.SetCursor(m.pos + 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace after cursor
m.SetCursor(m.pos + 1)
}
for m.pos < len(m.value) {
if !unicode.IsSpace(m.value[m.pos]) {
m.SetCursor(m.pos + 1)
} else {
break
}
}
if m.pos > len(m.value) {
m.value = m.value[:i]
} else {
m.value = append(m.value[:i], m.value[m.pos:]...)
}
m.SetCursor(i)
}
func (m *Model) wordLeft() {
if m.pos == 0 || len(m.value) == 0 {
return
@@ -237,7 +326,7 @@ func (m *Model) wordLeft() {
for i >= 0 {
if unicode.IsSpace(m.value[i]) {
m.pos--
m.SetCursor(m.pos - 1)
i--
} else {
break
@@ -246,7 +335,7 @@ func (m *Model) wordLeft() {
for i >= 0 {
if !unicode.IsSpace(m.value[i]) {
m.pos--
m.SetCursor(m.pos - 1)
i--
} else {
break
@@ -263,7 +352,7 @@ func (m *Model) wordRight() {
for i < len(m.value) {
if unicode.IsSpace(m.value[i]) {
m.pos++
m.SetCursor(m.pos + 1)
i++
} else {
break
@@ -272,7 +361,7 @@ func (m *Model) wordRight() {
for i < len(m.value) {
if !unicode.IsSpace(m.value[i]) {
m.pos++
m.SetCursor(m.pos + 1)
i++
} else {
break
@@ -280,6 +369,18 @@ func (m *Model) wordRight() {
}
}
func (m Model) echoTransform(v string) string {
switch m.EchoMode {
case EchoPassword:
return strings.Repeat(string(m.EchoCharacter), rw.StringWidth(v))
case EchoNone:
return ""
default:
return v
}
}
// BlinkMsg is sent when the cursor should alternate it's blinking state.
type BlinkMsg struct{}
@@ -287,11 +388,16 @@ type BlinkMsg struct{}
func NewModel() Model {
return Model{
Prompt: "> ",
BlinkSpeed: defaultBlinkSpeed,
Placeholder: "",
BlinkSpeed: defaultBlinkSpeed,
TextColor: "",
PlaceholderColor: "240",
CursorColor: "",
EchoCharacter: '*',
CharLimit: 0,
value: nil,
@@ -311,13 +417,15 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyBackspace:
fallthrough
case tea.KeyDelete:
case tea.KeyBackspace: // delete character before cursor
if msg.Alt {
m.deleteWordLeft()
} else {
if len(m.value) > 0 {
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
if m.pos > 0 {
m.pos--
m.SetCursor(m.pos - 1)
}
}
}
case tea.KeyLeft:
@@ -326,7 +434,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
break
}
if m.pos > 0 {
m.pos--
m.SetCursor(m.pos - 1)
}
case tea.KeyRight:
if msg.Alt { // alt+right arrow, forward one word
@@ -334,32 +442,37 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
break
}
if m.pos < len(m.value) {
m.pos++
m.SetCursor(m.pos + 1)
}
case tea.KeyCtrlW: // ^W, delete word left of cursor
m.deleteWordLeft()
case tea.KeyCtrlF: // ^F, forward one character
fallthrough
case tea.KeyCtrlB: // ^B, back one charcter
fallthrough
case tea.KeyCtrlA: // ^A, go to beginning
case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning
m.CursorStart()
case tea.KeyCtrlD: // ^D, delete char under cursor
case tea.KeyDelete, tea.KeyCtrlD: // ^D, delete char under cursor
if len(m.value) > 0 && m.pos < len(m.value) {
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...)
}
case tea.KeyCtrlE: // ^E, go to end
case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end
m.CursorEnd()
case tea.KeyCtrlK: // ^K, kill text after cursor
m.value = m.value[:m.pos]
m.pos = len(m.value)
m.SetCursor(len(m.value))
case tea.KeyCtrlU: // ^U, kill text before cursor
m.value = m.value[m.pos:]
m.pos = 0
m.SetCursor(0)
m.offset = 0
case tea.KeyCtrlV: // ^V paste
m.Paste()
case tea.KeyRune: // input a regular character
if msg.Alt {
if msg.Rune == 'd' { // alt+d, delete word right of cursor
m.deleteWordRight()
break
}
if msg.Rune == 'b' { // alt+b, back one word
m.wordLeft()
break
@@ -373,7 +486,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
// Input a regular character
if m.CharLimit <= 0 || len(m.value) < m.CharLimit {
m.value = append(m.value[:m.pos], append([]rune{msg.Rune}, m.value[m.pos:]...)...)
m.pos++
m.SetCursor(m.pos + 1)
}
}
@@ -396,12 +509,11 @@ func View(m Model) string {
value := m.value[m.offset:m.offsetRight]
pos := max(0, m.pos-m.offset)
v := m.colorText(string(value[:pos]))
v := m.colorText(m.echoTransform(string(value[:pos])))
if pos < len(value) {
v += cursorView(string(value[pos]), m) // cursor and text under it
v += m.colorText(string(value[pos+1:])) // text after cursor
v += cursorView(m.echoTransform(string(value[pos])), m) // cursor and text under it
v += m.colorText(m.echoTransform(string(value[pos+1:]))) // text after cursor
} else {
v += cursorView(" ", m)
}

View File

@@ -77,7 +77,7 @@ func (m *Model) SetContent(s string) {
func (m Model) visibleLines() (lines []string) {
if len(m.lines) > 0 {
top := max(0, m.YOffset)
bottom := min(len(m.lines), m.YOffset+m.Height)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines))
lines = m.lines[top:bottom]
}
return lines
@@ -125,7 +125,7 @@ func (m *Model) HalfViewDown() (lines []string) {
if len(m.lines) > 0 {
top := max(m.YOffset+m.Height/2, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -145,7 +145,7 @@ func (m *Model) HalfViewUp() (lines []string) {
if len(m.lines) > 0 {
top := max(m.YOffset, 0)
bottom := min(m.YOffset+m.Height/2, len(m.lines)-1)
bottom := clamp(m.YOffset+m.Height/2, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -171,7 +171,7 @@ func (m *Model) LineDown(n int) (lines []string) {
if len(m.lines) > 0 {
top := max(m.YOffset+m.Height-n, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -193,7 +193,7 @@ func (m *Model) LineUp(n int) (lines []string) {
if len(m.lines) > 0 {
top := max(0, m.YOffset)
bottom := min(m.YOffset+n, len(m.lines)-1)
bottom := clamp(m.YOffset+n, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -210,7 +210,7 @@ func (m *Model) GotoTop() (lines []string) {
if len(m.lines) > 0 {
top := m.YOffset
bottom := min(m.YOffset+m.Height, len(m.lines)-1)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -245,7 +245,7 @@ func Sync(m Model) tea.Cmd {
// TODO: we should probably use m.visibleLines() rather than these two
// expressions.
top := max(m.YOffset, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1)
bottom := clamp(m.YOffset+m.Height, 0, len(m.lines)-1)
return tea.SyncScrollArea(
m.lines[top:bottom],
@@ -304,14 +304,14 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
}
// Down half page
case "d":
case "d", "ctrl+d":
lines := m.HalfViewDown()
if m.HighPerformanceRendering {
cmd = ViewDown(m, lines)
}
// Up half page
case "u":
case "u", "ctrl+u":
lines := m.HalfViewUp()
if m.HighPerformanceRendering {
cmd = ViewUp(m, lines)
@@ -376,6 +376,10 @@ func View(m Model) string {
// ETC
func clamp(v, low, high int) int {
return min(high, max(low, v))
}
func min(a, b int) int {
if a < b {
return a