diff --git a/tui/.gitignore b/tui/.gitignore new file mode 100644 index 0000000..9239013 --- /dev/null +++ b/tui/.gitignore @@ -0,0 +1 @@ +vex.log diff --git a/tui/http.go b/tui/cmd/http.go similarity index 95% rename from tui/http.go rename to tui/cmd/http.go index 1dd4404..696ae50 100644 --- a/tui/http.go +++ b/tui/cmd/http.go @@ -9,6 +9,7 @@ import ( "net/http" tea "github.com/charmbracelet/bubbletea" + "github.com/zoryia/vex/tui/models" ) type statusMsg int @@ -133,7 +134,7 @@ func register(username string, password string, email string) tea.Cmd { } } -type getEntriesSuccessMsg []Entry +type getEntriesSuccessMsg []models.Entry func getEntries(jwt *string) tea.Cmd { return func() tea.Msg { @@ -148,7 +149,7 @@ func getEntries(jwt *string) tea.Cmd { if err != nil { return httpErrorMsg(err) } - var entries []Entry + var entries []models.Entry err = json.Unmarshal(data, &entries) if err != nil { return httpErrorMsg(err) @@ -157,7 +158,7 @@ func getEntries(jwt *string) tea.Cmd { } } -type getFeedsSuccessMsg []Feed +type getFeedsSuccessMsg []models.Feed func getFeeds(jwt *string) tea.Cmd { return func() tea.Msg { @@ -171,7 +172,7 @@ func getFeeds(jwt *string) tea.Cmd { if err != nil { return httpErrorMsg(err) } - var feeds []Feed + var feeds []models.Feed err = json.Unmarshal(data, &feeds) if err != nil { return httpErrorMsg(err) diff --git a/tui/main.go b/tui/cmd/main.go similarity index 67% rename from tui/main.go rename to tui/cmd/main.go index 7f3b856..381c380 100644 --- a/tui/main.go +++ b/tui/cmd/main.go @@ -1,8 +1,8 @@ package main import ( - "fmt" - "net/http" + "log" + _ "log" "os" "strings" "time" @@ -10,32 +10,27 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" huh "github.com/charmbracelet/huh" + . "github.com/zoryia/vex/tui/models" + . "github.com/zoryia/vex/tui/pages" + "github.com/zoryia/vex/tui/pages/auth" + "github.com/zoryia/vex/tui/pages/preview" ) -func (e Entry) FilterValue() string { - return e.ArticleTitle -} - -func (e Entry) Title() string { - return e.ArticleTitle -} - -func (e Entry) Description() string { - return fmt.Sprintf("%s", "my desc") // TODO: real description -} - type Model struct { list list.Model textInput textinput.Model err error - auth Auth + auth auth.Model page VexPage query string feeds []Feed entries []Entry tags []string + keys *ListKeyMap + Preview preview.Model } func New() *Model { @@ -44,12 +39,12 @@ func New() *Model { ti.Focus() ti.CharLimit = 156 ti.Width = 56 - return &Model{textInput: ti, auth: Auth{loginForm: getLoginForm(), registerForm: getRegisterForm(), jwt: new(string)}, page: LOGIN} + return &Model{textInput: ti, auth: auth.New(), page: ENTRIES, keys: NewListKeyMap(), Preview: preview.Model{Viewport: viewport.New(0, 0)}} } func (m Model) getEverything() tea.Cmd { return func() tea.Msg { - return tea.Batch(getEntries(m.auth.jwt)) // getTags, getFeeds) + return tea.Batch(getEntries(m.auth.Jwt)) // getTags, getFeeds) } } @@ -59,14 +54,14 @@ func (m *Model) initList(width int, height int) { m.list.SetFilteringEnabled(false) var f = Feed{Id: "1", Tags: []string{"Devops", "Kubernetes"}, Name: "zwindler", Url: "zwindler.blog", FaviconUrl: "zwindler.blog.favicon"} m.list.SetItems([]list.Item{ - Entry{Id: "1", ArticleTitle: "yay", Content: "ouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f}, + Entry{Id: "1", ArticleTitle: "yay", Content: "ouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouinouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f}, Entry{Id: "2", ArticleTitle: "grrrrr", Content: "ouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f}, Entry{Id: "3", ArticleTitle: "my life is pain", Content: "ouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f}, }) } func (m Model) Init() tea.Cmd { - return tea.Batch(checkServer, m.auth.loginForm.Init(), m.auth.registerForm.Init(), checkJwt(m.auth.jwt)) + return tea.Batch(m.auth.LoginForm.Init(), m.auth.RegisterForm.Init(), checkJwt(m.auth.Jwt)) } @@ -152,38 +147,26 @@ func (m *Model) deleteWordBackward() { } } -const url = "localhost:3000" - -func checkServer() tea.Msg { - c := &http.Client{ - Timeout: 10 * time.Second, - } - res, err := c.Get(url) - if err != nil { - return errMsg{err} - } - defer res.Body.Close() // nolint:errcheck - - return statusMsg(res.StatusCode) - -} - func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + + var cmds []tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: m.initList(msg.Width, msg.Height) + m.Preview.Viewport.Width = msg.Width + m.Preview.Viewport.Height = msg.Height - m.Preview.VerticalMarginHeight() case invalidJwtMsg: - m.auth.jwt = new(string) + m.auth.Jwt = new(string) m.page = LOGIN return m, nil case loginSuccessMsg: - *m.auth.jwt = msg.string + *m.auth.Jwt = msg.string m.page = FEEDS return m, m.getEverything() case registerSuccessMsg: - *m.auth.jwt = msg.string + *m.auth.Jwt = msg.string m.page = FEEDS return m, m.getEverything() @@ -205,22 +188,50 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if len(words) > 0 && (strings.HasPrefix(words[len(words)-1], "tag:") || strings.HasPrefix(words[len(words)-1], "feed:")) { m.deleteWordBackward() } + case key.Matches(msg, m.keys.IgnoreToggle) && m.page == "FEEDS": + // TODO: ignore the post + return m, nil + + case key.Matches(msg, m.keys.ReadToggle): + // TODO: mark as read + return m, nil + + case key.Matches(msg, m.keys.ReadLaterToggle): + // TODO: add to read later + return m, nil + + case key.Matches(msg, m.keys.BookmarkToggle): + // TODO: toggle bookmark + return m, nil + + case key.Matches(msg, m.keys.Query): + // TODO: launch query input + return m, nil + + case key.Matches(msg, m.keys.PreviewPost): + var e = m.list.SelectedItem() + + entry := e.(Entry) + m.Preview.Entry = entry + m.Preview.Viewport.SetContent(entry.Content) + m.page = PREVIEW + log.Print(entry.Content) + log.Print(m.Preview.Viewport.VisibleLineCount()) } } - var cmds []tea.Cmd // Process the form // LOGIN if m.page == LOGIN { - form, cmd := m.auth.loginForm.Update(msg) + form, cmd := m.auth.LoginForm.Update(msg) if f, ok := form.(*huh.Form); ok { - m.auth.loginForm = f + m.auth.LoginForm = f cmds = append(cmds, cmd) } - if m.auth.loginForm.State == huh.StateCompleted { - username := m.auth.loginForm.GetString("email") - password := m.auth.loginForm.GetString("password") + if m.auth.LoginForm.State == huh.StateCompleted { + username := m.auth.LoginForm.GetString("email") + password := m.auth.LoginForm.GetString("password") cmds = append(cmds, login(username, password)) } } @@ -229,21 +240,24 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Process the form // LOGIN - registerForm, cmd := m.auth.registerForm.Update(msg) + registerForm, cmd := m.auth.RegisterForm.Update(msg) if f, ok := registerForm.(*huh.Form); ok { - m.auth.registerForm = f + m.auth.RegisterForm = f cmds = append(cmds, cmd) } - if m.auth.registerForm.State == huh.StateCompleted { - username := m.auth.registerForm.GetString("username") - password := m.auth.registerForm.GetString("password") - email := m.auth.registerForm.GetString("email") + if m.auth.RegisterForm.State == huh.StateCompleted { + username := m.auth.RegisterForm.GetString("username") + password := m.auth.RegisterForm.GetString("password") + email := m.auth.RegisterForm.GetString("email") cmds = append(cmds, register(username, password, email)) } } + var cmd tea.Cmd + m.Preview.Viewport, cmd = m.Preview.Viewport.Update(msg) + cmds = append(cmds, cmd) m.list, cmd = m.list.Update(msg) cmds = append(cmds, cmd) m.textInput, cmd = m.textInput.Update(msg) @@ -261,7 +275,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func main() { tea.LogToFile("vex.log", "") m := New() - p := tea.NewProgram(m) + p := tea.NewProgram(m, + tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer" + tea.WithMouseCellMotion(), + ) if _, err := p.Run(); err != nil { os.Exit(1) } diff --git a/tui/pages.go b/tui/cmd/views.go similarity index 54% rename from tui/pages.go rename to tui/cmd/views.go index ea11a64..319ed8f 100644 --- a/tui/pages.go +++ b/tui/cmd/views.go @@ -4,30 +4,21 @@ import ( "fmt" "github.com/charmbracelet/lipgloss" + . "github.com/zoryia/vex/tui/pages" ) -const ( - LOGIN = "LOGIN" - REGISTER = "REGISTER" - ENTRIES = "ENTRIES" - FEEDS = "FEEDS" - TAGS = "TAGS" -) - -type VexPage string - -func (m Model) LoginView() string { +func (m Model) AuthView() string { return lipgloss.JoinHorizontal( lipgloss.Left, - m.auth.loginForm.View(), - m.auth.registerForm.View(), + m.auth.LoginForm.View(), + m.auth.RegisterForm.View(), ) } func (m Model) EntriesView() string { - return "" + return m.list.View() } func (m Model) FeedsView() string { - return fmt.Sprintf("%s ", *m.auth.jwt) + return fmt.Sprintf("%s ", *m.auth.Jwt) } func (m Model) TagsView() string { return "" @@ -35,15 +26,17 @@ func (m Model) TagsView() string { func (m Model) View() string { switch m.page { case LOGIN: - return m.LoginView() + return m.AuthView() case REGISTER: - return m.LoginView() + return m.AuthView() case ENTRIES: return m.EntriesView() case FEEDS: return m.FeedsView() case TAGS: return m.TagsView() + case PREVIEW: + return m.Preview.View() } - return m.textInput.View() + m.list.View() + return "Really unexpected state, get help" } diff --git a/tui/entry.go b/tui/entry.go deleted file mode 100644 index 6af4fca..0000000 --- a/tui/entry.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "time" -) - -type Feed struct { - Id string `json:"id"` - Name string `json:"name"` - Url string `json:"url"` - FaviconUrl string `json:"faviconUrl"` - Tags []string `json:"tags"` -} - -type Entry struct { - Id string `json:"id"` - ArticleTitle string `json:"title"` - Content string `json:"content"` - Link string `json:"link"` - Date time.Time `json:"time"` - - Author *string `json:"author"` // author not always specified - IsRead bool `json:"isRead"` - IsBookmarked bool `json:"IsBookmarked"` - IsIgnored bool `json:"isIgnored"` - IsReadLater bool `json:"isReadLater"` - Feed Feed `json:"feed"` -} diff --git a/tui/go.mod b/tui/go.mod index 04012b9..fe0f0a2 100644 --- a/tui/go.mod +++ b/tui/go.mod @@ -1,31 +1,48 @@ -module vex.tui +module github.com/zoryia/vex/tui go 1.22.2 require ( github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.26.1 + github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/huh v0.3.0 + github.com/charmbracelet/lipgloss v0.10.0 + github.com/go-playground/validator/v10 v10.20.0 ) require ( + github.com/alecthomas/chroma/v2 v2.8.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/badoux/checkmail v1.2.4 // indirect github.com/catppuccin/go v0.2.0 // indirect - github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.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.15 // indirect + github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect + github.com/yuin/goldmark v1.5.4 // indirect + github.com/yuin/goldmark-emoji v1.0.2 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.19.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/tui/go.sum b/tui/go.sum index 319a10c..6565963 100644 --- a/tui/go.sum +++ b/tui/go.sum @@ -1,30 +1,65 @@ +github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= +github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= +github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/badoux/checkmail v1.2.4 h1:4zMjdYDjE2Q7xF06VNfyN8P9JGU7epLjNb+Yu5OThVI= +github.com/badoux/checkmail v1.2.4/go.mod h1:XroCOBU5zzZJcLvgwU15I+2xXyCdTWXyR9MGfRhBYy0= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= github.com/charmbracelet/bubbletea v0.26.1 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0= github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo= +github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= +github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE= github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA= github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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.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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= +github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= 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= @@ -33,12 +68,27 @@ 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.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= +github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= +github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -47,5 +97,7 @@ golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/tui/models/entry.go b/tui/models/entry.go new file mode 100644 index 0000000..42cd316 --- /dev/null +++ b/tui/models/entry.go @@ -0,0 +1,83 @@ +package models + +import ( + "fmt" + "time" + + "github.com/charmbracelet/bubbles/key" +) + +type Feed struct { + Id string `json:"id"` + Name string `json:"name"` + Url string `json:"url"` + FaviconUrl string `json:"faviconUrl"` + Tags []string `json:"tags"` +} + +type Entry struct { + Id string `json:"id"` + ArticleTitle string `json:"title"` + Content string `json:"content"` + Link string `json:"link"` + Date time.Time `json:"time"` + + Author *string `json:"author"` // author not always specified + IsRead bool `json:"isRead"` + IsBookmarked bool `json:"IsBookmarked"` + IsIgnored bool `json:"isIgnored"` + IsReadLater bool `json:"isReadLater"` + Feed Feed `json:"feed"` +} + +func (e Entry) FilterValue() string { + return e.ArticleTitle +} + +func (e Entry) Title() string { + return e.ArticleTitle +} + +func (e Entry) Description() string { + return fmt.Sprintf("%s", "my desc") // TODO: real description (tags and author + date ?) +} + +type ListKeyMap struct { + Query key.Binding + BookmarkToggle key.Binding + ReadToggle key.Binding + ReadLaterToggle key.Binding + IgnoreToggle key.Binding + PreviewPost key.Binding +} + +func NewListKeyMap() *ListKeyMap { + return &ListKeyMap{ + PreviewPost: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "preview post"), + ), + Query: key.NewBinding( + key.WithKeys("/"), + key.WithHelp("/", "query posts"), + ), + BookmarkToggle: key.NewBinding( + key.WithKeys("b"), + key.WithHelp("b", "toggle bookmarked"), + ), + ReadToggle: key.NewBinding( + key.WithKeys("r"), + key.WithHelp("r", "toggle mark as read"), + ), + + IgnoreToggle: key.NewBinding( + key.WithKeys("x", "d"), + key.WithHelp("x", "ignore post"), + key.WithHelp("d", "ignore post"), + ), + ReadLaterToggle: key.NewBinding( + key.WithKeys("m"), + key.WithHelp("m", "add to read later"), + ), + } +} diff --git a/tui/auth.go b/tui/pages/auth/auth.go similarity index 53% rename from tui/auth.go rename to tui/pages/auth/auth.go index b649383..dadf8a9 100644 --- a/tui/auth.go +++ b/tui/pages/auth/auth.go @@ -1,13 +1,14 @@ -package main +package auth import ( + "github.com/badoux/checkmail" huh "github.com/charmbracelet/huh" ) -type Auth struct { - loginForm *huh.Form - registerForm *huh.Form - jwt *string +type Model struct { + LoginForm *huh.Form + RegisterForm *huh.Form + Jwt *string } func getLoginForm() *huh.Form { @@ -15,7 +16,14 @@ func getLoginForm() *huh.Form { huh.NewGroup( huh.NewInput(). Title("Email"). - Key("email"), + Key("email").Validate( + func(s string) error { + err := checkmail.ValidateFormat(s) + if err != nil { + return err + } + return nil + }), huh.NewInput(). Title("Password"). Key("password"). @@ -28,7 +36,14 @@ func getRegisterForm() *huh.Form { huh.NewGroup( huh.NewInput(). Title("Email"). - Key("email"), + Key("email").Validate( + func(s string) error { + err := checkmail.ValidateFormat(s) + if err != nil { + return err + } + return nil + }), huh.NewInput(). Title("Username"). Key("username"), @@ -42,3 +57,8 @@ func getRegisterForm() *huh.Form { Password(true), )).WithWidth(40) } + +func New() Model { + return Model{RegisterForm: getRegisterForm(), LoginForm: getLoginForm(), Jwt: new(string)} + +} diff --git a/tui/pages/pages.go b/tui/pages/pages.go new file mode 100644 index 0000000..2984f4a --- /dev/null +++ b/tui/pages/pages.go @@ -0,0 +1,13 @@ +package pages + +const ( + LOGIN = "LOGIN" + REGISTER = "REGISTER" + ENTRIES = "ENTRIES" + FEEDS = "FEEDS" + TAGS = "TAGS" + IGNORED = "IGNORED" + PREVIEW = "PREVIEW" +) + +type VexPage string diff --git a/tui/pages/preview/model.go b/tui/pages/preview/model.go new file mode 100644 index 0000000..860bac9 --- /dev/null +++ b/tui/pages/preview/model.go @@ -0,0 +1,88 @@ +package preview + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/lipgloss" + "github.com/zoryia/vex/tui/models" + + "errors" +) + +type Model struct { + Entry models.Entry + Viewport viewport.Model +} + +// RenderMarkdown renders the markdown content with glamour. +func RenderMarkdown(width int, content string) (string, error) { + background := "light" + + if lipgloss.HasDarkBackground() { + background = "dark" + } + + r, _ := glamour.NewTermRenderer( + glamour.WithWordWrap(width), + glamour.WithStandardStyle(background), + ) + + out, err := r.Render(content) + if err != nil { + return "", errors.Unwrap(err) + } + + return out, nil +} + +func renderMarkdownCmd(width int, entry models.Entry) tea.Cmd { + return func() tea.Msg { + _, err := RenderMarkdown(width, entry.Content) + if err != nil { + //return errorMsg(err) + } + return nil + // return renderMarkdownMsg(markdownContent) + } +} + +var ( + titleStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Right = "├" + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + infoStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Left = "┤" + return titleStyle.Copy().BorderStyle(b) + }() +) + +// TODO: render as markdown with glamour +func (m Model) View() string { + return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.Viewport.View(), m.footerView()) +} + +func (m Model) VerticalMarginHeight() int { + headerHeight := lipgloss.Height(m.headerView()) + footerHeight := lipgloss.Height(m.footerView()) + return headerHeight + footerHeight +} + +func (m Model) headerView() string { + title := titleStyle.Render(m.Entry.ArticleTitle) + line := strings.Repeat("─", max(0, m.Viewport.Width-lipgloss.Width(title))) + return lipgloss.JoinHorizontal(lipgloss.Center, title, line) +} + +func (m Model) footerView() string { + info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.Viewport.ScrollPercent()*100)) + line := strings.Repeat("─", max(0, m.Viewport.Width-lipgloss.Width(info))) + return lipgloss.JoinHorizontal(lipgloss.Center, line, info) +}