mirror of
https://github.com/ivabus/pop
synced 2024-11-10 02:25:19 +03:00
feat: email tui
This commit is contained in:
parent
2c740644ee
commit
5c247b107d
45
attachments.go
Normal file
45
attachments.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/list"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type attachment string
|
||||||
|
|
||||||
|
func (a attachment) FilterValue() string {
|
||||||
|
return string(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
type attachmentDelegate struct {
|
||||||
|
focused bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d attachmentDelegate) Height() int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d attachmentDelegate) Spacing() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d attachmentDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
|
||||||
|
path := filepath.Base(item.(attachment).FilterValue())
|
||||||
|
style := textStyle
|
||||||
|
if m.Index() == index && d.focused {
|
||||||
|
style = activeTextStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Index() == index {
|
||||||
|
_, _ = w.Write([]byte(style.Render("• " + path)))
|
||||||
|
} else {
|
||||||
|
_, _ = w.Write([]byte(style.Render(" " + path)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d attachmentDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
28
go.mod
28
go.mod
|
@ -1,3 +1,31 @@
|
||||||
module github.com/maaslalani/email
|
module github.com/maaslalani/email
|
||||||
|
|
||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/charmbracelet/bubbles v0.16.1
|
||||||
|
github.com/spf13/cobra v1.7.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/bubbletea v0.24.3-0.20230609163353-b80eb8303bba // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v0.7.1 // indirect
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
|
github.com/muesli/termenv v0.15.1 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
golang.org/x/term v0.6.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
)
|
||||||
|
|
53
go.sum
53
go.sum
|
@ -0,0 +1,53 @@
|
||||||
|
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/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
|
||||||
|
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
|
||||||
|
github.com/charmbracelet/bubbletea v0.24.1 h1:LpdYfnu+Qc6XtvMz6d/6rRY71yttHTP5HtrjMgWvixc=
|
||||||
|
github.com/charmbracelet/bubbletea v0.24.1/go.mod h1:rK3g/2+T8vOSEkNHvtq40umJpeVYDn6bLaqbgzhL/hg=
|
||||||
|
github.com/charmbracelet/bubbletea v0.24.3-0.20230609163353-b80eb8303bba h1:hWjlqzPiG3dWN5KZUAKKnqz/R8BlYqX1Aw+nk7riQss=
|
||||||
|
github.com/charmbracelet/bubbletea v0.24.3-0.20230609163353-b80eb8303bba/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
|
||||||
|
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
|
||||||
|
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
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/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||||
|
github.com/mattn/go-isatty v0.0.18/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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
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/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
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.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
|
||||||
|
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
|
||||||
|
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||||
|
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||||
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
4
go.work.sum
Normal file
4
go.work.sum
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||||
|
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
67
keymap.go
Normal file
67
keymap.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/bubbles/key"
|
||||||
|
|
||||||
|
// KeyMap represents the key bindings for the application.
|
||||||
|
type KeyMap struct {
|
||||||
|
NextInput key.Binding
|
||||||
|
PrevInput key.Binding
|
||||||
|
Send key.Binding
|
||||||
|
Attach key.Binding
|
||||||
|
Unattach key.Binding
|
||||||
|
Quit key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultKeybinds() KeyMap {
|
||||||
|
return KeyMap{
|
||||||
|
NextInput: key.NewBinding(
|
||||||
|
key.WithKeys("tab", "ctrl+n"),
|
||||||
|
key.WithHelp("tab", "next"),
|
||||||
|
),
|
||||||
|
PrevInput: key.NewBinding(
|
||||||
|
key.WithKeys("shift+tab", "ctrl+p"),
|
||||||
|
),
|
||||||
|
Send: key.NewBinding(
|
||||||
|
key.WithKeys("ctrl+d", "enter"),
|
||||||
|
key.WithHelp("enter", "send"),
|
||||||
|
key.WithDisabled(),
|
||||||
|
),
|
||||||
|
Attach: key.NewBinding(
|
||||||
|
key.WithKeys("a"),
|
||||||
|
key.WithHelp("a", "attach file"),
|
||||||
|
key.WithDisabled(),
|
||||||
|
),
|
||||||
|
Unattach: key.NewBinding(
|
||||||
|
key.WithKeys("x"),
|
||||||
|
key.WithHelp("x", "remove"),
|
||||||
|
key.WithDisabled(),
|
||||||
|
),
|
||||||
|
Quit: key.NewBinding(
|
||||||
|
key.WithKeys("ctrl+c"),
|
||||||
|
key.WithHelp("ctrl+c", "quit"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KeyMap) ShortHelp() []key.Binding {
|
||||||
|
return []key.Binding{
|
||||||
|
k.NextInput,
|
||||||
|
k.Quit,
|
||||||
|
k.Attach,
|
||||||
|
k.Unattach,
|
||||||
|
k.Send,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k KeyMap) FullHelp() [][]key.Binding {
|
||||||
|
return [][]key.Binding{
|
||||||
|
{k.NextInput, k.Send, k.Attach, k.Unattach, k.Quit},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) updateKeymap() {
|
||||||
|
m.keymap.Attach.SetEnabled(m.state == editingAttachments)
|
||||||
|
canSend := m.From.Value() != "" && m.To.Value() != "" && m.Subject.Value() != "" && m.Body.Value() != ""
|
||||||
|
m.keymap.Send.SetEnabled(canSend && m.state != editingBody)
|
||||||
|
m.keymap.Unattach.SetEnabled(m.state == editingAttachments && len(m.Attachments.Items()) > 0)
|
||||||
|
}
|
29
main.go
Normal file
29
main.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "email",
|
||||||
|
Short: "email is a command line interface for sending emails.",
|
||||||
|
Long: `email is a command line interface for sending emails.`,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
p := tea.NewProgram(NewModel())
|
||||||
|
_, err := p.Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := rootCmd.Execute()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
282
model.go
Normal file
282
model.go
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/filepicker"
|
||||||
|
"github.com/charmbracelet/bubbles/help"
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
"github.com/charmbracelet/bubbles/list"
|
||||||
|
"github.com/charmbracelet/bubbles/textarea"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State int
|
||||||
|
|
||||||
|
const (
|
||||||
|
editingFrom State = iota
|
||||||
|
editingTo
|
||||||
|
editingSubject
|
||||||
|
editingBody
|
||||||
|
editingAttachments
|
||||||
|
pickingFile
|
||||||
|
sendingEmail
|
||||||
|
)
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
// state represents the current state of the application.
|
||||||
|
state State
|
||||||
|
|
||||||
|
// From represents the sender's email address.
|
||||||
|
From textinput.Model
|
||||||
|
// To represents the recipient's email address.
|
||||||
|
// This can be a comma-separated list of addresses.
|
||||||
|
To textinput.Model
|
||||||
|
// Subject represents the email's subject.
|
||||||
|
Subject textinput.Model
|
||||||
|
// Body represents the email's body.
|
||||||
|
// This can be written in markdown and will be converted to HTML.
|
||||||
|
Body textarea.Model
|
||||||
|
// Attachments represents the email's attachments.
|
||||||
|
// This is a list of file paths which are picked with a filepicker.
|
||||||
|
Attachments list.Model
|
||||||
|
|
||||||
|
// filepicker is used to pick file attachments.
|
||||||
|
filepicker filepicker.Model
|
||||||
|
help help.Model
|
||||||
|
keymap KeyMap
|
||||||
|
quitting bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModel() Model {
|
||||||
|
from := textinput.New()
|
||||||
|
from.Prompt = "From "
|
||||||
|
from.Placeholder = "me@example.com"
|
||||||
|
from.PromptStyle = labelStyle.Copy()
|
||||||
|
from.PromptStyle = activeLabelStyle
|
||||||
|
from.TextStyle = activeTextStyle
|
||||||
|
from.Cursor.Style = cursorStyle
|
||||||
|
from.PlaceholderStyle = placeholderStyle
|
||||||
|
from.Focus()
|
||||||
|
|
||||||
|
to := textinput.New()
|
||||||
|
to.Prompt = "To "
|
||||||
|
to.PromptStyle = labelStyle.Copy()
|
||||||
|
to.Cursor.Style = cursorStyle
|
||||||
|
to.PlaceholderStyle = placeholderStyle
|
||||||
|
to.Placeholder = "you@example.com"
|
||||||
|
|
||||||
|
subject := textinput.New()
|
||||||
|
subject.Prompt = "Subject "
|
||||||
|
subject.PromptStyle = labelStyle.Copy()
|
||||||
|
subject.Cursor.Style = cursorStyle
|
||||||
|
subject.PlaceholderStyle = placeholderStyle
|
||||||
|
subject.Placeholder = "Hello!"
|
||||||
|
|
||||||
|
body := textarea.New()
|
||||||
|
body.Placeholder = "# Hi"
|
||||||
|
body.ShowLineNumbers = false
|
||||||
|
body.FocusedStyle.CursorLine = activeTextStyle
|
||||||
|
body.FocusedStyle.Prompt = activeLabelStyle
|
||||||
|
body.FocusedStyle.Text = activeTextStyle
|
||||||
|
body.FocusedStyle.Placeholder = placeholderStyle
|
||||||
|
body.BlurredStyle.CursorLine = textStyle
|
||||||
|
body.BlurredStyle.Prompt = labelStyle
|
||||||
|
body.BlurredStyle.Text = textStyle
|
||||||
|
body.BlurredStyle.Placeholder = placeholderStyle
|
||||||
|
body.Cursor.Style = cursorStyle
|
||||||
|
body.Blur()
|
||||||
|
|
||||||
|
attachments := list.New([]list.Item{}, attachmentDelegate{}, 0, 3)
|
||||||
|
attachments.DisableQuitKeybindings()
|
||||||
|
attachments.SetShowTitle(true)
|
||||||
|
attachments.Title = "Attachments"
|
||||||
|
attachments.Styles.Title = labelStyle
|
||||||
|
attachments.Styles.TitleBar = labelStyle
|
||||||
|
attachments.SetShowHelp(false)
|
||||||
|
attachments.SetShowStatusBar(false)
|
||||||
|
attachments.SetStatusBarItemName("attachment", "attachments")
|
||||||
|
attachments.SetShowPagination(false)
|
||||||
|
|
||||||
|
picker := filepicker.New()
|
||||||
|
picker.CurrentDirectory, _ = os.UserHomeDir()
|
||||||
|
|
||||||
|
return Model{
|
||||||
|
state: editingFrom,
|
||||||
|
From: from,
|
||||||
|
To: to,
|
||||||
|
Subject: subject,
|
||||||
|
Body: body,
|
||||||
|
Attachments: attachments,
|
||||||
|
filepicker: picker,
|
||||||
|
help: help.New(),
|
||||||
|
keymap: DefaultKeybinds(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Init() tea.Cmd {
|
||||||
|
return tea.Batch(
|
||||||
|
m.From.Cursor.BlinkCmd(),
|
||||||
|
m.filepicker.Init(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.From.Width = msg.Width - 2
|
||||||
|
m.To.Width = msg.Width - 2
|
||||||
|
m.Subject.Width = msg.Width - 2
|
||||||
|
m.Body.SetWidth(msg.Width - 2)
|
||||||
|
m.Attachments.SetWidth(msg.Width - 2)
|
||||||
|
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.keymap.NextInput):
|
||||||
|
m.blurInputs()
|
||||||
|
switch m.state {
|
||||||
|
case editingFrom:
|
||||||
|
m.state = editingTo
|
||||||
|
m.To.Focus()
|
||||||
|
case editingTo:
|
||||||
|
m.state = editingSubject
|
||||||
|
case editingSubject:
|
||||||
|
m.state = editingBody
|
||||||
|
case editingBody:
|
||||||
|
m.state = editingAttachments
|
||||||
|
case editingAttachments:
|
||||||
|
m.state = editingFrom
|
||||||
|
}
|
||||||
|
m.focusActiveInput()
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keymap.PrevInput):
|
||||||
|
m.blurInputs()
|
||||||
|
switch m.state {
|
||||||
|
case editingFrom:
|
||||||
|
m.state = editingAttachments
|
||||||
|
case editingTo:
|
||||||
|
m.state = editingFrom
|
||||||
|
case editingSubject:
|
||||||
|
m.state = editingTo
|
||||||
|
case editingBody:
|
||||||
|
m.state = editingSubject
|
||||||
|
case editingAttachments:
|
||||||
|
m.state = editingBody
|
||||||
|
}
|
||||||
|
m.focusActiveInput()
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keymap.Send):
|
||||||
|
case key.Matches(msg, m.keymap.Attach):
|
||||||
|
m.state = pickingFile
|
||||||
|
case key.Matches(msg, m.keymap.Unattach):
|
||||||
|
m.Attachments.RemoveItem(m.Attachments.Index())
|
||||||
|
m.Attachments.SetHeight(max(len(m.Attachments.Items()), 1) + 2)
|
||||||
|
case key.Matches(msg, m.keymap.Quit):
|
||||||
|
m.quitting = true
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.updateKeymap()
|
||||||
|
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.From, cmd = m.From.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
m.To, cmd = m.To.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
m.Subject, cmd = m.Subject.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
m.Body, cmd = m.Body.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
m.filepicker, cmd = m.filepicker.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
if m.state == pickingFile {
|
||||||
|
if didSelect, path := m.filepicker.DidSelectFile(msg); didSelect {
|
||||||
|
m.Attachments.InsertItem(0, attachment(path))
|
||||||
|
m.Attachments.SetHeight(len(m.Attachments.Items()) + 2)
|
||||||
|
m.state = editingAttachments
|
||||||
|
m.updateKeymap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.state == editingAttachments {
|
||||||
|
m.Attachments, cmd = m.Attachments.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
m.help, cmd = m.help.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) blurInputs() {
|
||||||
|
m.From.Blur()
|
||||||
|
m.To.Blur()
|
||||||
|
m.Subject.Blur()
|
||||||
|
m.Body.Blur()
|
||||||
|
m.From.PromptStyle = labelStyle
|
||||||
|
m.To.PromptStyle = labelStyle
|
||||||
|
m.Subject.PromptStyle = labelStyle
|
||||||
|
m.From.TextStyle = textStyle
|
||||||
|
m.To.TextStyle = textStyle
|
||||||
|
m.Subject.TextStyle = textStyle
|
||||||
|
m.Attachments.Styles.Title = labelStyle
|
||||||
|
m.Attachments.SetDelegate(attachmentDelegate{false})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) focusActiveInput() {
|
||||||
|
switch m.state {
|
||||||
|
case editingFrom:
|
||||||
|
m.From.PromptStyle = activeLabelStyle
|
||||||
|
m.From.TextStyle = activeTextStyle
|
||||||
|
m.From.Focus()
|
||||||
|
m.From.CursorEnd()
|
||||||
|
case editingTo:
|
||||||
|
m.To.PromptStyle = activeLabelStyle
|
||||||
|
m.To.TextStyle = activeTextStyle
|
||||||
|
m.To.Focus()
|
||||||
|
m.To.CursorEnd()
|
||||||
|
case editingSubject:
|
||||||
|
m.Subject.PromptStyle = activeLabelStyle
|
||||||
|
m.Subject.TextStyle = activeTextStyle
|
||||||
|
m.Subject.Focus()
|
||||||
|
m.Subject.CursorEnd()
|
||||||
|
case editingBody:
|
||||||
|
m.Body.Focus()
|
||||||
|
m.Body.CursorEnd()
|
||||||
|
case editingAttachments:
|
||||||
|
m.Attachments.Styles.Title = activeLabelStyle
|
||||||
|
m.Attachments.SetDelegate(attachmentDelegate{true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) View() string {
|
||||||
|
if m.quitting {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.state == pickingFile {
|
||||||
|
return "\n" + activeLabelStyle.Render("Attachments") +
|
||||||
|
"\n\n" + m.filepicker.View()
|
||||||
|
}
|
||||||
|
|
||||||
|
var s strings.Builder
|
||||||
|
|
||||||
|
s.WriteString(m.From.View())
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(m.To.View())
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(m.Subject.View())
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
s.WriteString(m.Body.View())
|
||||||
|
s.WriteString("\n\n")
|
||||||
|
s.WriteString(m.Attachments.View())
|
||||||
|
s.WriteString("\n")
|
||||||
|
s.WriteString(m.help.View(m.keymap))
|
||||||
|
|
||||||
|
return s.String()
|
||||||
|
}
|
20
style.go
Normal file
20
style.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
|
const accentColor = lipgloss.Color("99")
|
||||||
|
const whiteColor = lipgloss.Color("255")
|
||||||
|
const grayColor = lipgloss.Color("241")
|
||||||
|
const darkGrayColor = lipgloss.Color("236")
|
||||||
|
const lightGrayColor = lipgloss.Color("247")
|
||||||
|
|
||||||
|
var (
|
||||||
|
activeTextStyle = lipgloss.NewStyle().Foreground(whiteColor)
|
||||||
|
textStyle = lipgloss.NewStyle().Foreground(lightGrayColor)
|
||||||
|
|
||||||
|
activeLabelStyle = lipgloss.NewStyle().Foreground(accentColor)
|
||||||
|
labelStyle = lipgloss.NewStyle().Foreground(grayColor)
|
||||||
|
|
||||||
|
placeholderStyle = lipgloss.NewStyle().Foreground(darkGrayColor)
|
||||||
|
cursorStyle = lipgloss.NewStyle().Foreground(whiteColor)
|
||||||
|
)
|
Loading…
Reference in a new issue