73 Commits

Author SHA1 Message Date
Christian Rocha
84f7b047bb Make Viewport more idomatic BubbleTea (per v0.12.x) 2020-11-08 21:20:15 -05:00
Christian Rocha
9e1e435bba Make Spinner more idomatic Bubble Tea (per v0.12.x) 2020-11-08 21:20:15 -05:00
Christian Rocha
0fd072ddcc Make Paginator more idomatic Bubble Tea (per v0.12.x) 2020-11-08 21:20:15 -05:00
ololosha228
a07ab1d6af Added more spinners 2020-11-04 14:48:03 +01:00
Christian Rocha
8e49ee609e Bump Bubble Tea dependency to v0.12.2 2020-11-02 10:24:20 -05:00
Christian Rocha
d02641f6b5 Update textinput for multi-char (IME) input in Bubble Tea master 2020-11-01 09:11:18 -05:00
Christian Rocha
9b47f26bdd Doc and logic corrections per the linter 2020-10-28 21:27:15 -04:00
Christian Rocha
9c780011ff Underpinnings for always-on and always-hidden cursor modes 2020-10-28 21:27:15 -04:00
Christian Rocha
8148e61443 Correct ^F/^B keybindings for forward/back cursor movement 2020-10-28 21:27:15 -04:00
Christian Rocha
ce7d8da084 Fix lock-up that could occur with cursor blinking
Note that to do this we've replaced the blink timer with a context.
2020-10-28 21:27:15 -04:00
Christian Rocha
d14fdf585c Textinput Update and View functions are now methods on the model 2020-10-28 21:27:15 -04:00
Christian Rocha
bf7719e6c1 Handle paste via command/message since it's IO 2020-10-28 21:27:15 -04:00
Christian Rocha
1b530b293c Reset blink timer when moving the cursor 2020-10-28 21:27:15 -04:00
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
Christian Rocha
74e84eca55 Bump Bubbletea dependency 2020-07-14 18:26:59 -04:00
Christian Rocha
ccf4c90b6b Rework spinner to allow for user-defined custom spinners 2020-07-14 18:21:48 -04:00
Christian Rocha
f341e3c896 Correct mouse wheel behavior 2020-06-23 13:10:50 -04:00
Christian Rocha
c9196e5407 Add basic mouse wheel support to viewport 2020-06-23 12:00:17 -04:00
Christian Rocha
f967f6a87f Fix a bug where placeholder text would not reappear post-input 2020-06-22 15:28:38 -04:00
Christian Rocha
35c3cd626d Fix spinner frame skipping and remove custom message functionality 2020-06-22 14:49:21 -04:00
11 changed files with 842 additions and 349 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 ## License
[MIT](https://github.com/charmbracelet/teaparty/raw/master/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!

10
go.mod
View File

@@ -3,10 +3,10 @@ module github.com/charmbracelet/bubbles
go 1.13 go 1.13
require ( require (
github.com/charmbracelet/bubbletea v0.8.3-0.20200622174044-8bab4bce9a84 github.com/atotto/clipboard v0.1.2
github.com/charmbracelet/bubbletea v0.12.2
github.com/mattn/go-runewidth v0.0.9 github.com/mattn/go-runewidth v0.0.9
github.com/muesli/termenv v0.5.3-0.20200617154346-da5de72b61c0 github.com/muesli/termenv v0.7.4
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 // indirect 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

31
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.2 h1:y9Yo2Pv8tcm3mAJsWONGsmHhzrbNxJVxpVtemikxE9A=
github.com/charmbracelet/bubbletea v0.12.2/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 h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= 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= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
@@ -6,19 +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-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 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/muesli/termenv v0.5.3-0.20200617154346-da5de72b61c0 h1:IASsj5pQ478TEN9TLiuhP+ugUQEotZ1QdzQvcJUQ1dM= github.com/muesli/termenv v0.7.2 h1:r1raklL3uKE7rOvWgSenmEm2px+dnc33OTisZ8YR1fw=
github.com/muesli/termenv v0.5.3-0.20200617154346-da5de72b61c0/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI= github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=
github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03 h1:pd4YKIqCB0U7O2I4gWHgEUA2mCEOENmco0l/bM957bU= github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8=
github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03/go.mod h1:Z9+Ul5bCbBKnbCvdOWbLqTHhJiYV414CURZJba6L8qA= 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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/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-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-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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200620081246-981b61492c35 h1:wb/9mP8eUAmHfkM8RmpeLq6nUA7c2i5+bQOtcDftjaE= golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200620081246-981b61492c35/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= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -13,25 +13,26 @@ import (
// Type specifies the way we render pagination. // Type specifies the way we render pagination.
type Type int type Type int
// Pagination rendering options // Pagination rendering options.
const ( const (
Arabic Type = iota Arabic Type = iota
Dots Dots
) )
// Model is the Tea model for this user interface. // Model is the Bubble Tea model for this user interface.
type Model struct { type Model struct {
Type Type Type Type
Page int Page int
PerPage int PerPage int
TotalPages int TotalPages int
ActiveDot string ActiveDot string
InactiveDot string InactiveDot string
ArabicFormat string ArabicFormat string
UseLeftRightKeys bool UsePgUpPgDownKeys bool
UseUpDownKeys bool UseLeftRightKeys bool
UseHLKeys bool UseUpDownKeys bool
UseJKKeys bool UseHLKeys bool
UseJKKeys bool
} }
// SetTotalPages is a helper function for calculatng the total number of pages // SetTotalPages is a helper function for calculatng the total number of pages
@@ -39,8 +40,8 @@ type Model struct {
// used for other things beyond navigating sets. Note that it both returns the // used for other things beyond navigating sets. Note that it both returns the
// number of total pages and alters the model. // number of total pages and alters the model.
func (m *Model) SetTotalPages(items int) int { func (m *Model) SetTotalPages(items int) int {
if items == 0 { if items < 1 {
return 0 return m.TotalPages
} }
n := items / m.PerPage n := items / m.PerPage
if items%m.PerPage > 0 { if items%m.PerPage > 0 {
@@ -98,24 +99,33 @@ func (m Model) OnLastPage() bool {
// NewModel creates a new model with defaults. // NewModel creates a new model with defaults.
func NewModel() Model { func NewModel() Model {
return Model{ return Model{
Type: Arabic, Type: Arabic,
Page: 0, Page: 0,
PerPage: 1, PerPage: 1,
TotalPages: 1, TotalPages: 1,
ActiveDot: "•", ActiveDot: "•",
InactiveDot: "○", InactiveDot: "○",
ArabicFormat: "%d/%d", ArabicFormat: "%d/%d",
UseLeftRightKeys: true, UsePgUpPgDownKeys: true,
UseUpDownKeys: false, UseLeftRightKeys: true,
UseHLKeys: true, UseUpDownKeys: false,
UseJKKeys: false, UseHLKeys: true,
UseJKKeys: false,
} }
} }
// Update is the Tea update function which binds keystrokes to pagination. // Update is the Tea update function which binds keystrokes to pagination.
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
if m.UsePgUpPgDownKeys {
switch msg.String() {
case "pgup":
m.PrevPage()
case "pgdown":
m.NextPage()
}
}
if m.UseLeftRightKeys { if m.UseLeftRightKeys {
switch msg.String() { switch msg.String() {
case "left": case "left":
@@ -154,20 +164,16 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
} }
// View renders the pagination to a string. // View renders the pagination to a string.
func View(model tea.Model) string { func (m Model) View() string {
m, ok := model.(Model)
if !ok {
return "could not perform assertion on model"
}
switch m.Type { switch m.Type {
case Dots: case Dots:
return dotsView(m) return m.dotsView()
default: default:
return arabicView(m) return m.arabicView()
} }
} }
func dotsView(m Model) string { func (m Model) dotsView() string {
var s string var s string
for i := 0; i < m.TotalPages; i++ { for i := 0; i < m.TotalPages; i++ {
if i == m.Page { if i == m.Page {
@@ -179,7 +185,7 @@ func dotsView(m Model) string {
return s return s
} }
func arabicView(m Model) string { func (m Model) arabicView() string {
return fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages) return fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages)
} }

View File

@@ -7,25 +7,36 @@ import (
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
const defaultFPS = time.Second / 10
// Spinner is a set of frames used in animating the spinner. // Spinner is a set of frames used in animating the spinner.
type Spinner = int type Spinner = []string
// Available types of spinners
const (
Line Spinner = iota
Dot
)
const (
defaultFPS = 9
)
var ( var (
// Spinner frames // Some spinners to choose from. You could also make your own.
spinners = map[Spinner][]string{ Line = Spinner{"|", "/", "-", "\\"}
Line: {"|", "/", "-", "\\"}, Dot = Spinner{"⣾ ", "⣽ ", "⣻ ", "", "", "", "⣯ ", "⣷ "}
Dot: {"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", " ", " ", " "}, Globe = Spinner{"🌍 ", "🌎 ", "🌏 "}
} Moon = Spinner{"🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "}
Monkey = Spinner{"🙈 ", "🙈 ", "🙉 ", "🙊 "}
Jump = Spinner{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"}
Bit8 = Spinner{
"", "⠁", "⠂", "⠃", "⠄", "⠅", "⠆", "⠇", "⡀", "⡁", "⡂", "⡃", "⡄", "⡅", "⡆", "⡇",
"⠈", "⠉", "⠊", "⠋", "⠌", "⠍", "⠎", "⠏", "⡈", "⡉", "⡊", "⡋", "⡌", "⡍", "⡎", "⡏",
"⠐", "⠑", "⠒", "⠓", "⠔", "⠕", "⠖", "⠗", "⡐", "⡑", "⡒", "⡓", "⡔", "⡕", "⡖", "⡗",
"⠘", "⠙", "⠚", "⠛", "⠜", "⠝", "⠞", "⠟", "⡘", "⡙", "⡚", "⡛", "⡜", "⡝", "⡞", "⡟",
"⠠", "⠡", "⠢", "⠣", "⠤", "⠥", "⠦", "⠧", "⡠", "⡡", "⡢", "⡣", "⡤", "⡥", "⡦", "⡧",
"⠨", "⠩", "⠪", "⠫", "⠬", "⠭", "⠮", "⠯", "⡨", "⡩", "⡪", "⡫", "⡬", "⡭", "⡮", "⡯",
"⠰", "⠱", "⠲", "⠳", "⠴", "⠵", "⠶", "⠷", "⡰", "⡱", "⡲", "⡳", "⡴", "⡵", "⡶", "⡷",
"⠸", "⠹", "⠺", "⠻", "⠼", "⠽", "⠾", "⠿", "⡸", "⡹", "⡺", "⡻", "⡼", "⡽", "⡾", "⡿",
"⢀", "⢁", "⢂", "⢃", "⢄", "⢅", "⢆", "⢇", "⣀", "⣁", "⣂", "⣃", "⣄", "⣅", "⣆", "⣇",
"⢈", "⢉", "⢊", "⢋", "⢌", "⢍", "⢎", "⢏", "⣈", "⣉", "⣊", "⣋", "⣌", "⣍", "⣎", "⣏",
"⢐", "⢑", "⢒", "⢓", "⢔", "⢕", "⢖", "⢗", "⣐", "⣑", "⣒", "⣓", "⣔", "⣕", "⣖", "⣗",
"⢘", "⢙", "⢚", "⢛", "⢜", "⢝", "⢞", "⢟", "⣘", "⣙", "⣚", "⣛", "⣜", "⣝", "⣞", "⣟",
"⢠", "⢡", "⢢", "⢣", "⢤", "⢥", "⢦", "⢧", "⣠", "⣡", "⣢", "⣣", "⣤", "⣥", "⣦", "⣧",
"⢨", "⢩", "⢪", "⢫", "⢬", "⢭", "⢮", "⢯", "⣨", "⣩", "⣪", "⣫", "⣬", "⣭", "⣮", "⣯",
"⢰", "⢱", "⢲", "⢳", "⢴", "⢵", "⢶", "⢷", "⣰", "⣱", "⣲", "⣳", "⣴", "⣵", "⣶", "⣷",
"⢸", "⢹", "⢺", "⢻", "⢼", "⢽", "⢾", "⢿", "⣸", "⣹", "⣺", "⣻", "⣼", "⣽", "⣾", "⣿"}
color = termenv.ColorProfile().Color color = termenv.ColorProfile().Color
) )
@@ -35,10 +46,10 @@ var (
type Model struct { type Model struct {
// Type is the set of frames to use. See Spinner. // Type is the set of frames to use. See Spinner.
Type Spinner Frames Spinner
// FPS is the speed at which the ticker should tick // FPS is the speed at which the ticker should tick.
FPS int FPS time.Duration
// ForegroundColor sets the background color of the spinner. It can be a // ForegroundColor sets the background color of the spinner. It can be a
// hex code or one of the 256 ANSI colors. If the terminal emulator can't // hex code or one of the 256 ANSI colors. If the terminal emulator can't
@@ -52,67 +63,139 @@ type Model struct {
// (per github.com/muesli/termenv). // (per github.com/muesli/termenv).
BackgroundColor string BackgroundColor string
// CustomMsgFunc can be used to a custom message on tick. This can be // MinimumLifetime is the minimum amount of time the spinner can run. Any
// useful when you have spinners in different parts of your application and // logic around this can be implemented in view that implements this
// want to differentiate between the messages for clarity and simplicity. // spinner. If HideFor is set MinimumLifetime will be added on top of
// If nil, this setting is ignored. // HideFor. In other words, if HideFor is 100ms and MinimumLifetime is
CustomMsgFunc func() tea.Msg // 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
frame int // 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. // NewModel returns a model with default values.
func NewModel() Model { func NewModel() Model {
return Model{ return Model{
Type: Line, Frames: Line,
FPS: defaultFPS, FPS: defaultFPS,
} }
} }
// TickMsg indicates that the timer has ticked and we should render a frame. // TickMsg indicates that the timer has ticked and we should render a frame.
type TickMsg struct{} type TickMsg struct {
Time time.Time
}
type startTick struct{}
// Update is the Tea update function. This will advance the spinner one frame // 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 // every time it's called, regardless the message passed, so be sure the logic
// is setup so as not to call this Update needlessly. // is setup so as not to call this Update needlessly.
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.frame++ switch msg.(type) {
if m.frame >= len(spinners[m.Type]) { case startTick:
m.frame = 0 return m, m.tick()
case TickMsg:
m.frame++
if m.frame >= len(m.Frames) {
m.frame = 0
}
return m, m.tick()
default:
return m, nil
} }
if m.CustomMsgFunc != nil {
return m, Tick(m)
}
return m, Tick(m)
} }
// View renders the model's view. // View renders the model's view.
func View(model Model) string { func (m Model) View() string {
s := spinners[model.Type] if m.frame >= len(m.Frames) {
if model.frame >= len(s) { return "error"
return "[error]"
} }
str := s[model.frame] frame := m.Frames[m.frame]
if model.ForegroundColor != "" || model.BackgroundColor != "" { if m.ForegroundColor != "" || m.BackgroundColor != "" {
return termenv. return termenv.
String(str). String(frame).
Foreground(color(model.ForegroundColor)). Foreground(color(m.ForegroundColor)).
Background(color(model.BackgroundColor)). Background(color(m.BackgroundColor)).
String() String()
} }
return str return frame
} }
// Tick is the command used to advance the spinner one frame. // Tick is the command used to advance the spinner one frame.
func Tick(model Model) tea.Cmd { func Tick() tea.Msg {
return func() tea.Msg { return startTick{}
time.Sleep(time.Second / time.Duration(model.FPS)) }
if model.CustomMsgFunc != nil {
return model.CustomMsgFunc() func (m Model) tick() tea.Cmd {
} return tea.Tick(m.FPS, func(t time.Time) tea.Msg {
return TickMsg{} return TickMsg{
} Time: t,
}
})
} }

View File

@@ -1,40 +1,77 @@
package textinput package textinput
import ( import (
"context"
"strings" "strings"
"time" "time"
"unicode" "unicode"
"github.com/atotto/clipboard"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
rw "github.com/mattn/go-runewidth" rw "github.com/mattn/go-runewidth"
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
const defaultBlinkSpeed = time.Millisecond * 530
// color is a helper for returning colors.
var color func(s string) termenv.Color = termenv.ColorProfile().Color
// blinkMsg and blinkCanceled are used to manage cursor blinking.
type blinkMsg struct{}
type blinkCanceled struct{}
// Messages for clipboard events.
type pasteMsg string
type pasteErrMsg struct{ error }
// EchoMode sets the input behavior of the text input field.
type EchoMode int
const ( 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
) )
var ( // blinkCtx manages cursor blinking.
// color is a helper for returning colors type blinkCtx struct {
color func(s string) termenv.Color = termenv.ColorProfile().Color ctx context.Context
cancel context.CancelFunc
}
type cursorMode int
const (
cursorBlink = iota
cursorStatic
cursorHide
) )
// ErrMsg indicates there's been an error. We don't handle errors in the this // Model is the Bubble Tea model for this text input element.
// 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.
type Model struct { type Model struct {
Err error Err error
// General settings
Prompt string Prompt string
Placeholder string
Cursor string Cursor string
BlinkSpeed time.Duration BlinkSpeed time.Duration
Placeholder string
TextColor string TextColor string
BackgroundColor string BackgroundColor string
PlaceholderColor string PlaceholderColor string
CursorColor string CursorColor string
EchoMode EchoMode
EchoCharacter rune
// CharLimit is the maximum amount of characters this input element will // CharLimit is the maximum amount of characters this input element will
// accept. If 0 or less, there's no limit. // accept. If 0 or less, there's no limit.
@@ -45,23 +82,53 @@ type Model struct {
// viewport. If 0 or less this setting is ignored. // viewport. If 0 or less this setting is ignored.
Width int Width int
// Underlying text value // Underlying text value.
value []rune value []rune
// Focus indicates whether user input focus should be on this input // Focus indicates whether user input focus should be on this input
// component. When false, don't blink and ignore keyboard input. // component. When false, don't blink and ignore keyboard input.
focus bool focus bool
// Cursor blink state // Cursor blink state.
blink bool blink bool
// Cursor position // Cursor position.
pos int pos int
// Used to emulate a viewport when width is set and the content is // Used to emulate a viewport when width is set and the content is
// overflowing // overflowing.
offset int offset int
offsetRight int offsetRight int
// Used to manage cursor blink
blinkCtx *blinkCtx
// cursorMode determines the behavior of the cursor
cursorMode cursorMode
}
// NewModel creates a new model with default settings.
func NewModel() Model {
return Model{
Prompt: "> ",
Placeholder: "",
BlinkSpeed: defaultBlinkSpeed,
TextColor: "",
PlaceholderColor: "240",
CursorColor: "",
EchoCharacter: '*',
CharLimit: 0,
value: nil,
focus: false,
blink: true,
pos: 0,
cursorMode: cursorBlink,
blinkCtx: &blinkCtx{
ctx: context.Background(),
},
}
} }
// SetValue sets the value of the text input. // SetValue sets the value of the text input.
@@ -73,7 +140,7 @@ func (m *Model) SetValue(s string) {
m.value = runes m.value = runes
} }
if m.pos > len(m.value) { if m.pos > len(m.value) {
m.pos = len(m.value) m.SetCursor(len(m.value))
} }
m.handleOverflow() m.handleOverflow()
} }
@@ -83,23 +150,30 @@ func (m Model) Value() string {
return string(m.value) return string(m.value)
} }
// Cursor start moves the cursor to the given position. If the position is out // SetCursor start moves the cursor to the given position. If the position is
// of bounds the cursor will be moved to the start or end accordingly. // out of bounds the cursor will be moved to the start or end accordingly.
func (m *Model) SetCursor(pos int) { // Returns whether or nor the cursor timer should be reset.
func (m *Model) SetCursor(pos int) bool {
m.pos = clamp(pos, 0, len(m.value)) m.pos = clamp(pos, 0, len(m.value))
m.handleOverflow() m.handleOverflow()
// Show the cursor unless it's been explicitly hidden
m.blink = m.cursorMode == cursorHide
// Reset cursor blink if necessary
return m.cursorMode == cursorBlink
} }
// CursorStart moves the cursor to the start of the field. // CursorStart moves the cursor to the start of the field. Returns whether or
func (m *Model) CursorStart() { // not the curosr blink should be reset.
m.pos = 0 func (m *Model) CursorStart() bool {
m.handleOverflow() return m.SetCursor(0)
} }
// CursorEnd moves the cursor to the end of the field. // CursorEnd moves the cursor to the end of the field. Returns whether or not
func (m *Model) CursorEnd() { // the cursor blink should be reset.
m.pos = len(m.value) func (m *Model) CursorEnd() bool {
m.handleOverflow() return m.SetCursor(len(m.value))
} }
// Focused returns the focus state on the model. // Focused returns the focus state on the model.
@@ -110,7 +184,7 @@ func (m Model) Focused() bool {
// Focus sets the focus state on the model. // Focus sets the focus state on the model.
func (m *Model) Focus() { func (m *Model) Focus() {
m.focus = true m.focus = true
m.blink = false m.blink = m.cursorMode == cursorHide // show the cursor unless we've explicitly hidden it
} }
// Blur removes the focus state on the model. // Blur removes the focus state on the model.
@@ -119,11 +193,57 @@ func (m *Model) Blur() {
m.blink = true m.blink = true
} }
// Reset sets the input to its default state with no input. // Reset sets the input to its default state with no input. Returns whether
func (m *Model) Reset() { // or not the cursor blink should reset.
func (m *Model) Reset() bool {
m.value = nil m.value = nil
m.pos = 0 return m.SetCursor(0)
m.blink = false }
// handle a clipboard paste event, if supported. Returns whether or not the
// cursor blink should be reset.
func (m *Model) handlePaste(v string) (blink bool) {
paste := []rune(v)
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 if necessary and run overflow checks
return m.SetCursor(m.pos)
} }
// If a max width is defined, perform some logic to treat the visible area // If a max width is defined, perform some logic to treat the visible area
@@ -139,7 +259,6 @@ func (m *Model) handleOverflow() {
m.offsetRight = min(m.offsetRight, len(m.value)) m.offsetRight = min(m.offsetRight, len(m.value))
if m.pos < m.offset { if m.pos < m.offset {
m.offset = m.pos m.offset = m.pos
w := 0 w := 0
@@ -154,9 +273,7 @@ func (m *Model) handleOverflow() {
} }
m.offsetRight = m.offset + i m.offsetRight = m.offset + i
} else if m.pos >= m.offsetRight { } else if m.pos >= m.offsetRight {
m.offsetRight = m.pos m.offsetRight = m.pos
w := 0 w := 0
@@ -171,7 +288,6 @@ func (m *Model) handleOverflow() {
} }
m.offset = m.offsetRight - (len(runes) - 1 - i) m.offset = m.offsetRight - (len(runes) - 1 - i)
} }
} }
@@ -195,7 +311,76 @@ func (m *Model) colorPlaceholder(s string) string {
String() String()
} }
func (m *Model) wordLeft() { // deleteWordLeft deletes the word left to the cursor. Returns whether or not
// the cursor blink should be reset.
func (m *Model) deleteWordLeft() (blink bool) {
if m.pos == 0 || len(m.value) == 0 {
return
}
i := m.pos
blink = m.SetCursor(m.pos - 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace before cursor
blink = m.SetCursor(m.pos - 1)
}
for m.pos > 0 {
if !unicode.IsSpace(m.value[m.pos]) {
blink = m.SetCursor(m.pos - 1)
} else {
if m.pos > 0 {
// keep the previous space
blink = 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:]...)
}
return
}
// deleteWordRight deletes the word right to the cursor. Returns whether or not
// the cursor blink should be reset.
func (m *Model) deleteWordRight() (blink bool) {
if m.pos >= len(m.value) || len(m.value) == 0 {
return
}
i := m.pos
blink = m.SetCursor(m.pos + 1)
for unicode.IsSpace(m.value[m.pos]) {
// ignore series of whitespace after cursor
blink = m.SetCursor(m.pos + 1)
}
for m.pos < len(m.value) {
if !unicode.IsSpace(m.value[m.pos]) {
blink = 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:]...)
}
blink = m.SetCursor(i)
return
}
// wordLeft moves the cursor one word to the left. Returns whether or not the
// cursor blink should be reset.
func (m *Model) wordLeft() (blink bool) {
if m.pos == 0 || len(m.value) == 0 { if m.pos == 0 || len(m.value) == 0 {
return return
} }
@@ -204,7 +389,7 @@ func (m *Model) wordLeft() {
for i >= 0 { for i >= 0 {
if unicode.IsSpace(m.value[i]) { if unicode.IsSpace(m.value[i]) {
m.pos-- blink = m.SetCursor(m.pos - 1)
i-- i--
} else { } else {
break break
@@ -213,15 +398,19 @@ func (m *Model) wordLeft() {
for i >= 0 { for i >= 0 {
if !unicode.IsSpace(m.value[i]) { if !unicode.IsSpace(m.value[i]) {
m.pos-- blink = m.SetCursor(m.pos - 1)
i-- i--
} else { } else {
break break
} }
} }
return
} }
func (m *Model) wordRight() { // wordRight moves the cursor one word to the right. Returns whether or not the
// cursor blink should be reset.
func (m *Model) wordRight() (blink bool) {
if m.pos >= len(m.value) || len(m.value) == 0 { if m.pos >= len(m.value) || len(m.value) == 0 {
return return
} }
@@ -229,8 +418,8 @@ func (m *Model) wordRight() {
i := m.pos i := m.pos
for i < len(m.value) { for i < len(m.value) {
if unicode.IsSpace(rune(m.value[i])) { if unicode.IsSpace(m.value[i]) {
m.pos++ blink = m.SetCursor(m.pos + 1)
i++ i++
} else { } else {
break break
@@ -238,143 +427,153 @@ func (m *Model) wordRight() {
} }
for i < len(m.value) { for i < len(m.value) {
if !unicode.IsSpace(rune(m.value[i])) { if !unicode.IsSpace(m.value[i]) {
m.pos++ blink = m.SetCursor(m.pos + 1)
i++ i++
} else { } else {
break break
} }
} }
return
} }
// BlinkMsg is sent when the cursor should alternate it's blinking state. func (m Model) echoTransform(v string) string {
type BlinkMsg struct{} switch m.EchoMode {
case EchoPassword:
return strings.Repeat(string(m.EchoCharacter), rw.StringWidth(v))
case EchoNone:
return ""
// NewModel creates a new model with default settings. default:
func NewModel() Model { return v
return Model{
Prompt: "> ",
BlinkSpeed: defaultBlinkSpeed,
Placeholder: "",
TextColor: "",
PlaceholderColor: "240",
CursorColor: "",
CharLimit: 0,
value: nil,
focus: false,
blink: true,
pos: 0,
} }
} }
// Update is the Tea update loop. // Update is the Bubble Tea update loop.
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if !m.focus { if !m.focus {
m.blink = true m.blink = true
return m, nil return m, nil
} }
var resetBlink bool
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.Type { switch msg.Type {
case tea.KeyBackspace: case tea.KeyBackspace: // delete character before cursor
fallthrough if msg.Alt {
case tea.KeyDelete: resetBlink = m.deleteWordLeft()
if len(m.value) > 0 { } else {
m.value = append(m.value[:m.pos-1], m.value[m.pos:]...) if len(m.value) > 0 {
m.pos-- m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
if m.pos > 0 {
resetBlink = m.SetCursor(m.pos - 1)
}
}
} }
case tea.KeyLeft: case tea.KeyLeft, tea.KeyCtrlB:
if msg.Alt { // alt+left arrow, back one word if msg.Alt { // alt+left arrow, back one word
m.wordLeft() resetBlink = m.wordLeft()
break break
} }
if m.pos > 0 { if m.pos > 0 { // left arrow, ^F, back one character
m.pos-- resetBlink = m.SetCursor(m.pos - 1)
} }
case tea.KeyRight: case tea.KeyRight, tea.KeyCtrlF:
if msg.Alt { // alt+right arrow, forward one word if msg.Alt { // alt+right arrow, forward one word
m.wordRight() resetBlink = m.wordRight()
break break
} }
if m.pos < len(m.value) { if m.pos < len(m.value) { // right arrow, ^F, forward one word
m.pos++ resetBlink = m.SetCursor(m.pos + 1)
} }
case tea.KeyCtrlF: // ^F, forward one character case tea.KeyCtrlW: // ^W, delete word left of cursor
fallthrough resetBlink = m.deleteWordLeft()
case tea.KeyCtrlB: // ^B, back one charcter case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning
fallthrough resetBlink = m.CursorStart()
case tea.KeyCtrlA: // ^A, go to beginning case tea.KeyDelete, tea.KeyCtrlD: // ^D, delete char under cursor
m.CursorStart()
case tea.KeyCtrlD: // ^D, delete char under cursor
if len(m.value) > 0 && m.pos < len(m.value) { if len(m.value) > 0 && m.pos < len(m.value) {
m.value = append(m.value[:m.pos], m.value[m.pos+1:]...) 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() resetBlink = m.CursorEnd()
case tea.KeyCtrlK: // ^K, kill text after cursor case tea.KeyCtrlK: // ^K, kill text after cursor
m.value = m.value[:m.pos] m.value = m.value[:m.pos]
m.pos = len(m.value) resetBlink = m.SetCursor(len(m.value))
case tea.KeyCtrlU: // ^U, kill text before cursor case tea.KeyCtrlU: // ^U, kill text before cursor
m.value = m.value[m.pos:] m.value = m.value[m.pos:]
m.pos = 0 resetBlink = m.SetCursor(0)
m.offset = 0 m.offset = 0
case tea.KeyRune: // input a regular character case tea.KeyCtrlV: // ^V paste
return m, Paste
if msg.Alt { case tea.KeyRunes: // input regular characters
if msg.Rune == 'b' { // alt+b, back one word if msg.Alt && len(msg.Runes) == 1 {
m.wordLeft() if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor
resetBlink = m.deleteWordRight()
break break
} }
if msg.Rune == 'f' { // alt+f, forward one word if msg.Runes[0] == 'b' { // alt+b, back one word
m.wordRight() resetBlink = m.wordLeft()
break
}
if msg.Runes[0] == 'f' { // alt+f, forward one word
resetBlink = m.wordRight()
break break
} }
} }
// Input a regular character // Input a regular character
if m.CharLimit <= 0 || len(m.value) < m.CharLimit { 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.value = append(m.value[:m.pos], append(msg.Runes, m.value[m.pos:]...)...)
m.pos++ resetBlink = m.SetCursor(m.pos + len(msg.Runes))
} }
} }
case ErrMsg: case blinkMsg:
m.Err = msg var cmd tea.Cmd
if m.cursorMode == cursorBlink {
m.blink = !m.blink
cmd = m.blinkCmd()
}
return m, cmd
case BlinkMsg: case blinkCanceled: // no-op
m.blink = !m.blink return m, nil
return m, Blink(m)
case pasteMsg:
resetBlink = m.handlePaste(string(msg))
case pasteErrMsg:
m.Err = msg
}
var cmd tea.Cmd
if resetBlink {
cmd = m.blinkCmd()
} }
m.handleOverflow() m.handleOverflow()
return m, cmd
return m, nil
} }
// View renders the textinput in its current state. // View renders the textinput in its current state.
func View(model tea.Model) string { func (m Model) View() string {
m, ok := model.(Model)
if !ok {
return "could not perform assertion on model"
}
// Placeholder text // Placeholder text
if m.value == nil && m.Placeholder != "" { if len(m.value) == 0 && m.Placeholder != "" {
return placeholderView(m) return m.placeholderView()
} }
value := m.value[m.offset:m.offsetRight] value := m.value[m.offset:m.offsetRight]
pos := m.pos - m.offset pos := max(0, m.pos-m.offset)
v := m.colorText(m.echoTransform(string(value[:pos])))
v := m.colorText(string(value[:pos]))
if pos < len(value) { if pos < len(value) {
v += cursorView(string(value[pos]), m) // cursor and text under it v += m.cursorView(m.echoTransform(string(value[pos]))) // cursor and text under it
v += m.colorText(string(value[pos+1:])) // text after cursor v += m.colorText(m.echoTransform(string(value[pos+1:]))) // text after cursor
} else { } else {
v += cursorView(" ", m) v += m.cursorView(" ")
} }
// If a max width and background color were set fill the empty spaces with // If a max width and background color were set fill the empty spaces with
@@ -394,8 +593,8 @@ func View(model tea.Model) string {
return m.Prompt + v return m.Prompt + v
} }
// placeholderView // placeholderView returns the prompt and placeholder view, if any.
func placeholderView(m Model) string { func (m Model) placeholderView() string {
var ( var (
v string v string
p = m.Placeholder p = m.Placeholder
@@ -403,12 +602,9 @@ func placeholderView(m Model) string {
// Cursor // Cursor
if m.blink && m.PlaceholderColor != "" { if m.blink && m.PlaceholderColor != "" {
v += cursorView( v += m.cursorView(m.colorPlaceholder(p[:1]))
m.colorPlaceholder(p[:1]),
m,
)
} else { } else {
v += cursorView(p[:1], m) v += m.cursorView(p[:1])
} }
// The rest of the placeholder text // The rest of the placeholder text
@@ -418,29 +614,54 @@ func placeholderView(m Model) string {
} }
// cursorView styles the cursor. // cursorView styles the cursor.
func cursorView(s string, m Model) string { func (m Model) cursorView(v string) string {
if m.blink { if m.blink {
if m.TextColor != "" || m.BackgroundColor != "" { if m.TextColor != "" || m.BackgroundColor != "" {
return termenv.String(s). return termenv.String(v).
Foreground(color(m.TextColor)). Foreground(color(m.TextColor)).
Background(color(m.BackgroundColor)). Background(color(m.BackgroundColor)).
String() String()
} }
return s return v
} }
return termenv.String(s). return termenv.String(v).
Foreground(color(m.CursorColor)). Foreground(color(m.CursorColor)).
Background(color(m.BackgroundColor)). Background(color(m.BackgroundColor)).
Reverse(). Reverse().
String() String()
} }
// Blink is a command used to time the cursor blinking. // blinkCmd is an internal command used to manage cursor blinking.
func Blink(model Model) tea.Cmd { func (m Model) blinkCmd() tea.Cmd {
return func() tea.Msg { if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
time.Sleep(model.BlinkSpeed) m.blinkCtx.cancel()
return BlinkMsg{}
} }
ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed)
m.blinkCtx.cancel = cancel
return func() tea.Msg {
defer cancel()
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
return blinkMsg{}
}
return blinkCanceled{}
}
}
// Blink is a command used to initialize cursor blinking.
func Blink() tea.Msg {
return blinkMsg{}
}
// Paste is a command for pasting from the clipboard into the text input.
func Paste() tea.Msg {
str, err := clipboard.ReadAll()
if err != nil {
return pasteErrMsg{err}
}
return pasteMsg(str)
} }
func clamp(v, low, high int) int { func clamp(v, low, high int) int {

View File

@@ -7,6 +7,8 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
const spacebar = " "
// MODEL // MODEL
type Model struct { type Model struct {
@@ -33,29 +35,21 @@ type Model struct {
lines []string 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. // AtTop returns whether or not the viewport is in the very top position.
func (m Model) AtTop() bool { func (m Model) AtTop() bool {
return m.YOffset <= 0 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 { 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. // Scrollpercent returns the amount scrolled as a float between 0 and 1.
@@ -65,7 +59,7 @@ func (m Model) ScrollPercent() float64 {
} }
y := float64(m.YOffset) y := float64(m.YOffset)
h := float64(m.Height) h := float64(m.Height)
t := float64(len(m.lines)) t := float64(len(m.lines) - 1)
v := y / (t - h) v := y / (t - h)
return math.Max(0.0, math.Min(1.0, v)) return math.Max(0.0, math.Min(1.0, v))
} }
@@ -77,11 +71,11 @@ func (m *Model) SetContent(s string) {
m.lines = strings.Split(s, "\n") 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) { func (m Model) visibleLines() (lines []string) {
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(0, m.YOffset) 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] lines = m.lines[top:bottom]
} }
return lines return lines
@@ -95,8 +89,8 @@ func (m *Model) ViewDown() []string {
} }
m.YOffset = min( m.YOffset = min(
m.YOffset+m.Height, // target m.YOffset+m.Height, // target
len(m.lines)-m.Height, // fallback len(m.lines)-1-m.Height, // fallback
) )
return m.visibleLines() return m.visibleLines()
@@ -123,13 +117,13 @@ func (m *Model) HalfViewDown() (lines []string) {
} }
m.YOffset = min( m.YOffset = min(
m.YOffset+m.Height/2, // target m.YOffset+m.Height/2, // target
len(m.lines)-m.Height, // fallback len(m.lines)-1-m.Height, // fallback
) )
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(m.YOffset+m.Height/2, 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] lines = m.lines[top:bottom]
} }
@@ -149,27 +143,33 @@ func (m *Model) HalfViewUp() (lines []string) {
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(m.YOffset, 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] lines = m.lines[top:bottom]
} }
return lines 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) { func (m *Model) LineDown(n int) (lines []string) {
if m.AtBottom() || n == 0 { if m.AtBottom() || n == 0 {
return nil 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 = min(
m.YOffset+n, // target m.YOffset+n, // target
len(m.lines)-m.Height, // fallback len(m.lines)-1-m.Height, // fallback
) )
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(0, m.YOffset+m.Height-n) top := max(m.YOffset+m.Height-n, 0)
bottom := min(len(m.lines)-1, m.YOffset+m.Height) bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom] lines = m.lines[top:bottom]
} }
@@ -183,11 +183,45 @@ func (m *Model) LineUp(n int) (lines []string) {
return nil 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) m.YOffset = max(m.YOffset-n, 0)
if len(m.lines) > 0 { if len(m.lines) > 0 {
top := max(0, m.YOffset) 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] lines = m.lines[top:bottom]
} }
@@ -206,8 +240,10 @@ func Sync(m Model) tea.Cmd {
return nil return nil
} }
// TODO: we should probably use m.visibleLines() rather than these two
// expressions.
top := max(m.YOffset, 0) 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( return tea.SyncScrollArea(
m.lines[top:bottom], m.lines[top:bottom],
@@ -216,9 +252,9 @@ func Sync(m Model) tea.Cmd {
) )
} }
// ViewDown is a high performance command that moves the viewport up by one // ViewDown is a high performance command that moves the viewport up by a given
// viewport height. Use Model.ViewDown to get the lines that should be // numer of lines. Use Model.ViewDown to get the lines that should be rendered.
// rendered. For example: // For example:
// //
// lines := model.ViewDown(1) // lines := model.ViewDown(1)
// cmd := ViewDown(m, lines) // cmd := ViewDown(m, lines)
@@ -230,8 +266,8 @@ func ViewDown(m Model, lines []string) tea.Cmd {
return tea.ScrollDown(lines, m.YPosition, m.YPosition+m.Height) return tea.ScrollDown(lines, m.YPosition, m.YPosition+m.Height)
} }
// ViewUp is a high performance command the moves the viewport down by one // ViewUp is a high performance command the moves the viewport down by a given
// viewport height. Use Model.ViewDown to get the lines that should be // number of lines height. Use Model.ViewUp to get the lines that should be
// rendered. // rendered.
func ViewUp(m Model, lines []string) tea.Cmd { func ViewUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 { if len(lines) == 0 {
@@ -240,108 +276,72 @@ func ViewUp(m Model, lines []string) tea.Cmd {
return tea.ScrollUp(lines, m.YPosition, m.YPosition+m.Height) 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
// Update runs the update loop with default keybindings similar to popular // Update runs the update loop with default keybindings similar to popular
// pagers. To define your own keybindings use the methods on Model (i.e. // pagers. To define your own keybindings use the methods on Model (i.e.
// Model.LineDown()) and define your own update function. // Model.LineDown()) and define your own update function.
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch msg.String() { switch msg.String() {
// Down one page // Down one page
case "pgdown": case "pgdown", spacebar, "f":
fallthrough
case " ": // spacebar
fallthrough
case "f":
lines := m.ViewDown() lines := m.ViewDown()
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = ViewDown(m, lines) cmd = ViewDown(m, lines)
} }
// Up one page // Up one page
case "pgup": case "pgup", "b":
fallthrough
case "b":
lines := m.ViewUp() lines := m.ViewUp()
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = ViewUp(m, lines) cmd = ViewUp(m, lines)
} }
// Down half page // Down half page
case "d": case "d", "ctrl+d":
lines := m.HalfViewDown() lines := m.HalfViewDown()
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = HalfViewDown(m, lines) cmd = ViewDown(m, lines)
} }
// Up half page // Up half page
case "u": case "u", "ctrl+u":
lines := m.HalfViewUp() lines := m.HalfViewUp()
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = HalfViewUp(m, lines) cmd = ViewUp(m, lines)
} }
// Down one line // Down one line
case "down": case "down", "j":
fallthrough
case "j":
lines := m.LineDown(1) lines := m.LineDown(1)
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = LineDown(m, lines) cmd = ViewDown(m, lines)
} }
// Up one line // Up one line
case "up": case "up", "k":
fallthrough
case "k":
lines := m.LineUp(1) lines := m.LineUp(1)
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
cmd = LineUp(m, lines) cmd = ViewUp(m, lines)
}
}
case tea.MouseMsg:
switch msg.Type {
case tea.MouseWheelUp:
lines := m.LineUp(3)
if m.HighPerformanceRendering {
cmd = ViewUp(m, lines)
}
case tea.MouseWheelDown:
lines := m.LineDown(3)
if m.HighPerformanceRendering {
cmd = ViewDown(m, lines)
} }
} }
} }
@@ -352,8 +352,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
// VIEW // VIEW
// View renders the viewport into a string. // View renders the viewport into a string.
func View(m Model) string { func (m Model) View() string {
if m.HighPerformanceRendering { if m.HighPerformanceRendering {
// Just send newlines since we're doing to be rendering the actual // Just send newlines since we're doing to be rendering the actual
// content seprately. We still need send something that equals the // content seprately. We still need send something that equals the
@@ -375,6 +374,10 @@ func View(m Model) string {
// ETC // ETC
func clamp(v, low, high int) int {
return min(high, max(low, v))
}
func min(a, b int) int { func min(a, b int) int {
if a < b { if a < b {
return a return a
@@ -388,7 +391,3 @@ func max(a, b int) int {
} }
return b return b
} }
func clamp(val, low, high int) int {
return max(low, min(high, val))
}