repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
fc0f9fe
parent
e68f97e
author
Eric Bower
date
2024-05-13 16:45:13 +0000 UTC
chore: new screen
5 files changed,  +190, -63
M go.mod
M go.sum
M go.mod
+4, -0
 1@@ -13,6 +13,7 @@ require (
 2 	github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
 3 	github.com/charmbracelet/bubbles v0.18.0
 4 	github.com/charmbracelet/bubbletea v0.26.1
 5+	github.com/charmbracelet/glamour v0.7.0
 6 	github.com/charmbracelet/lipgloss v0.10.0
 7 	github.com/charmbracelet/promwish v0.7.0
 8 	github.com/charmbracelet/ssh v0.0.0-20240507011153-ec70bd03034c
 9@@ -49,6 +50,7 @@ require (
10 	git.sr.ht/~emersion/go-scfg v0.0.0-20240128091534-2ae16e782082 // indirect
11 	github.com/DavidGamba/go-getoptions v0.30.0 // indirect
12 	github.com/PuerkitoBio/goquery v1.9.2 // indirect
13+	github.com/alecthomas/chroma/v2 v2.8.0 // indirect
14 	github.com/andybalholm/cascadia v1.3.2 // indirect
15 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
16 	github.com/antoniomika/go-rsync-receiver v0.0.0-20231110145728-c94949e1ab7d // indirect
17@@ -108,6 +110,7 @@ require (
18 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
19 	github.com/muesli/cancelreader v0.2.2 // indirect
20 	github.com/neurosnap/go-jpeg-image-structure v0.0.0-20221010133817-70b1c1ff679e // indirect
21+	github.com/olekukonko/tablewriter v0.0.5 // indirect
22 	github.com/philhofer/fwd v1.1.2 // indirect
23 	github.com/pkg/sftp v1.13.6 // indirect
24 	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
25@@ -127,6 +130,7 @@ require (
26 	github.com/tinylib/msgp v1.1.9 // indirect
27 	github.com/tklauser/go-sysconf v0.3.14 // indirect
28 	github.com/tklauser/numcpus v0.8.0 // indirect
29+	github.com/yuin/goldmark-emoji v1.0.2 // indirect
30 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
31 	golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
32 	golang.org/x/net v0.25.0 // indirect
M go.sum
+16, -0
 1@@ -7,8 +7,14 @@ github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4
 2 github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
 3 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
 4 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
 5+github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
 6+github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
 7 github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
 8 github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
 9+github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
10+github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
11+github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
12+github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
13 github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
14 github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
15 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
16@@ -31,6 +37,8 @@ github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/
17 github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
18 github.com/charmbracelet/bubbletea v0.26.1 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0=
19 github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo=
20+github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng=
21+github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps=
22 github.com/charmbracelet/keygen v0.5.0 h1:XY0fsoYiCSM9axkrU+2ziE6u6YjJulo/b9Dghnw6MZc=
23 github.com/charmbracelet/keygen v0.5.0/go.mod h1:DfvCgLHxZ9rJxdK0DGw3C/LkV4SgdGbnliHcObV3L+8=
24 github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
25@@ -134,6 +142,8 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
26 github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
27 github.com/gorilla/feeds v1.1.2 h1:pxzZ5PD3RJdhFH2FsJJ4x6PqMqbgFk1+Vez4XWBW8Iw=
28 github.com/gorilla/feeds v1.1.2/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
29+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
30+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
31 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
32 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
33 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
34@@ -163,6 +173,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
35 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
36 github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
37 github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
38+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
39 github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
40 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
41 github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
42@@ -201,6 +212,8 @@ github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577 h1:hVmVNt
43 github.com/neurosnap/go-exif-remove v0.0.0-20221010134343-50d1e3c35577/go.mod h1:G3Cu1AW+dmRLDFpOi8eUAfc3cGoRHUjTkGjeRcndgl4=
44 github.com/neurosnap/go-jpeg-image-structure v0.0.0-20221010133817-70b1c1ff679e h1:76Dng5ms0fR+26doKZAvNqhi2UPfnLxGfPIDEr+BBlM=
45 github.com/neurosnap/go-jpeg-image-structure v0.0.0-20221010133817-70b1c1ff679e/go.mod h1:nZBDA7+RD63GDJwjZmxhxac65MJqiCIHUUUvdYOsFkk=
46+github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
47+github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
48 github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
49 github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
50 github.com/picosh/pobj v0.0.0-20240417140600-2071618b61c5 h1:iS9zagScak8DCVMCXX5Rvdk7OOQzWUwMPiomrbHCks8=
51@@ -282,10 +295,13 @@ github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYg
52 github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
53 github.com/x-way/crawlerdetect v0.2.21 h1:LORs0nEy+MWUsC3XvKf00hXyO7drB5w/hlGB8bztXbI=
54 github.com/x-way/crawlerdetect v0.2.21/go.mod h1:DVupfue81iupuoUmFjIyDUqPqGaJhtZfYQDWoP1ZUR4=
55+github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
56 github.com/yuin/goldmark v1.4.5/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
57 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
58 github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
59 github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
60+github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
61+github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
62 github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 h1:yHfZyN55+5dp1wG7wDKv8HQ044moxkyGq12KFFMFDxg=
63 github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594/go.mod h1:U9ihbh+1ZN7fR5Se3daSPoz1CGF9IYtSvWwVQtnzGHU=
64 github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
M pico/cli.go
+3, -59
 1@@ -10,6 +10,7 @@ import (
 2 	"github.com/picosh/pico/db"
 3 	"github.com/picosh/pico/shared"
 4 	"github.com/picosh/pico/tui/common"
 5+	"github.com/picosh/pico/tui/plus"
 6 	"github.com/picosh/send/send/utils"
 7 )
 8 
 9@@ -51,65 +52,8 @@ func (c *Cmd) help() {
10 }
11 
12 func (c *Cmd) plus() {
13-	clientRefId := c.User.Name
14-	paymentLink := "https://buy.stripe.com/6oEaIvaNq7DA4NO9AD"
15-	url := fmt.Sprintf("%s?client_reference_id=%s", paymentLink, clientRefId)
16-	md := fmt.Sprintf(`# pico+
17-
18-Signup to get premium access
19-
20-## $2/month (billed annually)
21-
22-Includes:
23-- pgs.sh - 10GB asset storage
24-- tuns.sh - full access
25-- imgs.sh - 5GB image registry storage
26-- prose.sh - 1GB image storage
27-- prose.sh - 1GB image storage
28-- beta access - Invited to join our private IRC channel
29-
30-There are a few ways to purchase a membership. We try our best to
31-provide immediate access to pico+ regardless of payment method.
32-
33-## Stripe (US/CA Only)
34-
35-%s
36-
37-This is the quickest way to access pico+. The Stripe payment
38-method requires an email address. We will never use your email
39-for anything unless absolutely necessary.
40-
41-## Snail Mail
42-
43-Send cash (USD) or check to:
44-- pico.sh LLC
45-- 206 E Huron St STE 103
46-- Ann Arbor MI 48104
47-
48-Message us when payment is in transit and we will grant you
49-temporary access topico+ that will be converted to a full
50-year after we received it.
51-
52-## Notes
53-
54-Have any questions not covered here? Email us or join IRC,
55-we will promptly respond.
56-
57-Unfortunately we do not have the labor bandwidth to support
58-international users for pico+ at this time. As a result,
59-we only offer our premium services to the US and Canada.
60-
61-We do not maintain active subscriptions for pico+. Every
62-year you must pay again. We do not take monthly payments,
63-you must pay for a year up-front. Pricing is subject to
64-change because we plan on continuing to include more services
65-as we build them.
66-
67-Need higher limits? We are more than happy to extend limits.
68-Just message us and we can chat.
69-`, url)
70-
71-	c.output(md)
72+	view := plus.PlusView(c.User.Name)
73+	c.output(view)
74 }
75 
76 type CliHandler struct {
M tui/cms.go
+26, -4
 1@@ -20,6 +20,7 @@ import (
 2 	"github.com/picosh/pico/tui/common"
 3 	"github.com/picosh/pico/tui/info"
 4 	"github.com/picosh/pico/tui/keys"
 5+	"github.com/picosh/pico/tui/plus"
 6 	"github.com/picosh/pico/tui/tokens"
 7 )
 8 
 9@@ -32,6 +33,7 @@ const (
10 	statusBrowsingKeys
11 	statusBrowsingTokens
12 	statusChat
13+	statusBrowsingPlus
14 	statusQuitting
15 )
16 
17@@ -53,16 +55,18 @@ const (
18 	keysChoice menuChoice = iota
19 	tokensChoice
20 	chatChoice
21+	plusChoice
22 	exitChoice
23 	unsetChoice // set when no choice has been made
24 )
25 
26 // menu text corresponding to menu choices. these are presented to the user.
27 var menuChoices = map[menuChoice]string{
28-	keysChoice:   "Manage keys",
29-	tokensChoice: "Manage tokens",
30-	chatChoice:   "Chat",
31-	exitChoice:   "Exit",
32+	keysChoice:   "manage keys",
33+	tokensChoice: "manage tokens",
34+	chatChoice:   "chat",
35+	plusChoice:   "pico+",
36+	exitChoice:   "exit",
37 }
38 
39 func NewSpinner(styles common.Styles) spinner.Model {
40@@ -137,6 +141,7 @@ type model struct {
41 	spinner         spinner.Model
42 	keys            keys.Model
43 	tokens          tokens.Model
44+	plus            plus.Model
45 	createAccount   account.CreateModel
46 	terminalSize    tea.WindowSizeMsg
47 	session         ssh.Session
48@@ -237,6 +242,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
49 		m.keys = keys.NewModel(m.styles, m.cfg.Logger, m.dbpool, m.user)
50 		m.tokens = tokens.NewModel(m.styles, m.dbpool, m.user)
51 		m.createAccount = account.NewCreateModel(m.styles, m.dbpool, m.publicKey)
52+		m.plus = plus.NewModel(m.styles, m.user, m.session)
53 	}
54 
55 	switch m.status {
56@@ -245,6 +251,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
57 		m.keys = keys.NewModel(m.styles, m.cfg.Logger, m.dbpool, m.user)
58 		m.tokens = tokens.NewModel(m.styles, m.dbpool, m.user)
59 		m.createAccount = account.NewCreateModel(m.styles, m.dbpool, m.publicKey)
60+		m.plus = plus.NewModel(m.styles, m.user, m.session)
61 		if m.user == nil {
62 			m.status = statusNoAccount
63 		} else {
64@@ -296,6 +303,15 @@ func updateChildren(msg tea.Msg, m model) (model, tea.Cmd) {
65 			m.status = statusQuitting
66 			return m, tea.Quit
67 		}
68+	case statusBrowsingPlus:
69+		m.plus, cmd = plus.Update(msg, m.plus)
70+		if m.plus.Done {
71+			m.plus = plus.NewModel(m.styles, m.user, m.session)
72+			m.status = statusReady
73+		} else if m.tokens.Quit {
74+			m.status = statusQuitting
75+			return m, tea.Quit
76+		}
77 	case statusNoAccount:
78 		m.createAccount, cmd = account.Update(msg, m.createAccount)
79 		if m.createAccount.Done {
80@@ -317,6 +333,9 @@ func updateChildren(msg tea.Msg, m model) (model, tea.Cmd) {
81 		m.status = statusBrowsingTokens
82 		m.menuChoice = unsetChoice
83 		cmd = tokens.LoadKeys(m.tokens)
84+	case plusChoice:
85+		m.status = statusBrowsingPlus
86+		m.menuChoice = unsetChoice
87 	case chatChoice:
88 		m.status = statusChat
89 		m.menuChoice = unsetChoice
90@@ -413,6 +432,9 @@ func (m model) View() string {
91 		s += m.keys.View()
92 	case statusBrowsingTokens:
93 		s += m.tokens.View()
94+	case statusBrowsingPlus:
95+		s = plus.View(m.plus)
96+		return s
97 	}
98 	return m.styles.App.Render(wrap.String(wordwrap.String(s, w), w))
99 }
A tui/plus/plus.go
+141, -0
  1@@ -0,0 +1,141 @@
  2+package plus
  3+
  4+import (
  5+	"fmt"
  6+
  7+	"github.com/charmbracelet/bubbles/viewport"
  8+	tea "github.com/charmbracelet/bubbletea"
  9+	"github.com/charmbracelet/glamour"
 10+	"github.com/charmbracelet/ssh"
 11+	"github.com/picosh/pico/db"
 12+	"github.com/picosh/pico/tui/common"
 13+)
 14+
 15+func PlusView(username string) string {
 16+	clientRefId := username
 17+	paymentLink := "https://buy.stripe.com/6oEaIvaNq7DA4NO9AD"
 18+	url := fmt.Sprintf("%s?client_reference_id=%s", paymentLink, clientRefId)
 19+	md := fmt.Sprintf(`# pico+
 20+
 21+Signup to get premium access
 22+
 23+## $2/month (billed annually)
 24+
 25+- tuns
 26+  - full access
 27+- pages
 28+  - full access
 29+  - per-site analytics
 30+- prose
 31+  - increased storage limits
 32+  - blog analytics
 33+- docker registry
 34+  - full access
 35+
 36+There are a few ways to purchase a membership. We try our best to
 37+provide immediate access to <code>pico+</code> regardless of payment
 38+method.
 39+
 40+## Stripe (US/CA Only)
 41+
 42+%s
 43+
 44+## Snail Mail
 45+
 46+Send cash (USD) or check to:
 47+- pico.sh LLC
 48+- 206 E Huron St STE 103
 49+- Ann Arbor MI 48104
 50+
 51+## Notes
 52+
 53+Have any questions not covered here? [Email](mailto:hello@pico.sh)
 54+us or join [IRC](https://pico.sh/irc), we will promptly respond.
 55+
 56+Unfortunately we do not have the human bandwidth to support
 57+international users for pico+ at this time. As a
 58+result, we only offer our premium services to the US and Canada.
 59+
 60+We do not maintain active subscriptions for pico+.
 61+Every year you must pay again. We do not take monthly payments, you
 62+must pay for a year up-front. Pricing is subject to change because
 63+we plan on continuing to include more services as we build them.`, url)
 64+
 65+	r, _ := glamour.NewTermRenderer(
 66+		// detect background color and pick either the default dark or light theme
 67+		glamour.WithAutoStyle(),
 68+	)
 69+	out, _ := r.Render(md)
 70+	return out
 71+}
 72+
 73+// Model holds the state of the username UI.
 74+type Model struct {
 75+	Done bool // true when it's time to exit this view
 76+	Quit bool // true when the user wants to quit the whole program
 77+
 78+	styles   common.Styles
 79+	user     *db.User
 80+	viewport viewport.Model
 81+}
 82+
 83+func headerHeight(styles common.Styles) int {
 84+	return 0
 85+}
 86+
 87+func headerWidth(w int) int {
 88+	return w - 2
 89+}
 90+
 91+// NewModel returns a new username model in its initial state.
 92+func NewModel(styles common.Styles, user *db.User, session ssh.Session) Model {
 93+	pty, _, _ := session.Pty()
 94+	hh := headerHeight(styles)
 95+	viewport := viewport.New(headerWidth(pty.Window.Width), pty.Window.Height-hh)
 96+	viewport.YPosition = hh
 97+	viewport.SetContent(PlusView(user.Name))
 98+
 99+	return Model{
100+		Done:     false,
101+		Quit:     false,
102+		styles:   styles,
103+		user:     user,
104+		viewport: viewport,
105+	}
106+}
107+
108+// Update is the Bubble Tea update loop.
109+func Update(msg tea.Msg, m Model) (Model, tea.Cmd) {
110+	var cmd tea.Cmd
111+	var cmds []tea.Cmd
112+
113+	switch msg := msg.(type) {
114+	case tea.KeyMsg:
115+		switch msg.Type {
116+		case tea.KeyCtrlC, tea.KeyEscape:
117+			m.Quit = true
118+
119+		default:
120+			switch msg.String() {
121+			case "q", "esc":
122+				m.Done = true
123+			}
124+		}
125+
126+	case tea.WindowSizeMsg:
127+		m.viewport.Width = headerWidth(msg.Width)
128+		hh := headerHeight(m.styles)
129+		m.viewport.Height = msg.Height - hh
130+	}
131+
132+	m.viewport, cmd = m.viewport.Update(msg)
133+	cmds = append(cmds, cmd)
134+
135+	return m, tea.Batch(cmds...)
136+}
137+
138+// View renders current view from the model.
139+func View(m Model) string {
140+	s := m.viewport.View()
141+	return s
142+}