- 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
+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=
+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 {
+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 }
+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+}