Detailed changes
@@ -33,6 +33,7 @@ type Account struct {
type Config struct {
Accounts []Account `json:"accounts"`
DisableImages bool `json:"disable_images,omitempty"`
+ HideTips bool `json:"hide_tips,omitempty"`
}
// GetIMAPServer returns the IMAP server address for the account.
@@ -3,48 +3,40 @@ module github.com/floatpane/matcha
go 1.26.0
require (
+ charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
charm.land/lipgloss/v2 v2.0.0
github.com/PuerkitoBio/goquery v1.11.0
- github.com/charmbracelet/bubbles v1.0.0
- github.com/charmbracelet/bubbletea v1.3.10
- github.com/charmbracelet/lipgloss v1.1.0
github.com/emersion/go-imap v1.2.1
github.com/emersion/go-message v0.18.2
github.com/google/uuid v1.6.0
github.com/yuin/goldmark v1.7.16
+ github.com/zalando/go-keyring v0.2.6
golang.org/x/sys v0.41.0
golang.org/x/text v0.34.0
)
require (
- charm.land/bubbles/v2 v2.0.0 // indirect
+ al.essio.dev/pkg/shellescape v1.5.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
- github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
- github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
+ github.com/danieljoos/wincred v1.2.2 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
- github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
- github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
- github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
- github.com/zalando/go-keyring v0.2.6 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.19.0 // indirect
)
@@ -1,3 +1,5 @@
+al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
+al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
@@ -12,24 +14,14 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
-github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
-github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM=
github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
-github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
-github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
-github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
-github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
-github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
-github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA=
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
-github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
-github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
@@ -42,6 +34,10 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
+github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
+github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
@@ -51,33 +47,31 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTe
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
-github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
-github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
-github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
-github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
-github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
-github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
-github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
@@ -121,11 +115,9 @@ golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -161,3 +153,5 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -87,7 +87,11 @@ func newInitialModel(cfg *config.Config) *mainModel {
}
if cfg == nil || !cfg.HasAccounts() {
- initialModel.current = tui.NewLogin()
+ hideTips := false
+ if cfg != nil {
+ hideTips = cfg.HideTips
+ }
+ initialModel.current = tui.NewLogin(hideTips)
} else {
initialModel.current = tui.NewChoice()
initialModel.config = cfg
@@ -220,7 +224,11 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tui.GoToInboxMsg:
if m.config == nil || !m.config.HasAccounts() {
- m.current = tui.NewLogin()
+ hideTips := false
+ if m.config != nil {
+ hideTips = m.config.HideTips
+ }
+ m.current = tui.NewLogin(hideTips)
return m, m.current.Init()
}
// Try to load from cache first for instant display
@@ -233,7 +241,11 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tui.GoToSentInboxMsg:
if m.config == nil || !m.config.HasAccounts() {
- m.current = tui.NewLogin()
+ hideTips := false
+ if m.config != nil {
+ hideTips = m.config.HideTips
+ }
+ m.current = tui.NewLogin(hideTips)
return m, m.current.Init()
}
m.current = tui.NewStatus("Fetching sent emails from all accounts...")
@@ -241,7 +253,11 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tui.GoToTrashArchiveMsg:
if m.config == nil || !m.config.HasAccounts() {
- m.current = tui.NewLogin()
+ hideTips := false
+ if m.config != nil {
+ hideTips = m.config.HideTips
+ }
+ m.current = tui.NewLogin(hideTips)
return m, m.current.Init()
}
m.current = tui.NewStatus("Fetching trash and archive emails...")
@@ -467,12 +483,16 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case tui.GoToSendMsg:
+ hideTips := false
+ if m.config != nil {
+ hideTips = m.config.HideTips
+ }
if m.config != nil && len(m.config.Accounts) > 0 {
firstAccount := m.config.GetFirstAccount()
- composer := tui.NewComposerWithAccounts(m.config.Accounts, firstAccount.ID, msg.To, msg.Subject, msg.Body)
+ composer := tui.NewComposerWithAccounts(m.config.Accounts, firstAccount.ID, msg.To, msg.Subject, msg.Body, hideTips)
m.current = composer
} else {
- m.current = tui.NewComposer("", msg.To, msg.Subject, msg.Body)
+ m.current = tui.NewComposer("", msg.To, msg.Subject, msg.Body, hideTips)
}
m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
return m, m.current.Init()
@@ -485,10 +505,12 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tui.OpenDraftMsg:
var accounts []config.Account
+ hideTips := false
if m.config != nil {
accounts = m.config.Accounts
+ hideTips = m.config.HideTips
}
- composer := tui.NewComposerFromDraft(msg.Draft, accounts)
+ composer := tui.NewComposerFromDraft(msg.Draft, accounts, hideTips)
m.current = composer
m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
return m, m.current.Init()
@@ -509,7 +531,11 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.current.Init()
case tui.GoToAddAccountMsg:
- m.current = tui.NewLogin()
+ hideTips := false
+ if m.config != nil {
+ hideTips = m.config.HideTips
+ }
+ m.current = tui.NewLogin(hideTips)
m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
return m, m.current.Init()
@@ -592,15 +618,19 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
quotedText := fmt.Sprintf("\n\nOn %s, %s wrote:\n> %s", msg.Email.Date.Format("Jan 2, 2006 at 3:04 PM"), msg.Email.From, strings.ReplaceAll(msg.Email.Body, "\n", "\n> "))
var composer *tui.Composer
+ hideTips := false
+ if m.config != nil {
+ hideTips = m.config.HideTips
+ }
if m.config != nil && len(m.config.Accounts) > 0 {
// Use the account that received the email
accountID := msg.Email.AccountID
if accountID == "" {
accountID = m.config.GetFirstAccount().ID
}
- composer = tui.NewComposerWithAccounts(m.config.Accounts, accountID, to, subject, "")
+ composer = tui.NewComposerWithAccounts(m.config.Accounts, accountID, to, subject, "", hideTips)
} else {
- composer = tui.NewComposer("", to, subject, "")
+ composer = tui.NewComposer("", to, subject, "", hideTips)
}
composer.SetQuotedText(quotedText)
@@ -629,15 +659,19 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
body := forwardHeader + msg.Email.Body
var composer *tui.Composer
+ hideTips := false
+ if m.config != nil {
+ hideTips = m.config.HideTips
+ }
if m.config != nil && len(m.config.Accounts) > 0 {
// Use the account that received the email
accountID := msg.Email.AccountID
if accountID == "" {
accountID = m.config.GetFirstAccount().ID
}
- composer = tui.NewComposerWithAccounts(m.config.Accounts, accountID, "", subject, body)
+ composer = tui.NewComposerWithAccounts(m.config.Accounts, accountID, "", subject, body, hideTips)
} else {
- composer = tui.NewComposer("", "", subject, body)
+ composer = tui.NewComposer("", "", subject, body, hideTips)
}
m.current = composer
@@ -56,6 +56,7 @@ type Composer struct {
width int
height int
confirmingExit bool
+ hideTips bool
// Multi-account support
accounts []config.Account
@@ -80,9 +81,10 @@ type Composer struct {
}
// NewComposer initializes a new composer model.
-func NewComposer(from, to, subject, body string) *Composer {
+func NewComposer(from, to, subject, body string, hideTips bool) *Composer {
m := &Composer{
- draftID: uuid.New().String(),
+ draftID: uuid.New().String(),
+ hideTips: hideTips,
}
m.toInput = textinput.New()
@@ -130,8 +132,8 @@ func NewComposer(from, to, subject, body string) *Composer {
}
// NewComposerWithAccounts initializes a composer with multiple account support.
-func NewComposerWithAccounts(accounts []config.Account, selectedAccountID string, to, subject, body string) *Composer {
- m := NewComposer("", to, subject, body)
+func NewComposerWithAccounts(accounts []config.Account, selectedAccountID string, to, subject, body string, hideTips bool) *Composer {
+ m := NewComposer("", to, subject, body, hideTips)
m.accounts = accounts
// Find the selected account index
@@ -446,7 +448,29 @@ func (m *Composer) View() tea.View {
signatureLabel = blurredStyle.Render("Signature:")
}
- composerView.WriteString(lipgloss.JoinVertical(lipgloss.Left,
+ tip := ""
+ switch m.focusIndex {
+ case focusFrom:
+ tip = "Select the account to send from."
+ case focusTo:
+ tip = "Enter recipient email addresses."
+ case focusCc:
+ tip = "Carbon copy recipients."
+ case focusBcc:
+ tip = "Blind carbon copy recipients."
+ case focusSubject:
+ tip = "The subject line of your email."
+ case focusBody:
+ tip = "The main content of your email. Markdown and HTML are supported."
+ case focusSignature:
+ tip = "Your email signature. This will be appended to the end of the email."
+ case focusAttachment:
+ tip = "Press Enter to select a file to attach to this email."
+ case focusSend:
+ tip = "Press Enter to send the email."
+ }
+
+ composerViewElements := []string{
"Compose New Email",
fromField,
toFieldView,
@@ -458,8 +482,15 @@ func (m *Composer) View() tea.View {
m.signatureInput.View(),
attachmentStyle.Render(attachmentField),
button,
- helpStyle.Render("Markdown/HTML • tab/shift+tab: navigate • esc: save draft & exit"),
- ))
+ "",
+ }
+
+ if !m.hideTips && tip != "" {
+ composerViewElements = append(composerViewElements, TipStyle.Render("Tip: "+tip))
+ }
+ composerViewElements = append(composerViewElements, helpStyle.Render("Markdown/HTML • tab/shift+tab: navigate • esc: save draft & exit"))
+
+ composerView.WriteString(lipgloss.JoinVertical(lipgloss.Left, composerViewElements...))
// Account picker overlay
if m.showAccountPicker {
@@ -602,8 +633,8 @@ func (m *Composer) ToDraft() config.Draft {
}
// NewComposerFromDraft creates a composer from an existing draft.
-func NewComposerFromDraft(draft config.Draft, accounts []config.Account) *Composer {
- m := NewComposerWithAccounts(accounts, draft.AccountID, draft.To, draft.Subject, draft.Body)
+func NewComposerFromDraft(draft config.Draft, accounts []config.Account, hideTips bool) *Composer {
+ m := NewComposerWithAccounts(accounts, draft.AccountID, draft.To, draft.Subject, draft.Body, hideTips)
m.ccInput.SetValue(draft.Cc)
m.bccInput.SetValue(draft.Bcc)
m.draftID = draft.ID
@@ -13,7 +13,7 @@ func TestComposerUpdate(t *testing.T) {
accounts := []config.Account{
{ID: "account-1", Email: "test@example.com", Name: "Test User"},
}
- composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
+ composer := NewComposerWithAccounts(accounts, "account-1", "", "", "", false)
t.Run("Focus cycling", func(t *testing.T) {
// Initial focus is on the 'To' input (index 1, since From is 0).
@@ -82,7 +82,7 @@ func TestComposerUpdate(t *testing.T) {
t.Run("Send email message", func(t *testing.T) {
// Re-initialize composer for this test
- composer = NewComposerWithAccounts(accounts, "account-1", "", "", "")
+ composer = NewComposerWithAccounts(accounts, "account-1", "", "", "", false)
// Set values for the email fields.
composer.toInput.SetValue("recipient@example.com")
@@ -124,7 +124,7 @@ func TestComposerUpdate(t *testing.T) {
{ID: "account-1", Email: "test1@example.com", Name: "User 1"},
{ID: "account-2", Email: "test2@example.com", Name: "User 2"},
}
- multiComposer := NewComposerWithAccounts(multiAccounts, "account-1", "", "", "")
+ multiComposer := NewComposerWithAccounts(multiAccounts, "account-1", "", "", "", false)
// Move focus to From field
multiComposer.focusIndex = focusFrom
@@ -163,7 +163,7 @@ func TestComposerUpdate(t *testing.T) {
singleAccounts := []config.Account{
{ID: "account-1", Email: "test@example.com"},
}
- singleComposer := NewComposerWithAccounts(singleAccounts, "account-1", "", "", "")
+ singleComposer := NewComposerWithAccounts(singleAccounts, "account-1", "", "", "", false)
// Move focus to From field
singleComposer.focusIndex = focusFrom
@@ -182,7 +182,7 @@ func TestComposerUpdate(t *testing.T) {
{ID: "account-1", Email: "test1@example.com"},
{ID: "account-2", Email: "test2@example.com"},
}
- multiComposer := NewComposerWithAccounts(multiAccounts, "account-1", "", "", "")
+ multiComposer := NewComposerWithAccounts(multiAccounts, "account-1", "", "", "", false)
// Initial focus is on 'To' field
if multiComposer.focusIndex != focusTo {
@@ -222,7 +222,7 @@ func TestComposerGetFromAddress(t *testing.T) {
accounts := []config.Account{
{ID: "account-1", FetchEmail: "test@example.com", Name: "Test User"},
}
- composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
+ composer := NewComposerWithAccounts(accounts, "account-1", "", "", "", false)
fromAddr := composer.getFromAddress()
expected := "Test User <test@example.com>"
@@ -235,7 +235,7 @@ func TestComposerGetFromAddress(t *testing.T) {
accounts := []config.Account{
{ID: "account-1", FetchEmail: "test@example.com"},
}
- composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
+ composer := NewComposerWithAccounts(accounts, "account-1", "", "", "", false)
fromAddr := composer.getFromAddress()
expected := "test@example.com"
@@ -245,7 +245,7 @@ func TestComposerGetFromAddress(t *testing.T) {
})
t.Run("No accounts", func(t *testing.T) {
- composer := NewComposer("", "", "", "")
+ composer := NewComposer("", "", "", "", false)
fromAddr := composer.getFromAddress()
if fromAddr != "" {
@@ -261,7 +261,7 @@ func TestComposerSetSelectedAccount(t *testing.T) {
{ID: "account-2", FetchEmail: "test2@example.com"},
{ID: "account-3", FetchEmail: "test3@example.com"},
}
- composer := NewComposerWithAccounts(accounts, "account-1", "", "", "")
+ composer := NewComposerWithAccounts(accounts, "account-1", "", "", "", false)
composer.SetSelectedAccount("account-3")
if composer.selectedAccountIdx != 2 {
@@ -15,6 +15,7 @@ type Login struct {
showCustom bool // Show custom server fields
isEditMode bool // Whether we're editing an existing account
accountID string
+ hideTips bool
width int
height int
}
@@ -33,9 +34,10 @@ const (
)
// NewLogin creates a new login model for adding accounts.
-func NewLogin() *Login {
+func NewLogin(hideTips bool) *Login {
m := &Login{
- inputs: make([]textinput.Model, inputCount),
+ inputs: make([]textinput.Model, inputCount),
+ hideTips: hideTips,
}
var t textinput.Model
@@ -215,6 +217,28 @@ func (m *Login) View() tea.View {
customHint = "\n" + accountEmailStyle.Render("Custom provider selected - configure server settings below")
}
+ tip := ""
+ switch m.focusIndex {
+ case inputProvider:
+ tip = "Enter your email provider (e.g., gmail, icloud) or 'custom'."
+ case inputName:
+ tip = "The name that will appear on emails you send."
+ case inputEmail:
+ tip = "Your full email address used to log in."
+ case inputFetchEmail:
+ tip = "The email address to fetch messages from (often same as Username)."
+ case inputPassword:
+ tip = "Your password or an app-specific password if using 2FA."
+ case inputIMAPServer:
+ tip = "The server address for receiving emails."
+ case inputIMAPPort:
+ tip = "The port for the IMAP server (usually 993 for SSL)."
+ case inputSMTPServer:
+ tip = "The server address for sending emails."
+ case inputSMTPPort:
+ tip = "The port for the SMTP server (usually 587 for TLS)."
+ }
+
views := []string{
titleStyle.Render(title),
"Enter your email account credentials.",
@@ -237,6 +261,10 @@ func (m *Login) View() tea.View {
)
}
+ views = append(views, "")
+ if !m.hideTips && tip != "" {
+ views = append(views, TipStyle.Render("Tip: "+tip))
+ }
views = append(views, helpStyle.Render("\nenter: save • tab: next field • esc: back to menu"))
return tea.NewView(lipgloss.JoinVertical(lipgloss.Left, views...))
@@ -75,8 +75,8 @@ func (m *Settings) updateMain(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
m.cursor--
}
case "down", "j":
- // Options: 0: Email Accounts, 1: Image Display, 2: Edit Signature
- if m.cursor < 2 {
+ // Options: 0: Email Accounts, 1: Image Display, 2: Edit Signature, 3: Contextual Tips
+ if m.cursor < 3 {
m.cursor++
}
case "enter":
@@ -92,6 +92,11 @@ func (m *Settings) updateMain(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
return m, nil
case 2: // Edit Signature
return m, func() tea.Msg { return GoToSignatureEditorMsg{} }
+ case 3: // Contextual Tips
+ m.cfg.HideTips = !m.cfg.HideTips
+ // Save config immediately
+ _ = config.SaveConfig(m.cfg)
+ return m, nil
}
case "esc":
return m, func() tea.Msg { return GoToChoiceMenuMsg{} }
@@ -189,8 +194,38 @@ func (m *Settings) viewMain() string {
} else {
b.WriteString(accountItemStyle.Render(" " + sigText))
}
+ b.WriteString("\n")
+
+ // Option 3: Contextual Tips
+ tipsStatus := "ON"
+ if m.cfg.HideTips {
+ tipsStatus = "OFF"
+ }
+ tipsText := fmt.Sprintf("Contextual Tips: %s", tipsStatus)
+ if m.cursor == 3 {
+ b.WriteString(selectedAccountItemStyle.Render("> " + tipsText))
+ } else {
+ b.WriteString(accountItemStyle.Render(" " + tipsText))
+ }
b.WriteString("\n\n")
+ if !m.cfg.HideTips {
+ tip := ""
+ switch m.cursor {
+ case 0:
+ tip = "Manage your connected email accounts."
+ case 1:
+ tip = "Toggle displaying images in emails."
+ case 2:
+ tip = "Configure the signature appended to your outgoing emails."
+ case 3:
+ tip = "Toggle displaying helpful contextual tips like this one."
+ }
+ if tip != "" {
+ b.WriteString(TipStyle.Render("Tip: "+tip) + "\n\n")
+ }
+ }
+
b.WriteString(helpStyle.Render("↑/↓: navigate • enter: select/toggle • esc: back"))
return docStyle.Render(b.String())
@@ -28,6 +28,7 @@ var (
BorderBottom(true)
HelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
+ TipStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Italic(true)
SuccessStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)
InfoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)