diff --git a/cmd/root.go b/cmd/root.go index b852c41..40c7688 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -22,6 +22,7 @@ var configFile string func Execute() { cobra.OnInitialize(func() { lib.InitializeConfig(configFile) }) rootCmd.AddCommand(loginCmd) + rootCmd.AddCommand(tuiCmd) addConfigCmd() addFoldersCmd() addNotesCmd() diff --git a/cmd/tui.go b/cmd/tui.go new file mode 100644 index 0000000..5537e1e --- /dev/null +++ b/cmd/tui.go @@ -0,0 +1,16 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + "git.iamthefij.com/iamthefij/imap-notes/tui" +) + +var tuiCmd = &cobra.Command{ + Use: "gui", + Short: "Load GUI for IMAP Notes", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + tui.LaunchTUI() + }, +} diff --git a/go.mod b/go.mod index 94dde22..03f0b00 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,9 @@ go 1.17 require ( git.iamthefij.com/iamthefij/slog v1.3.0 github.com/JohannesKaufmann/html-to-markdown v1.3.3 + github.com/charmbracelet/bubbles v0.10.2 + github.com/charmbracelet/bubbletea v0.19.3 + github.com/charmbracelet/lipgloss v0.4.0 github.com/emersion/go-imap v1.2.0 github.com/russross/blackfriday/v2 v2.1.0 github.com/spf13/cobra v1.3.0 @@ -15,13 +18,24 @@ require ( require ( github.com/PuerkitoBio/goquery v1.5.1 // indirect github.com/andybalholm/cascadia v1.1.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/containerd/console v1.0.2 // indirect github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.5 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.9.0 // indirect github.com/pelletier/go-toml v1.9.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sahilm/fuzzy v0.1.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect diff --git a/go.sum b/go.sum index 47b4459..89ff4ce 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -77,6 +79,13 @@ github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.10.2 h1:VK1Q7nnBMDFTlrMmvBgE9nidtU5udsIcZvFXvjE2Cfk= +github.com/charmbracelet/bubbles v0.10.2/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA= +github.com/charmbracelet/bubbletea v0.19.3 h1:OKeO/Y13rQQqt4snX+lePB0QrnW80UdrMNolnCcmoAw= +github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= +github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g= +github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -93,6 +102,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -260,6 +271,10 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -273,7 +288,13 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= @@ -290,6 +311,13 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8= +github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -297,6 +325,8 @@ github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhEC github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -314,6 +344,9 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +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= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -321,6 +354,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie/v2 v2.5.1 h1:hh70HvG4n3T3MNRJN2z/baxPR8xutxo7JVxyi2svl+s= github.com/sebdah/goldie/v2 v2.5.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= @@ -538,6 +573,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -563,6 +599,7 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJRph7l9XkNveoExlrKYk= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/tui/folders.go b/tui/folders.go new file mode 100644 index 0000000..13b8e7c --- /dev/null +++ b/tui/folders.go @@ -0,0 +1,104 @@ +package tui + +import ( + "git.iamthefij.com/iamthefij/slog" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + + "git.iamthefij.com/iamthefij/imap-notes/lib" +) + +type folderListItem struct { + folder *lib.NoteFolder +} + +func (fi folderListItem) Title() string { + return fi.folder.Name +} + +func (fi folderListItem) Description() string { + return "" +} + +func (fi folderListItem) FilterValue() string { + return fi.folder.Name +} + +type folderListModel struct { + list list.Model + selectedFolder *lib.NoteFolder +} + +func newFolderList() folderListModel { + m := folderListModel{} + + client, err := lib.GetClient() + slog.OnErrFatalf(err, "failed to log on") + + folders, err := client.ListNoteFolders() + slog.OnErrFatalf(err, "failed get folder list") + + folderItems := []list.Item{} + + for _, folder := range folders { + item := folderListItem{folder} + // item := listItem{title: folder.Name, desc: "", folder: folder} + // slog.Debugf("adding %s to list", item.Title()) + folderItems = append(folderItems, item) + } + + itemDelegate := list.NewDefaultDelegate() + itemDelegate.ShowDescription = false + + m.list = list.New(folderItems, itemDelegate, 0, 0) + m.list.Title = "Note Folders" + m.list.SetShowHelp(false) + m.list.DisableQuitKeybindings() + + return m +} + +func (m folderListModel) Init() tea.Cmd { + return nil +} + +func (m folderListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + + case "enter": + folderItem, ok := m.list.SelectedItem().(folderListItem) + if ok { + folder := folderItem.folder + // slog.Debugf("Yay! We selected folder %s", folder.Name) + m.selectedFolder = folder + + return m, nil + } + + slog.Fatalf("selected an unknown item?") + + return m, tea.Quit + } + + case tea.WindowSizeMsg: + m.list.SetSize(msg.Width, msg.Height) + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + + return m, cmd +} + +func (m folderListModel) View() string { + return m.list.View() +} + +func LaunchFolderView() { + p := tea.NewProgram(newFolderList(), tea.WithAltScreen()) + slog.OnErrFatalf(p.Start(), "error running TUI") +} diff --git a/tui/main.go b/tui/main.go new file mode 100644 index 0000000..71a4faa --- /dev/null +++ b/tui/main.go @@ -0,0 +1,54 @@ +package tui + +import ( + "git.iamthefij.com/iamthefij/slog" + tea "github.com/charmbracelet/bubbletea" +) + +type ActiveSection int + +const ( + FolderSelect ActiveSection = iota + NoteSelect + NotePager +) + +func LaunchTUI() { + // LaunchFolderView() + // p := tea.NewProgram(initialModel(), tea.WithAltScreen()) + mvm := NewMultiViewModel([]tea.Model{newFolderList(), newNoteList()}, nil) + mvm.OnColumnSelect = func(baseModel *MultiViewModel, msg tea.Msg) tea.Cmd { + switch baseModel.GetActiveIndex() { + case int(FolderSelect): + folderView := (*baseModel.GetActiveView()).(folderListModel) + noteView := (*baseModel.NextView()).(noteListModel) + noteView.SetFolder(folderView.selectedFolder) + baseModel.SetActiveView(noteView) + + case int(NoteSelect): + noteView := (*baseModel.GetActiveView()).(noteListModel) + markdown, err := noteView.selectedNote.ReadMarkdown() + slog.OnErrFatalf(err, "failed getting markdown for selected note") + + slog.Debugf("got markdown: %s", markdown) + + pager := NewPagerModel(markdown) + baseModel.SetMainView(pager) + baseModel.NextView() + } + + return nil + } + mvm.OnColumnBack = func(baseModel *MultiViewModel, msg tea.Msg) tea.Cmd { + if baseModel.GetActiveIndex() == int(NotePager) { + baseModel.ClearMainView() + } + + baseModel.PreviousView() + + return nil + } + + p := tea.NewProgram(mvm, tea.WithAltScreen()) + slog.OnErrFatalf(p.Start(), "error running TUI") +} diff --git a/tui/mutliview.go b/tui/mutliview.go new file mode 100644 index 0000000..04af4ee --- /dev/null +++ b/tui/mutliview.go @@ -0,0 +1,213 @@ +package tui + +import ( + // "git.iamthefij.com/iamthefij/slog" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type MultiViewModel struct { + // Internal state + columns []tea.Model + activeIndex int + mainModel tea.Model + lastWindowSizeMsg tea.WindowSizeMsg + + // Styles for system + BaseStyle lipgloss.Style + ActiveStyle lipgloss.Style + NumVisibleSections int + SideColumnWidth int + + // Select functionality + SelectKeys []string + OnColumnSelect func(baseModel *MultiViewModel, msg tea.Msg) tea.Cmd + BackKeys []string + OnColumnBack func(baseModel *MultiViewModel, msg tea.Msg) tea.Cmd + QuitKeys []string +} + +func NewMultiViewModel(columns []tea.Model, main tea.Model) *MultiViewModel { + baseStyle := lipgloss.NewStyle().Margin(1) + + return &MultiViewModel{ + columns: columns, + activeIndex: 0, + mainModel: main, + + BaseStyle: baseStyle, + ActiveStyle: baseStyle.Copy().UnsetMargins().BorderStyle(lipgloss.NormalBorder()).BorderForeground( + lipgloss.Color("21"), + ), + NumVisibleSections: 2, + SideColumnWidth: 40, + + SelectKeys: []string{tea.KeyEnter.String(), tea.KeyTab.String()}, + OnColumnSelect: func(baseModel *MultiViewModel, msg tea.Msg) tea.Cmd { + baseModel.NextView() + + return nil + }, + BackKeys: []string{tea.KeyEsc.String(), tea.KeyShiftTab.String()}, + OnColumnBack: func(baseModel *MultiViewModel, msg tea.Msg) tea.Cmd { + baseModel.PreviousView() + + return nil + }, + QuitKeys: []string{"q"}, + } +} + +func (m MultiViewModel) GetActiveIndex() int { + return m.activeIndex +} + +func (m MultiViewModel) GetActiveView() *tea.Model { + if m.activeIndex < len(m.columns) { + return &m.columns[m.activeIndex] + } + + return &m.mainModel +} + +func (m *MultiViewModel) SetActiveView(viewModel tea.Model) { + if m.activeIndex < len(m.columns) { + m.columns[m.activeIndex] = viewModel + } else { + m.mainModel = viewModel + } +} + +func (m *MultiViewModel) NextView() *tea.Model { + if m.activeIndex < len(m.columns) { + m.activeIndex++ + } + + return m.GetActiveView() +} + +func (m *MultiViewModel) PreviousView() *tea.Model { + if m.activeIndex > 0 { + m.activeIndex-- + } + + return m.GetActiveView() +} + +func (m *MultiViewModel) SetMainView(mainView tea.Model) { + top, right, bottom, left := m.BaseStyle.GetMargin() + m.mainModel, _ = mainView.Update(tea.WindowSizeMsg{ + Height: m.lastWindowSizeMsg.Height - top - bottom, + Width: m.MainSectionWidth() - right - left, + }) +} + +func (m *MultiViewModel) ClearMainView() { + m.mainModel = nil +} + +func (m MultiViewModel) Init() tea.Cmd { return nil } + +func (m MultiViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + // Pass key strokes to active view + var updatedView tea.Model + updatedView, cmd = (*m.GetActiveView()).Update(msg) + + switch { + case m.activeIndex < 0: + panic("where are we!?") + case m.activeIndex < len(m.columns): + m.columns[m.activeIndex] = updatedView + case m.activeIndex >= len(m.columns): + m.mainModel = updatedView + } + + // Handle global quit + for _, quitKey := range m.QuitKeys { + if quitKey == msg.String() { + return m, tea.Quit + } + } + + // Handle select key + for _, selectKey := range m.SelectKeys { + if selectKey == msg.String() { + m.OnColumnSelect(&m, msg) + + break + } + } + // Handle back key + for _, selectKey := range m.BackKeys { + if selectKey == msg.String() { + m.OnColumnBack(&m, msg) + + break + } + } + + case tea.WindowSizeMsg: + // Pass size message to all sub views + top, right, bottom, left := m.BaseStyle.GetMargin() + // top, right, bottom, left := m.BaseStyle.GetBorderStyle().GetTopSize + for i, model := range m.columns { + m.columns[i], _ = model.Update(tea.WindowSizeMsg{ + Height: msg.Height - top - bottom, + Width: m.SideColumnWidth - right - left, + }) + } + + if m.mainModel != nil { + m.mainModel, _ = m.mainModel.Update(tea.WindowSizeMsg{ + Height: msg.Height - top - bottom, + Width: m.MainSectionWidth() - right - left, + }) + } + + m.lastWindowSizeMsg = msg + } + + return m, cmd +} + +func (m MultiViewModel) MainSectionWidth() int { + return m.lastWindowSizeMsg.Width - (m.SideColumnWidth * m.NumVisibleSections) +} + +func (m MultiViewModel) View() string { + allModels := []tea.Model{} + allModels = append(allModels, m.columns...) + + if m.mainModel != nil { + allModels = append(allModels, m.mainModel) + } + + visibleContent := []string{} + + // Add visible columns + sideColStyle := m.BaseStyle.Copy().Width(m.SideColumnWidth) + mainStyle := m.BaseStyle.Copy().Width(m.MainSectionWidth()) + + for i, column := range allModels { + // Get the correct style + style := sideColStyle + if i >= len(m.columns) { + style = mainStyle + } + + isVisible := m.NumVisibleSections < 1 || i < m.activeIndex+m.NumVisibleSections + if isVisible { + if i == m.activeIndex { + visibleContent = append(visibleContent, m.ActiveStyle.Inherit(style).Render(column.View())) + } else { + visibleContent = append(visibleContent, style.Render(column.View())) + } + } + } + + return lipgloss.JoinHorizontal(lipgloss.Top, visibleContent...) +} diff --git a/tui/notes.go b/tui/notes.go new file mode 100644 index 0000000..2ab5d4f --- /dev/null +++ b/tui/notes.go @@ -0,0 +1,109 @@ +package tui + +import ( + "fmt" + + "git.iamthefij.com/iamthefij/slog" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + + "git.iamthefij.com/iamthefij/imap-notes/lib" +) + +type noteListItem struct { + note *lib.Note +} + +func (fi noteListItem) Title() string { + return fi.note.Name() +} + +func (fi noteListItem) Description() string { + return "some body description" +} + +func (fi noteListItem) FilterValue() string { + return fi.note.Name() +} + +type noteListModel struct { + list list.Model + selectedFolder *lib.NoteFolder + selectedNote *lib.Note +} + +func newNoteList() noteListModel { + m := noteListModel{} + + noteItems := []list.Item{} + + m.list = list.New(noteItems, list.NewDefaultDelegate(), 0, 0) + m.list.Title = "Notes" + m.list.SetShowHelp(false) + m.list.DisableQuitKeybindings() + + return m +} + +func (m noteListModel) Init() tea.Cmd { + return nil +} + +func (m *noteListModel) SetFolder(folder *lib.NoteFolder) error { + m.selectedFolder = folder + + noteItems := []list.Item{} + + notes, err := folder.ListNotes() + if err != nil { + return fmt.Errorf("failed to get notes for list view: %w", err) + } + + for _, note := range notes { + noteItems = append(noteItems, noteListItem{note}) + } + + m.list.SetItems(noteItems) + + return nil +} + +func (m noteListModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "enter": + noteItem, ok := m.list.SelectedItem().(noteListItem) + if ok { + note := noteItem.note + // slog.Debugf("Yay! We selected note %s", note.Name) + m.selectedNote = note + + return m, nil + } + + slog.Fatalf("selected an unknown item?") + + return m, tea.Quit + } + + case tea.WindowSizeMsg: + m.list.SetSize(msg.Width, msg.Height) + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + + return m, cmd +} + +func (m noteListModel) View() string { + return m.list.View() +} + +func LaunchNoteView() { + p := tea.NewProgram(newFolderList(), tea.WithAltScreen()) + slog.OnErrFatalf(p.Start(), "error running TUI") +} diff --git a/tui/pager.go b/tui/pager.go new file mode 100644 index 0000000..18b9923 --- /dev/null +++ b/tui/pager.go @@ -0,0 +1,43 @@ +package tui + +import ( + // "git.iamthefij.com/iamthefij/slog" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" +) + +type pagerModel struct { + content string + viewport viewport.Model +} + +func NewPagerModel(content string) pagerModel { + return pagerModel{ + content: content, + } +} + +func (m pagerModel) Init() tea.Cmd { return nil } + +func (m pagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.String() == "e" { + // edit! + } + case tea.WindowSizeMsg: + m.viewport = viewport.New(msg.Width, msg.Height) + m.viewport.SetContent(m.content) + } + + var cmd tea.Cmd + // m.viewport, cmd = m.viewport.Update(msg) + + return m, cmd +} + +func (m pagerModel) View() string { + // slog.Debugf("viewport view? %s", m.viewport.View()) + // return m.viewport.View() + return m.content +}