105 Commits

Author SHA1 Message Date
Christian Rocha
9449cc7e41 Don't allow word-based deletion when input is masked in textinput
alt+d and ctrl+w will now delete all the way to the beginning and end,
respectively, if EchoMode is not EchoNormal.
2021-04-26 12:03:57 -04:00
Christian Rocha
58a177394e Don't allow word-to-word movement when input is masked in textinput
When EchoMode is not set to EchoNormal, alt+left/alt+b and
alt+right/alt+b jumps to the beginning and the end, respectively, so as
not to reveal word breaks in the masked text.
2021-04-26 12:03:57 -04:00
Christian Rocha
f016c31d83 Add method for returning cursor position in textinput
Previously, Cursor was a member on the model that did absolutely
nothing.
2021-04-26 12:03:57 -04:00
Christian Rocha
74436326b9 Public cursor movement functions no longer return values in textinput
Previously, textinput.SetCursor, textinput.CursotStart, and
textinput.CursorEnd returned bools used interally for managing cursor
blinking. Those methods have been replaced with private counterparts.
2021-04-26 12:03:57 -04:00
Christian Rocha
158097df66 Keep y-offset in bounds when setting content 2021-04-19 19:47:50 -04:00
Christian Rocha
0d5d7e5acd Remove deadcode + small comment on Lip Gloss 2021-04-13 17:09:23 -04:00
Christian Rocha
ddd48d9ab7 TextInput now uses Lip Gloss for styling 2021-04-13 17:09:23 -04:00
Christian Rocha
7242bbe8dc Spinner now uses Lip Gloss for styling 2021-04-13 17:09:23 -04:00
Christian Muehlhaeuser
9e324205c2 Bump bubbletea, termenv, reflow, go-runewidth, and go-colorful deps 2021-04-03 01:56:59 +02:00
Christian Muehlhaeuser
b2f42066a2 Drop naked returns in textinput model
This actually uncovered a few ineffective assignments, particularly
in deleteWordRight.
2021-03-11 20:09:32 -05:00
Christian Muehlhaeuser
a0d7cb77a1 Fix godocs for various types and functions 2021-03-12 02:06:30 +01:00
Christian Muehlhaeuser
046b9ca129 Enable golint as linter 2021-03-12 02:00:32 +01:00
Christian Muehlhaeuser
da9a4049de Mark innocuous numbers as nolint 2021-03-12 01:47:40 +01:00
Christian Muehlhaeuser
490a599c05 Use a const for the mouse wheel delta in viewport 2021-03-12 01:33:51 +01:00
Kiyon
f719cc8cb1 Reset cursor in SetValue when initial pos is 0 2021-03-09 10:21:17 -05:00
Christian Rocha
c8f4855d20 Fix typo in README 2021-01-12 18:19:20 -05:00
Christian Rocha
43d43d14ae A external link to progress example and remove local example 2021-01-12 18:17:42 -05:00
Christian Rocha
de359c53bb README: improve wording + image update for dark backgrounds 2021-01-12 17:53:56 -05:00
Christian Rocha
c7d69b61f0 Add progressbar info and GIF to README 2021-01-12 17:27:14 -05:00
Christian Rocha
11f56f9b6b If using advanced settings, draw empty spinner when appropriate
When model.HideFor or model.MinimumLifetime is present, we won't draw
the spinner in cases where model.Visibe() returns false.
2020-12-11 19:37:37 -05:00
Christian Rocha
3be2d0585b Prevent spinner from responding to extra, unintentional tick messages 2020-12-11 19:37:37 -05:00
Christian Rocha
37a85afcd1 Fix a typo in a comment 2020-12-10 11:36:22 -05:00
Christian Rocha
f9c79eef64 Slightly more subtle default gradient 2020-12-01 17:03:48 -05:00
Christian Rocha
c303de1e85 Replace "ramp" with "gradient" on exposed functions 2020-12-01 17:03:48 -05:00
Christian Rocha
7e5ef42924 Add ability to style percentage readout 2020-12-01 17:03:48 -05:00
Christian Rocha
79ca4d09d1 Fix doc comments and remove magic number 2020-12-01 17:03:48 -05:00
Christian Rocha
f48e53556a Return an error if we could not initialize a new gradient ramp 2020-12-01 17:03:48 -05:00
Christian Rocha
5d6d8cb0fb Add functional argument for setting the width of the progress bar 2020-12-01 17:03:48 -05:00
Christian Rocha
47b8d9c6a8 Doc comments, optimizations, and magic number removal 2020-12-01 17:03:48 -05:00
Christian Rocha
b78277e7ec Add progress bar example 2020-12-01 17:03:48 -05:00
Christian Rocha
6a768905a6 Add initialization options for scaled ramps 2020-12-01 17:03:48 -05:00
Christian Rocha
0f06d78b92 Support for solid (non-gradient) fills too 2020-12-01 17:03:48 -05:00
Christian Rocha
1e16eca939 Set ramp in NewModel via functional argument 2020-12-01 17:03:48 -05:00
Christian Rocha
d81e5713d4 Cache termenv color profile lookup 2020-12-01 17:03:48 -05:00
Christian Rocha
b10dbcb4dd Add option to show percent 2020-12-01 17:03:48 -05:00
Christian Rocha
a2e0a2e72e Remove initial width argument; add default width 2020-12-01 17:03:48 -05:00
Christian Rocha
3dea7d036e Reduce progress component to (more or less) a pure view function
We really only need the View function in this case since the progress
value will come from the program that implements this.
2020-12-01 17:03:48 -05:00
Richard Cooper
68e87e08c5 gofmt-ed last commit 2020-12-01 17:03:48 -05:00
Richard Cooper
65a17d039e length field changed to width 2020-12-01 17:03:48 -05:00
ololosha228
38d59517fb Added progress bar 2020-12-01 17:03:48 -05:00
Christian Rocha
06109f45ce Add Bubbles title treatment to README 2020-11-19 19:11:24 -05:00
Christian Rocha
9cb8e8d90a Remove spaces after emoji spinners 2020-11-11 15:39:42 -05:00
Christian Rocha
e6219572e5 Add points spinner 2020-11-11 15:39:42 -05:00
Christian Rocha
81feaacf5b Remove bit8 spinner 2020-11-11 15:39:42 -05:00
Christian Rocha
9a25d8b8b9 Add pulse spinner 2020-11-11 15:39:42 -05:00
Christian Rocha
e8e052c64b Add mini dot spinner 2020-11-11 15:39:42 -05:00
Christian Rocha
3d7cd43046 Rework Spinner so that default spinners have FPS built-in 2020-11-11 15:39:42 -05:00
Christian Rocha
9c38e101d2 Remove internal msg in spinner so all tick msgs can be caught externally 2020-11-10 15:44:42 -05:00
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
12 changed files with 1067 additions and 329 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

27
.golangci.yml Normal file
View File

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

View File

@@ -1,34 +1,94 @@
# Bubbles
Bubbles
=======
Some components for [Bubble Tea](https://github.com/charmbraclet/bubbletea):
<p>
<img src="https://stuff.charm.sh/bubbles/bubbles-github.png" width="233" alt="The Bubbles Logo">
</p>
[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)
[![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions)
[![Go ReportCard](http://goreportcard.com/badge/charmbracelet/bubbles)](http://goreportcard.com/report/charmbracelet/bubbles)
Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea) applications.
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
* Text Input
* Paginator
* Viewport
## Spinner
A spinner, useful for indicating that some kind of invisible operation is
happening. There are a couple default ones, but you can also pass your own
”frames.”
<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
A text input field, akin to an `<input type="text">` in HTML.
<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)
## Progress
<img src="https://stuff.charm.sh/bubbles-examples/progress.gif" width="800" alt="Progressbar Example">
A simple, customizable progress meter. Supports solid and gradient fills. The
empty and filled runes can be set to whatever you'd like. The percentage readout
is customizable and can also be omitted entirely.
* [Example code](https://github.com/charmbracelet/bubbletea/blob/master/examples/progress/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
A viewport for vertically scrolling content which optionally includes standard
<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. This is
generally only necessary when dealing with content with a very large amount of
ANSI escape sequences.
for applications which make use of the alternate 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
## 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-unrounded.jpg" width="400"></a>
Charm热爱开源! / Charm loves open source!

15
go.mod
View File

@@ -3,11 +3,12 @@ module github.com/charmbracelet/bubbles
go 1.13
require (
github.com/charmbracelet/bubbletea v0.9.1-0.20200713153904-2f53eeb54b90
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/atotto/clipboard v0.1.2
github.com/charmbracelet/bubbletea v0.13.1
github.com/charmbracelet/lipgloss v0.1.2
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/mattn/go-runewidth v0.0.12
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68
github.com/muesli/termenv v0.8.1
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
)
replace github.com/charmbracelet/bubbletea => ../bubbletea

45
go.sum
View File

@@ -1,26 +1,41 @@
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
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.13.1 h1:huvX8mPaeMZ8DLulT50iEWRF+iitY5FNEDqDVLu69nM=
github.com/charmbracelet/bubbletea v0.13.1/go.mod h1:tp9tr9Dadh0PLhgiwchE5zZJXm5543JYjHG9oY+5qSg=
github.com/charmbracelet/lipgloss v0.1.2 h1:D+LUMg34W7n2pkuMrevKVxT7HXqnoRHm7IoomkX3/ZU=
github.com/charmbracelet/lipgloss v0.1.2/go.mod h1:5D8zradw52m7QmxRF6QgwbwJi9je84g8MkWiGN07uKg=
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/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/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
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/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=
github.com/muesli/termenv v0.8.1 h1:9q230czSP3DHVpkaPDXGp0TOfAwyjyYwXlUCQxQSaBk=
github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -1,4 +1,4 @@
// package paginator provides a Bubble Tea package for calulating pagination
// Package paginator provides a Bubble Tea package for calulating pagination
// and rendering pagination info. Note that this package does not render actual
// pages: it's purely for handling keystrokes related to pagination, and
// rendering pagination status.
@@ -13,13 +13,13 @@ import (
// Type specifies the way we render pagination.
type Type int
// Pagination rendering options
// Pagination rendering options.
const (
Arabic Type = iota
Dots
)
// Model is the Tea model for this user interface.
// Model is the Bubble Tea model for this user interface.
type Model struct {
Type Type
Page int
@@ -40,8 +40,8 @@ type Model struct {
// used for other things beyond navigating sets. Note that it both returns the
// number of total pages and alters the model.
func (m *Model) SetTotalPages(items int) int {
if items == 0 {
return 0
if items < 1 {
return m.TotalPages
}
n := items / m.PerPage
if items%m.PerPage > 0 {
@@ -91,7 +91,7 @@ func (m *Model) NextPage() {
}
}
// LastPage returns whether or not we're on the last page.
// OnLastPage returns whether or not we're on the last page.
func (m Model) OnLastPage() bool {
return m.Page == m.TotalPages-1
}
@@ -115,7 +115,7 @@ func NewModel() Model {
}
// 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) {
case tea.KeyMsg:
if m.UsePgUpPgDownKeys {
@@ -164,20 +164,16 @@ 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 (m Model) View() string {
switch m.Type {
case Dots:
return dotsView(m)
return m.dotsView()
default:
return arabicView(m)
return m.arabicView()
}
}
func dotsView(m Model) string {
func (m Model) dotsView() string {
var s string
for i := 0; i < m.TotalPages; i++ {
if i == m.Page {
@@ -189,7 +185,7 @@ func dotsView(m Model) string {
return s
}
func arabicView(m Model) string {
func (m Model) arabicView() string {
return fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages)
}

192
progress/progress.go Normal file
View File

@@ -0,0 +1,192 @@
package progress
import (
"fmt"
"strings"
"github.com/lucasb-eyer/go-colorful"
"github.com/muesli/reflow/ansi"
"github.com/muesli/termenv"
)
const defaultWidth = 40
var color func(string) termenv.Color = termenv.ColorProfile().Color
// Option is used to set options in NewModel. For example:
//
// progress := NewModel(
// WithRamp("#ff0000", "#0000ff"),
// WithoutPercentage(),
// )
type Option func(*Model) error
// WithDefaultGradient sets a gradient fill with default colors.
func WithDefaultGradient() Option {
return WithGradient("#5A56E0", "#EE6FF8")
}
// WithGradient sets a gradient fill blending between two colors.
func WithGradient(colorA, colorB string) Option {
return func(m *Model) error {
return m.setRamp(colorA, colorB, false)
}
}
// WithDefaultScaledGradient sets a gradient with default colors, and scales the
// gradient to fit the filled portion of the ramp.
func WithDefaultScaledGradient() Option {
return WithScaledGradient("#5A56E0", "#EE6FF8")
}
// WithScaledGradient scales the gradient to fit the width of the filled portion of
// the progress bar.
func WithScaledGradient(colorA, colorB string) Option {
return func(m *Model) error {
return m.setRamp(colorA, colorB, true)
}
}
// WithSolidFill sets the progress to use a solid fill with the given color.
func WithSolidFill(color string) Option {
return func(m *Model) error {
m.FullColor = color
m.useRamp = false
return nil
}
}
// WithoutPercentage hides the numeric percentage.
func WithoutPercentage() Option {
return func(m *Model) error {
m.ShowPercentage = false
return nil
}
}
// WithWidth sets the initial width of the progress bar. Note that you can also
// set the width via the Width property, which can come in handy if you're
// waiting for a tea.WindowSizeMsg.
func WithWidth(w int) Option {
return func(m *Model) error {
m.Width = w
return nil
}
}
// Model stores values we'll use when rendering the progress bar.
type Model struct {
// Total width of the progress bar, including percentage, if set.
Width int
// "Filled" sections of the progress bar
Full rune
FullColor string
// "Empty" sections of progress bar
Empty rune
EmptyColor string
// Settings for rendering the numeric percentage
ShowPercentage bool
PercentFormat string // a fmt string for a float
PercentageStyle *termenv.Style
useRamp bool
rampColorA colorful.Color
rampColorB colorful.Color
// When true, we scale the gradient to fit the width of the filled section
// of the progress bar. When false, the width of the gradient will be set
// to the full width of the progress bar.
scaleRamp bool
}
// NewModel returns a model with default values.
func NewModel(opts ...Option) (*Model, error) {
m := &Model{
Width: defaultWidth,
Full: '█',
FullColor: "#7571F9",
Empty: '░',
EmptyColor: "#606060",
ShowPercentage: true,
PercentFormat: " %3.0f%%",
}
for _, opt := range opts {
if err := opt(m); err != nil {
return nil, err
}
}
return m, nil
}
// View renders the progress bar as a given percentage.
func (m Model) View(percent float64) string {
b := strings.Builder{}
if m.ShowPercentage {
percentage := fmt.Sprintf(m.PercentFormat, percent*100) //nolint:gomnd
if m.PercentageStyle != nil {
percentage = m.PercentageStyle.Styled(percentage)
}
m.bar(&b, percent, ansi.PrintableRuneWidth(percentage))
b.WriteString(percentage)
} else {
m.bar(&b, percent, 0)
}
return b.String()
}
func (m Model) bar(b *strings.Builder, percent float64, textWidth int) {
var (
tw = m.Width - textWidth // total width
fw = int(float64(tw) * percent) // filled width
p float64
)
if m.useRamp {
// Gradient fill
for i := 0; i < fw; i++ {
if m.scaleRamp {
p = float64(i) / float64(fw)
} else {
p = float64(i) / float64(tw)
}
c := m.rampColorA.BlendLuv(m.rampColorB, p).Hex()
b.WriteString(termenv.
String(string(m.Full)).
Foreground(color(c)).
String(),
)
}
} else {
// Solid fill
s := termenv.String(string(m.Full)).Foreground(color(m.FullColor)).String()
b.WriteString(strings.Repeat(s, fw))
}
// Empty fill
e := termenv.String(string(m.Empty)).Foreground(color(m.EmptyColor)).String()
b.WriteString(strings.Repeat(e, tw-fw))
}
func (m *Model) setRamp(colorA, colorB string, scaled bool) error {
a, err := colorful.Hex(colorA)
if err != nil {
return err
}
b, err := colorful.Hex(colorB)
if err != nil {
return err
}
m.useRamp = true
m.scaleRamp = scaled
m.rampColorA = a
m.rampColorB = b
return nil
}

View File

@@ -1,99 +1,242 @@
package spinner
import (
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/muesli/termenv"
)
const (
defaultFPS = time.Second / 10
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/ansi"
)
// Spinner is a set of frames used in animating the spinner.
type Spinner = []string
type Spinner struct {
Frames []string
FPS time.Duration
}
// Some spinners to choose from. You could also make your own.
var (
// Some spinners to choose from. You could also make your own.
Line = Spinner([]string{"|", "/", "-", "\\"})
Dot = Spinner([]string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "})
color = termenv.ColorProfile().Color
Line = Spinner{
Frames: []string{"|", "/", "-", "\\"},
FPS: time.Second / 10, //nolint:gomnd
}
Dot = Spinner{
Frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
FPS: time.Second / 10, //nolint:gomnd
}
MiniDot = Spinner{
Frames: []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
FPS: time.Second / 12, //nolint:gomnd
}
Jump = Spinner{
Frames: []string{"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"},
FPS: time.Second / 10, //nolint:gomnd
}
Pulse = Spinner{
Frames: []string{"█", "▓", "▒", "░"},
FPS: time.Second / 8, //nolint:gomnd
}
Points = Spinner{
Frames: []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●"},
FPS: time.Second / 7, //nolint:gomnd
}
Globe = Spinner{
Frames: []string{"🌍", "🌎", "🌏"},
FPS: time.Second / 4, //nolint:gomnd
}
Moon = Spinner{
Frames: []string{"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"},
FPS: time.Second / 8, //nolint:gomnd
}
Monkey = Spinner{
Frames: []string{"🙈", "🙉", "🙊"},
FPS: time.Second / 3, //nolint:gomnd
}
)
// Model contains the state for the spinner. Use NewModel to create new models
// rather than using Model as a struct literal.
type Model struct {
// Type is the set of frames to use. See Spinner.
Frames Spinner
// Spinner settings to use. See type Spinner.
Spinner Spinner
// FPS is the speed at which the ticker should tick
FPS time.Duration
// Style sets the styling for the spinner. Most of the time you'll just
// want foreground and background coloring, and potentially some padding.
//
// For an introduction to styling with Lip Gloss see:
// https://github.com/charmbracelet/lipgloss
Style lipgloss.Style
// 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
// doesn't support the color specified it will automatically degrade
// (per github.com/muesli/termenv).
ForegroundColor 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
// BackgroundColor 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
// doesn't support the color specified it will automatically degrade
// (per github.com/muesli/termenv).
BackgroundColor string
// 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
frame int
startTime time.Time
tag int
}
// Start resets resets the spinner start time. For use with MinimumLifetime and
// MinimumStartTime. Optional.
//
// This function is optional and generally considered for advanced use only.
// Most of the time your application logic will obviate the need for this
// method.
//
// This is considered experimental and may not appear in future versions of
// this library.
func (m *Model) Start() {
m.startTime = time.Now()
}
// Finish sets the internal timer to a completed state so long as the spinner
// isn't flagged to be showing. If it is showing, finish has no effect. The
// idea here is that you call Finish if your operation has completed and, if
// the spinner isn't showing yet (by virtue of HideFor) then Visible() doesn't
// show the spinner at all.
//
// This is intended to work in conjunction with MinimumLifetime and
// MinimumStartTime, is completely optional.
//
// This function is optional and generally considered for advanced use only.
// Most of the time your application logic will obviate the need for this
// method.
//
// This is considered experimental and may not appear in future versions of
// this library.
func (m *Model) Finish() {
if m.hidden() {
m.startTime = time.Time{}
}
}
// advancedMode returns whether or not the user is making use of HideFor and
// MinimumLifetime properties.
func (m Model) advancedMode() bool {
return m.HideFor > 0 && m.MinimumLifetime > 0
}
// 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 the minimum lifetime of this spinner has been
// exceeded.
func (m Model) finished() bool {
if m.startTime.IsZero() || 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 method 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.
//
// This function is optional and generally considered for advanced use only.
// Most of the time your application logic will obviate the need for this
// method.
//
// 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.
func NewModel() Model {
return Model{
Frames: Line,
FPS: defaultFPS,
}
return Model{Spinner: Line}
}
// TickMsg indicates that the timer has ticked and we should render a frame.
type TickMsg struct{}
type TickMsg struct {
Time time.Time
tag int
}
// 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
// is setup so as not to call this Update needlessly.
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
if _, ok := msg.(TickMsg); ok {
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) {
case TickMsg:
// If a tag is set, and it's not the one we expect, reject the message.
// This prevents the spinner from receiving too many messages and
// this spinning too fast.
if msg.tag > 0 && msg.tag != m.tag {
return m, nil
}
m.frame++
if m.frame >= len(m.Frames) {
if m.frame >= len(m.Spinner.Frames) {
m.frame = 0
}
return m, Tick(m)
m.tag++
return m, m.tick(m.tag)
default:
return m, nil
}
return m, nil
}
// View renders the model's view.
func View(model Model) string {
if model.frame >= len(model.Frames) {
return "error"
func (m Model) View() string {
if m.frame >= len(m.Spinner.Frames) {
return "(error)"
}
frame := model.Frames[model.frame]
frame := m.Spinner.Frames[m.frame]
if model.ForegroundColor != "" || model.BackgroundColor != "" {
return termenv.
String(frame).
Foreground(color(model.ForegroundColor)).
Background(color(model.BackgroundColor)).
String()
// If we're using the fine-grained hide/show spinner rules and those rules
// deem that the spinner should be hidden, draw an empty space in place of
// the spinner.
if m.advancedMode() && !m.Visible() {
frame = strings.Repeat(" ", ansi.PrintableRuneWidth(frame))
}
return frame
return m.Style.Render(frame)
}
// 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{}
// Tick is the command used to advance the spinner one frame. Use this command
// to effectively start the spinner.
func Tick() tea.Msg {
return TickMsg{Time: time.Now()}
}
func (m Model) tick(tag int) tea.Cmd {
return tea.Tick(m.Spinner.FPS, func(t time.Time) tea.Msg {
return TickMsg{
Time: t,
tag: tag,
}
})
}

View File

@@ -1,40 +1,79 @@
package textinput
import (
"context"
"strings"
"time"
"unicode"
"github.com/atotto/clipboard"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
rw "github.com/mattn/go-runewidth"
"github.com/muesli/termenv"
)
const defaultBlinkSpeed = time.Millisecond * 530
// 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 (
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 (
// color is a helper for returning colors
color func(s string) termenv.Color = termenv.ColorProfile().Color
// blinkCtx manages cursor blinking.
type blinkCtx struct {
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
// 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
Cursor string
BlinkSpeed time.Duration
Placeholder string
TextColor string
BackgroundColor string
PlaceholderColor string
CursorColor string
Err error
// General settings.
Prompt string
Placeholder string
BlinkSpeed time.Duration
EchoMode EchoMode
EchoCharacter rune
// Styles. These will be applied as inline styles.
//
// For an introduction to styling with Lip Gloss see:
// https://github.com/charmbracelet/lipgloss
PromptStyle lipgloss.Style
TextStyle lipgloss.Style
BackgroundStyle lipgloss.Style
PlaceholderStyle lipgloss.Style
CursorStyle lipgloss.Style
// CharLimit is the maximum amount of characters this input element will
// accept. If 0 or less, there's no limit.
@@ -45,23 +84,50 @@ 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
// 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: "> ",
BlinkSpeed: defaultBlinkSpeed,
EchoCharacter: '*',
CharLimit: 0,
PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
value: nil,
focus: false,
blink: true,
pos: 0,
cursorMode: cursorBlink,
blinkCtx: &blinkCtx{
ctx: context.Background(),
},
}
}
// SetValue sets the value of the text input.
@@ -72,8 +138,8 @@ func (m *Model) SetValue(s string) {
} else {
m.value = runes
}
if m.pos > len(m.value) {
m.pos = len(m.value)
if m.pos == 0 || m.pos > len(m.value) {
m.setCursor(len(m.value))
}
m.handleOverflow()
}
@@ -83,23 +149,51 @@ 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.
// Cursor returns the cursor position.
func (m Model) Cursor() int {
return m.pos
}
// SetCursor 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.setCursor(pos)
}
// setCursor moves the cursor to the given position and returns whether or not
// the cursor blink should be reset. If the position is out of bounds the
// cursor will be moved to the start or end accordingly.
func (m *Model) setCursor(pos int) bool {
m.pos = clamp(pos, 0, len(m.value))
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 input field.
func (m *Model) CursorStart() {
m.pos = 0
m.handleOverflow()
m.cursorStart()
}
// CursorEnd moves the cursor to the end of the field.
// cursorStart moves the cursor to the start of the input field and returns
// whether or not the curosr blink should be reset.
func (m *Model) cursorStart() bool {
return m.setCursor(0)
}
// CursorEnd moves the cursor to the end of the input field
func (m *Model) CursorEnd() {
m.pos = len(m.value)
m.handleOverflow()
m.cursorEnd()
}
// cursorEnd moves the cursor to the end of the input field and returns whether
// or not
func (m *Model) cursorEnd() bool {
return m.setCursor(len(m.value))
}
// Focused returns the focus state on the model.
@@ -110,7 +204,7 @@ func (m Model) Focused() bool {
// Focus sets the focus state on the model.
func (m *Model) Focus() {
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.
@@ -119,11 +213,57 @@ func (m *Model) Blur() {
m.blink = true
}
// Reset sets the input to its default state with no input.
func (m *Model) Reset() {
// Reset sets the input to its default state with no input. Returns whether
// or not the cursor blink should reset.
func (m *Model) Reset() bool {
m.value = nil
m.pos = 0
m.blink = false
return m.setCursor(0)
}
// handle a clipboard paste event, if supported. Returns whether or not the
// cursor blink should be reset.
func (m *Model) handlePaste(v string) 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 false
}
// 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
@@ -139,7 +279,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 +293,7 @@ func (m *Model) handleOverflow() {
}
m.offsetRight = m.offset + i
} else if m.pos >= m.offsetRight {
m.offsetRight = m.pos
w := 0
@@ -171,40 +308,117 @@ func (m *Model) handleOverflow() {
}
m.offset = m.offsetRight - (len(runes) - 1 - i)
}
}
// colorText colorizes a given string according to the TextColor value of the
// model.
func (m *Model) colorText(s string) string {
return termenv.
String(s).
Foreground(color(m.TextColor)).
Background(color(m.BackgroundColor)).
String()
// deleteBeforeCursor deletes all text before the cursor. Returns whether or
// not the cursor blink should be reset.
func (m *Model) deleteBeforeCursor() bool {
m.value = m.value[m.pos:]
m.offset = 0
return m.setCursor(0)
}
// colorPlaceholder colorizes a given string according to the TextColor value
// of the model.
func (m *Model) colorPlaceholder(s string) string {
return termenv.
String(s).
Foreground(color(m.PlaceholderColor)).
Background(color(m.BackgroundColor)).
String()
// deleteAfterCursor deletes all text after the cursor. Returns whether or not
// the cursor blink should be reset. If input is masked delete everything after
// the cursor so as not to reveal word breaks in the masked input.
func (m *Model) deleteAfterCursor() bool {
m.value = m.value[:m.pos]
return m.setCursor(len(m.value))
}
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() bool {
if m.pos == 0 || len(m.value) == 0 {
return
return false
}
i := m.pos - 1
if m.EchoMode != EchoNormal {
return m.deleteBeforeCursor()
}
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 blink
}
// deleteWordRight deletes the word right to the cursor. Returns whether or not
// the cursor blink should be reset. If input is masked delete everything after
// the cursor so as not to reveal word breaks in the masked input.
func (m *Model) deleteWordRight() bool {
if m.pos >= len(m.value) || len(m.value) == 0 {
return false
}
if m.EchoMode != EchoNormal {
return m.deleteAfterCursor()
}
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:]...)
}
return m.setCursor(i)
}
// wordLeft moves the cursor one word to the left. Returns whether or not the
// cursor blink should be reset. If input is masked, move input to the start
// so as not to reveal word breaks in the masked input.
func (m *Model) wordLeft() bool {
if m.pos == 0 || len(m.value) == 0 {
return false
}
if m.EchoMode != EchoNormal {
return m.cursorStart()
}
blink := false
i := m.pos - 1
for i >= 0 {
if unicode.IsSpace(m.value[i]) {
m.pos--
blink = m.setCursor(m.pos - 1)
i--
} else {
break
@@ -213,24 +427,33 @@ func (m *Model) wordLeft() {
for i >= 0 {
if !unicode.IsSpace(m.value[i]) {
m.pos--
blink = m.setCursor(m.pos - 1)
i--
} else {
break
}
}
return blink
}
func (m *Model) wordRight() {
// wordRight moves the cursor one word to the right. Returns whether or not the
// cursor blink should be reset. If the input is masked, move input to the end
// so as not to reveal word breaks in the masked input.
func (m *Model) wordRight() bool {
if m.pos >= len(m.value) || len(m.value) == 0 {
return
return false
}
if m.EchoMode != EchoNormal {
return m.cursorEnd()
}
blink := false
i := m.pos
for i < len(m.value) {
if unicode.IsSpace(rune(m.value[i])) {
m.pos++
if unicode.IsSpace(m.value[i]) {
blink = m.setCursor(m.pos + 1)
i++
} else {
break
@@ -238,209 +461,228 @@ func (m *Model) wordRight() {
}
for i < len(m.value) {
if !unicode.IsSpace(rune(m.value[i])) {
m.pos++
if !unicode.IsSpace(m.value[i]) {
blink = m.setCursor(m.pos + 1)
i++
} else {
break
}
}
return blink
}
// BlinkMsg is sent when the cursor should alternate it's blinking state.
type BlinkMsg struct{}
func (m Model) echoTransform(v string) string {
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.
func NewModel() Model {
return Model{
Prompt: "> ",
BlinkSpeed: defaultBlinkSpeed,
Placeholder: "",
TextColor: "",
PlaceholderColor: "240",
CursorColor: "",
CharLimit: 0,
value: nil,
focus: false,
blink: true,
pos: 0,
default:
return v
}
}
// Update is the Tea update loop.
func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
// Update is the Bubble Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if !m.focus {
m.blink = true
return m, nil
}
var resetBlink bool
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyBackspace:
fallthrough
case tea.KeyDelete:
if len(m.value) > 0 {
m.value = append(m.value[:m.pos-1], m.value[m.pos:]...)
m.pos--
case tea.KeyBackspace: // delete character before cursor
if msg.Alt {
resetBlink = m.deleteWordLeft()
} else {
if len(m.value) > 0 {
m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...)
if m.pos > 0 {
resetBlink = m.setCursor(m.pos - 1)
}
}
}
case tea.KeyLeft:
case tea.KeyLeft, tea.KeyCtrlB:
if msg.Alt { // alt+left arrow, back one word
m.wordLeft()
resetBlink = m.wordLeft()
break
}
if m.pos > 0 {
m.pos--
if m.pos > 0 { // left arrow, ^F, back one character
resetBlink = m.setCursor(m.pos - 1)
}
case tea.KeyRight:
case tea.KeyRight, tea.KeyCtrlF:
if msg.Alt { // alt+right arrow, forward one word
m.wordRight()
resetBlink = m.wordRight()
break
}
if m.pos < len(m.value) {
m.pos++
if m.pos < len(m.value) { // right arrow, ^F, forward one character
resetBlink = m.setCursor(m.pos + 1)
}
case tea.KeyCtrlF: // ^F, forward one character
fallthrough
case tea.KeyCtrlB: // ^B, back one charcter
fallthrough
case tea.KeyCtrlA: // ^A, go to beginning
m.CursorStart()
case tea.KeyCtrlD: // ^D, delete char under cursor
case tea.KeyCtrlW: // ^W, delete word left of cursor
resetBlink = m.deleteWordLeft()
case tea.KeyHome, tea.KeyCtrlA: // ^A, go to beginning
resetBlink = m.cursorStart()
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
m.CursorEnd()
case tea.KeyCtrlE, tea.KeyEnd: // ^E, go to end
resetBlink = m.cursorEnd()
case tea.KeyCtrlK: // ^K, kill text after cursor
m.value = m.value[:m.pos]
m.pos = len(m.value)
resetBlink = m.deleteAfterCursor()
case tea.KeyCtrlU: // ^U, kill text before cursor
m.value = m.value[m.pos:]
m.pos = 0
m.offset = 0
case tea.KeyRune: // input a regular character
if msg.Alt {
if msg.Rune == 'b' { // alt+b, back one word
m.wordLeft()
resetBlink = m.deleteBeforeCursor()
case tea.KeyCtrlV: // ^V paste
return m, Paste
case tea.KeyRunes: // input regular characters
if msg.Alt && len(msg.Runes) == 1 {
if msg.Runes[0] == 'd' { // alt+d, delete word right of cursor
resetBlink = m.deleteWordRight()
break
}
if msg.Rune == 'f' { // alt+f, forward one word
m.wordRight()
if msg.Runes[0] == 'b' { // alt+b, back one word
resetBlink = m.wordLeft()
break
}
if msg.Runes[0] == 'f' { // alt+f, forward one word
resetBlink = m.wordRight()
break
}
}
// 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.value = append(m.value[:m.pos], append(msg.Runes, m.value[m.pos:]...)...)
resetBlink = m.setCursor(m.pos + len(msg.Runes))
}
}
case ErrMsg:
m.Err = msg
case blinkMsg:
var cmd tea.Cmd
if m.cursorMode == cursorBlink {
m.blink = !m.blink
cmd = m.blinkCmd()
}
return m, cmd
case BlinkMsg:
m.blink = !m.blink
return m, Blink(m)
case blinkCanceled: // no-op
return m, nil
case pasteMsg:
resetBlink = m.handlePaste(string(msg))
case pasteErrMsg:
m.Err = msg
}
var cmd tea.Cmd
if resetBlink {
cmd = m.blinkCmd()
}
m.handleOverflow()
return m, nil
return m, 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 (m Model) View() string {
// Placeholder text
if len(m.value) == 0 && m.Placeholder != "" {
return placeholderView(m)
return m.placeholderView()
}
value := m.value[m.offset:m.offsetRight]
pos := m.pos - m.offset
styleText := m.TextStyle.Inline(true).Render
v := m.colorText(string(value[:pos]))
value := m.value[m.offset:m.offsetRight]
pos := max(0, m.pos-m.offset)
v := styleText(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 += m.cursorView(m.echoTransform(string(value[pos]))) // cursor and text under it
v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor
} else {
v += cursorView(" ", m)
v += m.cursorView(" ")
}
// If a max width and background color were set fill the empty spaces with
// the background color.
valWidth := rw.StringWidth(string(value))
if m.Width > 0 && len(m.BackgroundColor) > 0 && valWidth <= m.Width {
if m.Width > 0 && valWidth <= m.Width {
padding := max(0, m.Width-valWidth)
if valWidth+padding <= m.Width && pos < len(value) {
padding++
}
v += strings.Repeat(
termenv.String(" ").Background(color(m.BackgroundColor)).String(),
padding,
)
v += styleText(strings.Repeat(" ", padding))
}
return m.Prompt + v
return m.PromptStyle.Render(m.Prompt) + v
}
// placeholderView
func placeholderView(m Model) string {
// placeholderView returns the prompt and placeholder view, if any.
func (m Model) placeholderView() string {
var (
v string
p = m.Placeholder
v string
p = m.Placeholder
style = m.PlaceholderStyle.Inline(true).Render
)
// Cursor
if m.blink && m.PlaceholderColor != "" {
v += cursorView(
m.colorPlaceholder(p[:1]),
m,
)
if m.blink {
v += m.cursorView(style(p[:1]))
} else {
v += cursorView(p[:1], m)
v += m.cursorView(p[:1])
}
// The rest of the placeholder text
v += m.colorPlaceholder(p[1:])
v += style(p[1:])
return m.Prompt + v
return m.PromptStyle.Render(m.Prompt) + v
}
// cursorView styles the cursor.
func cursorView(s string, m Model) string {
func (m Model) cursorView(v string) string {
if m.blink {
if m.TextColor != "" || m.BackgroundColor != "" {
return termenv.String(s).
Foreground(color(m.TextColor)).
Background(color(m.BackgroundColor)).
String()
}
return s
return m.TextStyle.Render(v)
}
return termenv.String(s).
Foreground(color(m.CursorColor)).
Background(color(m.BackgroundColor)).
Reverse().
String()
return m.CursorStyle.Inline(true).Reverse(true).Render(v)
}
// Blink is a command used to time the cursor blinking.
func Blink(model Model) tea.Cmd {
return func() tea.Msg {
time.Sleep(model.BlinkSpeed)
return BlinkMsg{}
// blinkCmd is an internal command used to manage cursor blinking.
func (m Model) blinkCmd() tea.Cmd {
if m.blinkCtx != nil && m.blinkCtx.cancel != nil {
m.blinkCtx.cancel()
}
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 {

View File

@@ -8,11 +8,11 @@ import (
)
const (
spacebar = " "
spacebar = " "
mouseWheelDelta = 3
)
// MODEL
// Model is the Bubble Tea model for this viewport element.
type Model struct {
Width int
Height int
@@ -37,21 +37,6 @@ 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
@@ -69,7 +54,7 @@ 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.
func (m Model) ScrollPercent() float64 {
if m.Height >= len(m.lines) {
return 1.0
@@ -86,13 +71,17 @@ func (m Model) ScrollPercent() float64 {
func (m *Model) SetContent(s string) {
s = strings.Replace(s, "\r\n", "\n", -1) // normalize line endings
m.lines = strings.Split(s, "\n")
if m.YOffset > len(m.lines)-1 {
m.GotoBottom()
}
}
// 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
@@ -140,7 +129,7 @@ func (m *Model) HalfViewDown() (lines []string) {
if len(m.lines) > 0 {
top := max(m.YOffset+m.Height/2, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -160,7 +149,7 @@ func (m *Model) HalfViewUp() (lines []string) {
if len(m.lines) > 0 {
top := max(m.YOffset, 0)
bottom := min(m.YOffset+m.Height/2, len(m.lines)-1)
bottom := clamp(m.YOffset+m.Height/2, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -186,7 +175,7 @@ func (m *Model) LineDown(n int) (lines []string) {
if len(m.lines) > 0 {
top := max(m.YOffset+m.Height-n, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -208,7 +197,7 @@ func (m *Model) LineUp(n int) (lines []string) {
if len(m.lines) > 0 {
top := max(0, m.YOffset)
bottom := min(m.YOffset+n, len(m.lines)-1)
bottom := clamp(m.YOffset+n, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
@@ -225,14 +214,14 @@ func (m *Model) GotoTop() (lines []string) {
if len(m.lines) > 0 {
top := m.YOffset
bottom := min(m.YOffset+m.Height, len(m.lines)-1)
bottom := clamp(m.YOffset+m.Height, top, len(m.lines)-1)
lines = m.lines[top:bottom]
}
return lines
}
// GotoTop sets the viewport to the bottom position.
// GotoBottom sets the viewport to the bottom position.
func (m *Model) GotoBottom() (lines []string) {
m.YOffset = max(len(m.lines)-1-m.Height, 0)
@@ -260,7 +249,7 @@ func Sync(m Model) tea.Cmd {
// TODO: we should probably use m.visibleLines() rather than these two
// expressions.
top := max(m.YOffset, 0)
bottom := min(m.YOffset+m.Height, len(m.lines)-1)
bottom := clamp(m.YOffset+m.Height, 0, len(m.lines)-1)
return tea.SyncScrollArea(
m.lines[top:bottom],
@@ -284,7 +273,7 @@ func ViewDown(m Model, lines []string) tea.Cmd {
}
// 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
// number of lines height. Use Model.ViewUp to get the lines that should be
// rendered.
func ViewUp(m Model, lines []string) tea.Cmd {
if len(lines) == 0 {
@@ -298,11 +287,10 @@ func ViewUp(m Model, lines []string) tea.Cmd {
// Update runs the update loop with default keybindings similar to popular
// pagers. To define your own keybindings use the methods on Model (i.e.
// 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
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
// Down one page
@@ -320,14 +308,14 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
}
// Down half page
case "d":
case "d", "ctrl+d":
lines := m.HalfViewDown()
if m.HighPerformanceRendering {
cmd = ViewDown(m, lines)
}
// Up half page
case "u":
case "u", "ctrl+u":
lines := m.HalfViewUp()
if m.HighPerformanceRendering {
cmd = ViewUp(m, lines)
@@ -349,16 +337,15 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
}
case tea.MouseMsg:
switch msg.Button {
switch msg.Type {
case tea.MouseWheelUp:
lines := m.LineUp(3)
lines := m.LineUp(mouseWheelDelta)
if m.HighPerformanceRendering {
cmd = ViewUp(m, lines)
}
case tea.MouseWheelDown:
lines := m.LineDown(3)
lines := m.LineDown(mouseWheelDelta)
if m.HighPerformanceRendering {
cmd = ViewDown(m, lines)
}
@@ -371,8 +358,7 @@ func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
// VIEW
// View renders the viewport into a string.
func View(m Model) string {
func (m Model) View() 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
@@ -394,6 +380,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
@@ -407,7 +397,3 @@ func max(a, b int) int {
}
return b
}
func clamp(val, low, high int) int {
return max(low, min(high, val))
}