diff --git a/config/config.go b/config/config.go index d94a3a7a41db0e9e5454af151bc62f5d83f17a07..5f483d57c8eea7833e97a888e737219dae89ebf4 100644 --- a/config/config.go +++ b/config/config.go @@ -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. diff --git a/go.mod b/go.mod index 9d1c2843f0e2b93a650fea5c6aca3a7bf2abe1ea..bf3b5d650b81c8daf99b1b0a6c44746932bc884e 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index eb6faa89107442babe9a267bf8ccee80aa4fac9b..770daab128109234e9f0b68a1ea6a6490e848ee7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 01e55b173a8ecc845e445aa5cae3a1187e8ef35b..dc3174ea44f0a07c8dba0469b6bc6190e6e8cc1c 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/tui/composer.go b/tui/composer.go index fbffe54eb43d18fd83b4cd2079d8a6d597e914e5..9cb2447c4e4910f6d905ba598a087f05e03e876c 100644 --- a/tui/composer.go +++ b/tui/composer.go @@ -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 diff --git a/tui/composer_test.go b/tui/composer_test.go index ca3a9b8fa0642bb5c424654972838479396ce198..a650857653ff35e7cc646666d5448c1c56499aa9 100644 --- a/tui/composer_test.go +++ b/tui/composer_test.go @@ -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 " @@ -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 { diff --git a/tui/login.go b/tui/login.go index d6d85d13b268d083d87b1bde8e5ce444861b40c7..95260b456d659701301f99f7cf0d8983284ee87d 100644 --- a/tui/login.go +++ b/tui/login.go @@ -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...)) diff --git a/tui/settings.go b/tui/settings.go index c363d1b0c443736255402f28ed1687ff27c7149a..9c454f2905b451887e50e947a1b622d368fece04 100644 --- a/tui/settings.go +++ b/tui/settings.go @@ -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()) diff --git a/tui/styles.go b/tui/styles.go index c987d3192d68f62c48db9cb9eaf7fa94b095f7bd..9bd3ce1f03a8dcd821c8b80f8b60b523a2d34fa8 100644 --- a/tui/styles.go +++ b/tui/styles.go @@ -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)