54 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
Christian Rocha
a498b857d6 Update paginator for bubbletea 0.12.0 2020-10-16 14:36:08 -04:00
Christian Rocha
82097a1c79 Remove unused function 2020-10-16 11:54:44 -04:00
Christian Rocha
a8ec421800 Update bubbletea and termenv dependencies 2020-10-16 11:54:00 -04:00
Christian Rocha
9c10cd0586 Remove unused ErrMsg type from textinput module 2020-10-15 18:52:36 -04:00
Christian Rocha
154f3763f7 textinput.View can just accept a textinput.Model argument 2020-10-15 18:43:08 -04:00
Christian Rocha
eff931bdea Remove replace directive for bubbletea in go.mod 2020-10-12 22:15:02 -04:00
Christian Rocha
0d7b3ed8e8 Fix URLs to Charm and Glow in README 2020-10-06 13:25:46 -04:00
Christian Rocha
cbafae84f9 Update mouse wheel stuff in paginator per last-minute Bubble Tea API change 2020-10-06 12:29:18 -04:00
Christian Rocha
959598c9aa Fix outdated comment 2020-10-02 13:03:04 -04:00
Christian Rocha
5f7c8b5375 Simplify the API around fine-grained spinner display rules 2020-08-25 09:36:43 -04:00
Christian Rocha
75df6b96a1 Don't return the current frame in TickMsg 2020-08-24 21:09:28 -04:00
Christian Rocha
084d0acefd Fix comments + minimum lifetime should work in conjunction with HideFor 2020-08-24 21:07:45 -04:00
Christian Rocha
e8526ec4ef Optional fine-grained rules for the spinner about when and how long to show it 2020-08-24 20:03:54 -04:00
Christian Rocha
68f11d0b63 Update termenv to 0.7.2 2020-08-22 11:24:06 -04:00
Christian Muehlhaeuser
f78a02cc29 Avoid unnecessary conversions 2020-08-22 12:41:37 +02:00
Christian Muehlhaeuser
9a4fbb3be1 Make golint happy 2020-08-22 12:41:09 +02:00
Christian Rocha
d4a0ecd3ee Bump bubbletea dependency and set termenv dependency to v0.7.0 2020-08-20 13:31:31 -04:00
Christian Rocha
34b1f47068 Fix link to Bubble Tea in README 2020-07-31 19:11:13 -04:00
Christian Rocha
dbd40713ce Remove extraneous methods 2020-07-29 20:04:09 -04:00
Christian Rocha
54a0d84255 Add badges 2020-07-29 19:24:09 -04:00
Christian Rocha
004511e00f Add example GIFs 2020-07-29 19:20:41 -04:00
Christian Rocha
7fa53ea961 Link directly to example code and flesh out pagination component description 2020-07-29 16:38:59 -04:00
Christian Rocha
0cc5e71a63 Flesh out README and add Charm badge 2020-07-29 16:34:01 -04:00
Christian Rocha
f11ca377f4 Add clipboard paste support to textarea 2020-07-23 11:53:58 -04:00
Christian Rocha
10022c964c Fix slice out of bounds errors that could happen when deleting text 2020-07-21 18:22:18 -04:00
Christian Rocha
5357dd61bd Fix bug where scroll wheel could create duplicate lines on top and bottom 2020-07-20 17:46:15 -04:00
Christian Rocha
5a26cb0d8e Use pgup/pgdown as default paginator keystrokes 2020-07-20 12:13:53 -04:00
Christian Rocha
88469a499e Tidy up the key case switch in viewport 2020-07-20 11:56:35 -04:00
Christian Rocha
b130d96434 Add methods for jumping to the tom and bottom of the viewport 2020-07-17 19:22:08 -04:00
Christian Rocha
185a19e56f Fix cases where pager would add blank lines to bottom of document 2020-07-17 18:38:38 -04:00
Christian Rocha
dbb1d93970 Remove redundant viewport commands 2020-07-17 18:22:51 -04:00
Christian Rocha
2f909886c1 Be more consistent with max() argument order 2020-07-17 18:13:48 -04:00
Christian Rocha
5720cfb35a Correct scroll percentage reporting 2020-07-17 18:13:05 -04:00
Christian Rocha
3c07b97d37 Correct typo, add potential TODO 2020-07-17 17:02:37 -04:00
Christian Rocha
9b3e5476c3 Update README a little. 2020-07-17 14:03:02 -04:00
11 changed files with 621 additions and 238 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,9 +1,80 @@
# Tea Party
Bubbles
=======
Components for [Tea](https://github.com/charmbraclet/tea)
[![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.
These components are used in production in [Glow][glow] and [Charm][charm].
[glow]: https://github.com/charmbracelet/glow
[charm]: https://github.com/charmbracelet/charm
## Spinner
<img src="https://stuff.charm.sh/bubbles-examples/spinner.gif" width="400" alt="Spinner Example">
A spinner, useful for indicating that some kind an operation is happening.
There are a couple default ones, but you can also pass your own ”frames.”
* [Example code](https://github.com/charmbracelet/tea/tree/master/examples/spinner/main.go)
## Text Input
<img src="https://stuff.charm.sh/bubbles-examples/textinput.gif" width="400" alt="Text Input Example">
A text input field, akin to an `<input type="text">` in HTML. Supports unicode,
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/textinputs/main.go)
## Paginator
<img src="https://stuff.charm.sh/bubbles-examples/pagination.gif" width="200" alt="Paginator Example">
A component for handling pagination logic and optionally drawing pagination UI.
Supports "dot-style" pagination (similar to what you might see on iOS) and
numeric page numbering, but you could also just use this component for the
logic and visualize pagination however you like.
This component is used in [Glow][glow] to browse documents and [Charm][charm] to
browse SSH keys.
## Viewport
<img src="https://stuff.charm.sh/bubbles-examples/viewport.gif" width="600" alt="Viewport Example">
A viewport for vertically scrolling content. Optionally includes standard
pager keybindings and mouse wheel support. A high performance mode is available
for applications which make use of the alterate screen buffer.
* [Example code](https://github.com/charmbracelet/tea/tree/master/examples/pager/main.go)
This component is well complimented with [Reflow][reflow] for ANSI-aware
indenting and text wrapping.
[reflow]: https://github.com/muesli/reflow
⚠️ This project is a pre-release! Check back later.
## License
[MIT](https://github.com/charmbracelet/teaparty/raw/master/LICENSE)
***
Part of [Charm](https://charm.sh).
<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!

11
go.mod
View File

@@ -3,11 +3,10 @@ module github.com/charmbracelet/bubbles
go 1.13
require (
github.com/charmbracelet/bubbletea v0.9.1-0.20200713153904-2f53eeb54b90
github.com/atotto/clipboard v0.1.2
github.com/charmbracelet/bubbletea v0.12.1
github.com/mattn/go-runewidth v0.0.9
github.com/muesli/termenv v0.5.3-0.20200625163851-04b5c30e4c04
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 // indirect
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
github.com/muesli/termenv v0.7.4
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a // indirect
)
replace github.com/charmbracelet/bubbletea => ../bubbletea

33
go.sum
View File

@@ -1,3 +1,9 @@
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.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=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
@@ -6,21 +12,24 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/muesli/termenv v0.5.3-0.20200625163851-04b5c30e4c04 h1:Wr876oXlAk6avTWi0daXAriOr+r5fqIuyDmtNc/KwY0=
github.com/muesli/termenv v0.5.3-0.20200625163851-04b5c30e4c04/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI=
github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03 h1:pd4YKIqCB0U7O2I4gWHgEUA2mCEOENmco0l/bM957bU=
github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03/go.mod h1:Z9+Ul5bCbBKnbCvdOWbLqTHhJiYV414CURZJba6L8qA=
github.com/muesli/termenv v0.7.2 h1:r1raklL3uKE7rOvWgSenmEm2px+dnc33OTisZ8YR1fw=
github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=
github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8=
github.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-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

@@ -13,7 +13,7 @@ import (
// Type specifies the way we render pagination.
type Type int
// Pagination rendering options
// Pagination rendering options.
const (
Arabic Type = iota
Dots
@@ -28,6 +28,7 @@ type Model struct {
ActiveDot string
InactiveDot string
ArabicFormat string
UsePgUpPgDownKeys bool
UseLeftRightKeys bool
UseUpDownKeys bool
UseHLKeys bool
@@ -39,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 {
@@ -105,6 +106,7 @@ func NewModel() Model {
ActiveDot: "•",
InactiveDot: "○",
ArabicFormat: "%d/%d",
UsePgUpPgDownKeys: true,
UseLeftRightKeys: true,
UseUpDownKeys: false,
UseHLKeys: true,
@@ -116,6 +118,14 @@ func NewModel() Model {
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if m.UsePgUpPgDownKeys {
switch msg.String() {
case "pgup":
m.PrevPage()
case "pgdown":
m.NextPage()
}
}
if m.UseLeftRightKeys {
switch msg.String() {
case "left":
@@ -154,11 +164,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
}
// View renders the pagination to a string.
func View(model tea.Model) string {
m, ok := model.(Model)
if !ok {
return "could not perform assertion on model"
}
func View(m Model) string {
switch m.Type {
case Dots:
return dotsView(m)

View File

@@ -29,7 +29,7 @@ type Model struct {
// Type is the set of frames to use. See Spinner.
Frames Spinner
// FPS is the speed at which the ticker should tick
// FPS is the speed at which the ticker should tick.
FPS time.Duration
// ForegroundColor sets the background color of the spinner. It can be a
@@ -44,7 +44,76 @@ type Model struct {
// (per github.com/muesli/termenv).
BackgroundColor string
// MinimumLifetime is the minimum amount of time the spinner can run. Any
// logic around this can be implemented in view that implements this
// spinner. If HideFor is set MinimumLifetime will be added on top of
// HideFor. In other words, if HideFor is 100ms and MinimumLifetime is
// 200ms then MinimumLifetime will expire after 300ms.
//
// MinimumLifetime is optional.
//
// This is considered experimental and may not appear in future versions of
// this library.
MinimumLifetime time.Duration
// HideFor can be used to wait to show the spinner until a certain amount
// of time has passed. This can be useful for preventing flicking when load
// times are very fast.
// Optional.
//
// This is considered experimental and may not appear in future versions of
// this library.
HideFor time.Duration
frame int
startTime time.Time
}
// Start resets resets the spinner start time. For use with MinimumLifetime and
// MinimumStartTime. Optional.
//
// This is considered experimental and may not appear in future versions of
// this library.
func (m *Model) Start() {
m.startTime = time.Now()
}
// hidden returns whether or not Model.HideFor is in effect.
func (m Model) hidden() bool {
if m.startTime.IsZero() {
return false
}
if m.HideFor == 0 {
return false
}
return m.startTime.Add(m.HideFor).After(time.Now())
}
// finished returns whether Model.MinimumLifetimeReached has been met.
func (m Model) finished() bool {
if m.startTime.IsZero() {
return true
}
if m.MinimumLifetime == 0 {
return true
}
return m.startTime.Add(m.HideFor).Add(m.MinimumLifetime).Before(time.Now())
}
// Visible returns whether or not the view should be rendered. Works in
// conjunction with Model.HideFor and Model.MinimumLifetimeReached. You should
// use this message directly to determine whether or not to render this view in
// the parent view and whether to continue sending spin messaging in the
// parent update function.
//
// Also note that using this function is optional and generally considered for
// advanced use only. Most of the time your application logic will determine
// whether or not this view should be used.
//
// This is considered experimental and may not appear in future versions of
// this library.
func (m Model) Visible() bool {
return !m.hidden() && !m.finished()
}
// NewModel returns a model with default values.
@@ -56,7 +125,9 @@ func NewModel() Model {
}
// TickMsg indicates that the timer has ticked and we should render a frame.
type TickMsg struct{}
type TickMsg struct {
Time time.Time
}
// Update is the Tea update function. This will advance the spinner one frame
// every time it's called, regardless the message passed, so be sure the logic
@@ -93,7 +164,9 @@ func View(model Model) string {
// Tick is the command used to advance the spinner one frame.
func Tick(m Model) tea.Cmd {
return tea.Tick(m.FPS, func(time.Time) tea.Msg {
return TickMsg{}
return tea.Tick(m.FPS, func(t time.Time) tea.Msg {
return TickMsg{
Time: t,
}
})
}

View File

@@ -5,37 +5,55 @@ import (
"time"
"unicode"
"github.com/atotto/clipboard"
tea "github.com/charmbracelet/bubbletea"
rw "github.com/mattn/go-runewidth"
"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 is a helper for returning colors.
color func(s string) termenv.Color = termenv.ColorProfile().Color
)
// ErrMsg indicates there's been an error. We don't handle errors in the this
// package; we're expecting errors to be handle in the program that implements
// this text input.
type ErrMsg error
// 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
@@ -45,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
}
@@ -73,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()
}
@@ -83,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.
@@ -122,8 +139,56 @@ 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).
func (m *Model) Paste() {
pasteString, err := clipboard.ReadAll()
if err != nil {
m.Err = err
}
paste := []rune(pasteString)
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 {
return
}
// If there's not enough space to paste the whole thing cut the pasted
// runes down so they'll fit
if m.CharLimit > 0 && availSpace < len(paste) {
paste = paste[:len(paste)-availSpace]
}
// Stuff before and after the cursor
head := m.value[:m.pos]
tailSrc := m.value[m.pos:]
tail := make([]rune, len(tailSrc))
copy(tail, tailSrc)
// Insert pasted runes
for _, r := range paste {
head = append(head, r)
m.pos++
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
@@ -139,7 +204,6 @@ func (m *Model) handleOverflow() {
m.offsetRight = min(m.offsetRight, len(m.value))
if m.pos < m.offset {
m.offset = m.pos
w := 0
@@ -154,9 +218,7 @@ func (m *Model) handleOverflow() {
}
m.offsetRight = m.offset + i
} else if m.pos >= m.offsetRight {
m.offsetRight = m.pos
w := 0
@@ -171,7 +233,6 @@ func (m *Model) handleOverflow() {
}
m.offset = m.offsetRight - (len(runes) - 1 - i)
}
}
@@ -195,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
@@ -204,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
@@ -213,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
@@ -229,8 +351,8 @@ func (m *Model) wordRight() {
i := m.pos
for i < len(m.value) {
if unicode.IsSpace(rune(m.value[i])) {
m.pos++
if unicode.IsSpace(m.value[i]) {
m.SetCursor(m.pos + 1)
i++
} else {
break
@@ -238,8 +360,8 @@ func (m *Model) wordRight() {
}
for i < len(m.value) {
if !unicode.IsSpace(rune(m.value[i])) {
m.pos++
if !unicode.IsSpace(m.value[i]) {
m.SetCursor(m.pos + 1)
i++
} else {
break
@@ -247,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{}
@@ -254,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,
@@ -278,12 +417,16 @@ 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[:m.pos-1], m.value[m.pos:]...)
m.pos--
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
if m.pos > 0 {
m.SetCursor(m.pos - 1)
}
}
}
case tea.KeyLeft:
if msg.Alt { // alt+left arrow, back one word
@@ -291,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
@@ -299,30 +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
@@ -336,13 +486,10 @@ 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)
}
}
case ErrMsg:
m.Err = msg
case BlinkMsg:
m.blink = !m.blink
return m, Blink(m)
@@ -354,25 +501,19 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
}
// View renders the textinput in its current state.
func View(model tea.Model) string {
m, ok := model.(Model)
if !ok {
return "could not perform assertion on model"
}
func View(m Model) string {
// Placeholder text
if len(m.value) == 0 && m.Placeholder != "" {
return placeholderView(m)
}
value := m.value[m.offset:m.offsetRight]
pos := m.pos - m.offset
v := m.colorText(string(value[:pos]))
pos := max(0, m.pos-m.offset)
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)
}
@@ -394,7 +535,7 @@ func View(model tea.Model) string {
return m.Prompt + v
}
// placeholderView
// placeholderView.
func placeholderView(m Model) string {
var (
v string
@@ -403,10 +544,7 @@ func placeholderView(m Model) string {
// Cursor
if m.blink && m.PlaceholderColor != "" {
v += cursorView(
m.colorPlaceholder(p[:1]),
m,
)
v += cursorView(m.colorPlaceholder(p[:1]), m)
} else {
v += cursorView(p[:1], m)
}

View File

@@ -7,6 +7,10 @@ import (
tea "github.com/charmbracelet/bubbletea"
)
const (
spacebar = " "
)
// MODEL
type Model struct {
@@ -33,29 +37,21 @@ type Model struct {
lines []string
}
// TODO: do we really need this?
func NewModel(width, height int) Model {
return Model{
Width: width,
Height: height,
}
}
// TODO: do we really need this?
func (m Model) SetSize(yPos, width, height int) {
m.YPosition = yPos
m.Width = width
m.Height = height
}
// AtTop returns whether or not the viewport is in the very top position.
func (m Model) AtTop() bool {
return m.YOffset <= 0
}
// AtBottom returns whether or not the viewport is at the very botom position.
// AtBottom returns whether or not the viewport is at or past the very bottom
// position.
func (m Model) AtBottom() bool {
return m.YOffset >= len(m.lines)-m.Height-1
return m.YOffset >= len(m.lines)-1-m.Height
}
// PastBottom returns whether or not the viewport is scrolled beyond the last
// line. This can happen when adjusting the viewport height.
func (m Model) PastBottom() bool {
return m.YOffset > len(m.lines)-1-m.Height
}
// Scrollpercent returns the amount scrolled as a float between 0 and 1.
@@ -65,7 +61,7 @@ func (m Model) ScrollPercent() float64 {
}
y := float64(m.YOffset)
h := float64(m.Height)
t := float64(len(m.lines))
t := float64(len(m.lines) - 1)
v := y / (t - h)
return math.Max(0.0, math.Min(1.0, v))
}
@@ -77,11 +73,11 @@ func (m *Model) SetContent(s string) {
m.lines = strings.Split(s, "\n")
}
// Return the lines that should currently be visible in the viewport
// Return the lines that should currently be visible in the viewport.
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
@@ -96,7 +92,7 @@ func (m *Model) ViewDown() []string {
m.YOffset = min(
m.YOffset+m.Height, // target
len(m.lines)-m.Height, // fallback
len(m.lines)-1-m.Height, // fallback
)
return m.visibleLines()
@@ -124,12 +120,12 @@ func (m *Model) HalfViewDown() (lines []string) {
m.YOffset = min(
m.YOffset+m.Height/2, // target
len(m.lines)-m.Height, // fallback
len(m.lines)-1-m.Height, // fallback
)
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]
}
@@ -149,27 +145,33 @@ 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]
}
return lines
}
// LineDown moves the view up by the given number of lines.
// LineDown moves the view down by the given number of lines.
func (m *Model) LineDown(n int) (lines []string) {
if m.AtBottom() || n == 0 {
return nil
}
// Make sure the number of lines by which we're going to scroll isn't
// greater than the number of lines we actually have left before we reach
// the bottom.
maxDelta := (len(m.lines) - 1) - (m.YOffset + m.Height) // number of lines - viewport bottom edge
n = min(n, maxDelta)
m.YOffset = min(
m.YOffset+n, // target
len(m.lines)-m.Height, // fallback
len(m.lines)-1-m.Height, // fallback
)
if len(m.lines) > 0 {
top := max(0, m.YOffset+m.Height-n)
bottom := min(len(m.lines)-1, m.YOffset+m.Height)
top := max(m.YOffset+m.Height-n, 0)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -183,11 +185,45 @@ func (m *Model) LineUp(n int) (lines []string) {
return nil
}
// Make sure the number of lines by which we're going to scroll isn't
// greater than the number of lines we are from the top.
n = min(n, m.YOffset)
m.YOffset = max(m.YOffset-n, 0)
if len(m.lines) > 0 {
top := max(0, m.YOffset)
bottom := min(len(m.lines)-1, m.YOffset+n)
bottom := clamp(m.YOffset+n, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
return lines
}
// GotoTop sets the viewport to the top position.
func (m *Model) GotoTop() (lines []string) {
if m.AtTop() {
return nil
}
m.YOffset = 0
if len(m.lines) > 0 {
top := m.YOffset
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
return lines
}
// GotoTop sets the viewport to the bottom position.
func (m *Model) GotoBottom() (lines []string) {
m.YOffset = max(len(m.lines)-1-m.Height, 0)
if len(m.lines) > 0 {
top := m.YOffset
bottom := max(len(m.lines)-1, 0)
lines = m.lines[top:bottom]
}
@@ -206,8 +242,10 @@ func Sync(m Model) tea.Cmd {
return nil
}
// 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],
@@ -216,9 +254,9 @@ func Sync(m Model) tea.Cmd {
)
}
// ViewDown is a high performance command that moves the viewport up by one
// viewport height. Use Model.ViewDown to get the lines that should be
// rendered. For example:
// ViewDown is a high performance command that moves the viewport up by a given
// numer of lines. Use Model.ViewDown to get the lines that should be rendered.
// For example:
//
// lines := model.ViewDown(1)
// cmd := ViewDown(m, lines)
@@ -230,8 +268,8 @@ func ViewDown(m Model, lines []string) tea.Cmd {
return tea.ScrollDown(lines, m.YPosition, m.YPosition+m.Height)
}
// ViewUp is a high performance command the moves the viewport down by one
// viewport height. Use Model.ViewDown to get the lines that should be
// ViewUp is a high performance command the moves the viewport down by a given
// number of lines height. Use Model.ViewDown to get the lines that should be
// rendered.
func ViewUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
@@ -240,46 +278,6 @@ func ViewUp(m Model, lines []string) tea.Cmd {
return tea.ScrollUp(lines, m.YPosition, m.YPosition+m.Height)
}
// HalfViewDown is a high performance command the moves the viewport down by
// half of the height of the viewport. Use Model.HalfViewDown to get the lines
// that should be rendered.
func HalfViewDown(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
return nil
}
return tea.ScrollDown(lines, m.YPosition, m.YPosition+m.Height)
}
// HalfViewUp is a high performance command the moves the viewport up by
// half of the height of the viewport. Use Model.HalfViewUp to get the lines
// that should be rendered.
func HalfViewUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
return nil
}
return tea.ScrollUp(lines, m.YPosition, m.YPosition+m.Height)
}
// LineDown is a high performance command the moves the viewport down by
// a given number of lines. Use Model.LineDown to get the lines that should be
// rendered.
func LineDown(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
return nil
}
return tea.ScrollDown(lines, m.YPosition, m.YPosition+m.Height)
}
// LineDown is a high performance command the moves the viewport up by a given
// number of lines. Use Model.LineDown to get the lines that should be
// rendered.
func LineUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
return nil
}
return tea.ScrollUp(lines, m.YPosition, m.YPosition+m.Height)
}
// UPDATE
// Update runs the update loop with default keybindings similar to popular
@@ -289,75 +287,63 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
// Down one page
case "pgdown":
fallthrough
case " ": // spacebar
fallthrough
case "f":
case "pgdown", spacebar, "f":
lines := m.ViewDown()
if m.HighPerformanceRendering {
cmd = ViewDown(m, lines)
}
// Up one page
case "pgup":
fallthrough
case "b":
case "pgup", "b":
lines := m.ViewUp()
if m.HighPerformanceRendering {
cmd = ViewUp(m, lines)
}
// Down half page
case "d":
case "d", "ctrl+d":
lines := m.HalfViewDown()
if m.HighPerformanceRendering {
cmd = HalfViewDown(m, lines)
cmd = ViewDown(m, lines)
}
// Up half page
case "u":
case "u", "ctrl+u":
lines := m.HalfViewUp()
if m.HighPerformanceRendering {
cmd = HalfViewUp(m, lines)
cmd = ViewUp(m, lines)
}
// Down one line
case "down":
fallthrough
case "j":
case "down", "j":
lines := m.LineDown(1)
if m.HighPerformanceRendering {
cmd = LineDown(m, lines)
cmd = ViewDown(m, lines)
}
// Up one line
case "up":
fallthrough
case "k":
case "up", "k":
lines := m.LineUp(1)
if m.HighPerformanceRendering {
cmd = LineUp(m, lines)
cmd = ViewUp(m, lines)
}
}
case tea.MouseMsg:
switch msg.Button {
switch msg.Type {
case tea.MouseWheelUp:
lines := m.LineUp(3)
if m.HighPerformanceRendering {
cmd = LineUp(m, lines)
cmd = ViewUp(m, lines)
}
case tea.MouseWheelDown:
lines := m.LineDown(3)
if m.HighPerformanceRendering {
cmd = LineDown(m, lines)
cmd = ViewDown(m, lines)
}
}
}
@@ -369,7 +355,6 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
// View renders the viewport into a string.
func View(m Model) string {
if m.HighPerformanceRendering {
// Just send newlines since we're doing to be rendering the actual
// content seprately. We still need send something that equals the
@@ -391,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
@@ -404,7 +393,3 @@ func max(a, b int) int {
}
return b
}
func clamp(val, low, high int) int {
return max(low, min(high, val))
}