mirror of
https://github.com/zoriya/vex.git
synced 2025-12-06 07:06:09 +00:00
1
tui/.gitignore
vendored
Normal file
1
tui/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
vex.log
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/google/uuid"
|
||||
"github.com/zoryia/vex/tui/models"
|
||||
)
|
||||
|
||||
type statusMsg int
|
||||
@@ -133,12 +135,16 @@ 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 {
|
||||
url := fmt.Sprintf("%s/entries", serverUrl)
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return http.ErrMissingBoundary
|
||||
}
|
||||
if jwt == nil {
|
||||
return missingJwtMsg{}
|
||||
}
|
||||
@@ -146,18 +152,20 @@ func getEntries(jwt *string) tea.Cmd {
|
||||
req.Header.Add("Content-type", "application/json")
|
||||
data, err := getData(req)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return httpErrorMsg(err)
|
||||
}
|
||||
var entries []Entry
|
||||
var entries []models.Entry
|
||||
err = json.Unmarshal(data, &entries)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
return httpErrorMsg(err)
|
||||
}
|
||||
return getEntriesSuccessMsg(entries)
|
||||
}
|
||||
}
|
||||
|
||||
type getFeedsSuccessMsg []Feed
|
||||
type getFeedsSuccessMsg []models.Feed
|
||||
|
||||
func getFeeds(jwt *string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
@@ -171,7 +179,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)
|
||||
@@ -179,3 +187,82 @@ func getFeeds(jwt *string) tea.Cmd {
|
||||
return getFeedsSuccessMsg(feeds)
|
||||
}
|
||||
}
|
||||
|
||||
type addFeedSuccessMsg struct{}
|
||||
|
||||
func addFeed(jwt *string, link string, tags []string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
|
||||
url := fmt.Sprintf("%s/feeds", serverUrl)
|
||||
body := struct {
|
||||
Link string `json:"link"`
|
||||
Tags []string `json:"tags"`
|
||||
}{
|
||||
Link: link, Tags: tags,
|
||||
}
|
||||
out, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(out))
|
||||
req.Header.Add("Content-type", "application/json")
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *jwt))
|
||||
data, err := getData(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var addFeedResp AuthRes
|
||||
err = json.Unmarshal(data, &addFeedResp)
|
||||
if err != nil {
|
||||
return httpErrorMsg(err)
|
||||
}
|
||||
return addFeedSuccessMsg{}
|
||||
}
|
||||
}
|
||||
|
||||
type ignorePostSuccessMsg uuid.UUID
|
||||
|
||||
func ignorePost(jwt *string, e models.Entry) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
url := fmt.Sprintf("%s/ignore/%s", serverUrl, e.Id.String())
|
||||
req, _ := http.NewRequest(http.MethodPut, url, nil)
|
||||
if jwt == nil {
|
||||
return missingJwtMsg{}
|
||||
}
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", *jwt))
|
||||
data, err := getData(req)
|
||||
if err != nil {
|
||||
return httpErrorMsg(err)
|
||||
}
|
||||
var feeds []models.Feed
|
||||
err = json.Unmarshal(data, &feeds)
|
||||
if err != nil {
|
||||
return httpErrorMsg(err)
|
||||
}
|
||||
return getFeedsSuccessMsg(feeds)
|
||||
}
|
||||
}
|
||||
|
||||
type toggleReadSuccessMsg uuid.UUID
|
||||
|
||||
func toggleRead(jwt *string, e models.Entry) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type toggleReadLaterSuccessMsg uuid.UUID
|
||||
|
||||
func toggleReadLater(jwt *string, e models.Entry) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type toggleBookmarkSuccessMsg uuid.UUID
|
||||
|
||||
func toggleBookmark(jwt *string, e models.Entry) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
52
tui/cmd/keymaps.go
Normal file
52
tui/cmd/keymaps.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import "github.com/charmbracelet/bubbles/key"
|
||||
|
||||
type ListKeyMap struct {
|
||||
Query key.Binding
|
||||
BookmarkToggle key.Binding
|
||||
ReadToggle key.Binding
|
||||
ReadLaterToggle key.Binding
|
||||
IgnoreToggle key.Binding
|
||||
PreviewPost key.Binding
|
||||
GoToFeeds key.Binding
|
||||
GoToPosts key.Binding
|
||||
}
|
||||
|
||||
func NewListKeyMap() *ListKeyMap {
|
||||
return &ListKeyMap{
|
||||
GoToFeeds: key.NewBinding(
|
||||
key.WithKeys("f"),
|
||||
key.WithHelp("f", "go to feeds"),
|
||||
),
|
||||
GoToPosts: key.NewBinding(
|
||||
key.WithKeys("p"),
|
||||
key.WithHelp("p", "go to posts"),
|
||||
),
|
||||
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("d/x", "ignore post"),
|
||||
),
|
||||
ReadLaterToggle: key.NewBinding(
|
||||
key.WithKeys("m"),
|
||||
key.WithHelp("m", "add to read later"),
|
||||
),
|
||||
}
|
||||
}
|
||||
108
tui/cmd/main.go
Normal file
108
tui/cmd/main.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/google/uuid"
|
||||
. "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/feeds"
|
||||
"github.com/zoryia/vex/tui/pages/preview"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
// entries
|
||||
list list.Model
|
||||
queryInput textinput.Model
|
||||
|
||||
err error
|
||||
page VexPage
|
||||
tags []string
|
||||
keys *ListKeyMap
|
||||
|
||||
Preview preview.Model
|
||||
Feeds feeds.Model
|
||||
Auth auth.Model
|
||||
}
|
||||
|
||||
func queryInput() textinput.Model {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "Search Query"
|
||||
ti.CharLimit = 156
|
||||
ti.Width = 56
|
||||
return ti
|
||||
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
return &Model{
|
||||
queryInput: queryInput(),
|
||||
page: LOGIN,
|
||||
keys: NewListKeyMap(),
|
||||
|
||||
Preview: preview.Model{Viewport: viewport.New(0, 0)},
|
||||
Feeds: feeds.New(),
|
||||
Auth: auth.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) getEverything() tea.Cmd {
|
||||
return tea.Batch(getEntries(m.Auth.Jwt)) // getTags, getFeeds)
|
||||
}
|
||||
|
||||
func (m *Model) initList(width int, height int) {
|
||||
m.list = list.New([]list.Item{}, list.NewDefaultDelegate(), width, height)
|
||||
m.list.Title = "Posts"
|
||||
m.list.SetFilteringEnabled(false)
|
||||
m.list.DisableQuitKeybindings()
|
||||
m.list.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
m.keys.Query,
|
||||
m.keys.PreviewPost,
|
||||
}
|
||||
}
|
||||
m.list.AdditionalFullHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
m.keys.GoToFeeds,
|
||||
m.keys.ReadToggle,
|
||||
m.keys.ReadLaterToggle,
|
||||
m.keys.BookmarkToggle,
|
||||
m.keys.IgnoreToggle,
|
||||
m.keys.Query,
|
||||
m.keys.PreviewPost,
|
||||
}
|
||||
}
|
||||
var f = Feed{Id: uuid.UUID{}, Tags: []string{"Devops", "Kubernetes"}, Name: "zwindler", Url: "zwindler.blog", FaviconUrl: "zwindler.blog.favicon"}
|
||||
m.list.SetItems([]list.Item{
|
||||
Entry{Id: uuid.UUID{}, 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: uuid.UUID{}, ArticleTitle: "grrrrr", Content: "ouin ouin ouin", Link: "awd", Date: time.Now(), IsRead: false, IsIgnored: false, IsReadLater: false, IsBookmarked: false, Feed: f},
|
||||
Entry{Id: uuid.UUID{}, 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(
|
||||
m.Auth.LoginForm.Init(),
|
||||
m.Auth.RegisterForm.Init(),
|
||||
checkJwt(m.Auth.Jwt),
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
tea.LogToFile("vex.log", "")
|
||||
m := New()
|
||||
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)
|
||||
}
|
||||
}
|
||||
90
tui/cmd/search.go
Normal file
90
tui/cmd/search.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
huh "github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
func (m Model) handleSearchCompletion() (Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
queryWords := strings.Split(m.queryInput.Value(), " ")
|
||||
if len(queryWords) == 0 {
|
||||
return m, cmd
|
||||
}
|
||||
lastWord := queryWords[len(queryWords)-1]
|
||||
if lastWord == "tag:" {
|
||||
var feed string
|
||||
huh.NewSelect[string]().
|
||||
Title("Pick a feed.").
|
||||
Options(
|
||||
huh.NewOption("United States", "US"),
|
||||
huh.NewOption("Germany", "DE"),
|
||||
huh.NewOption("Brazil", "BR"),
|
||||
huh.NewOption("Canada", "CA"),
|
||||
).
|
||||
Value(&feed).Run()
|
||||
m.queryInput.SetValue(m.queryInput.Value() + feed)
|
||||
m.queryInput.CursorEnd()
|
||||
}
|
||||
|
||||
if lastWord == "feed:" {
|
||||
var feed string
|
||||
var feeds = []string{"Devops", "System", "Angular"}
|
||||
huh.NewSelect[string]().
|
||||
Title("Pick a feed.").
|
||||
Options(
|
||||
huh.NewOptions(feeds...)...,
|
||||
).
|
||||
Value(&feed).Run()
|
||||
m.queryInput.SetValue(m.queryInput.Value() + feed)
|
||||
m.queryInput.CursorEnd()
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) deleteWordBackward() {
|
||||
if m.queryInput.Position() == 0 || len(m.queryInput.Value()) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: wtf are other echo modes, dont care
|
||||
//if m.textInput.EchoMode != textinput.EchoNormal {
|
||||
// m.deleteBeforeCursor()
|
||||
// return
|
||||
//}
|
||||
|
||||
// Linter note: it's critical that we acquire the initial cursor position
|
||||
// here prior to altering it via SetCursor() below. As such, moving this
|
||||
// call into the corresponding if clause does not apply here.
|
||||
oldPos := m.queryInput.Position() //nolint:ifshort
|
||||
|
||||
m.queryInput.SetCursor(oldPos - 1)
|
||||
// ECHO character?
|
||||
for m.queryInput.Value()[m.queryInput.Position()] == ' ' {
|
||||
if m.queryInput.Position() <= 0 {
|
||||
break
|
||||
}
|
||||
// ignore series of whitespace before cursor
|
||||
m.queryInput.SetCursor(m.queryInput.Position() - 1)
|
||||
}
|
||||
|
||||
for m.queryInput.Position() > 0 {
|
||||
if m.queryInput.Value()[m.queryInput.Position()] != ' ' {
|
||||
m.queryInput.SetCursor(m.queryInput.Position() - 1)
|
||||
} else {
|
||||
if m.queryInput.Position() > 0 {
|
||||
// keep the previous space
|
||||
m.queryInput.SetCursor(m.queryInput.Position() + 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if oldPos > len(m.queryInput.Value()) {
|
||||
m.queryInput.SetValue(m.queryInput.Value()[:m.queryInput.Position()])
|
||||
} else {
|
||||
m.queryInput.SetValue(m.queryInput.Value()[:m.queryInput.Position()] + m.queryInput.Value()[oldPos:])
|
||||
}
|
||||
}
|
||||
247
tui/cmd/update.go
Normal file
247
tui/cmd/update.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
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/feeds"
|
||||
)
|
||||
|
||||
func (m Model) LoginUpdate(msg tea.Msg) (tea.Model, []tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
if m.page != LOGIN {
|
||||
return m, nil
|
||||
}
|
||||
return m, cmds
|
||||
}
|
||||
|
||||
func (m Model) GlobalUpdate(msg tea.Msg) (Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.initList(msg.Width, msg.Height-2)
|
||||
m.queryInput.Width = msg.Width - 5
|
||||
m.Preview.Viewport.Width = msg.Width
|
||||
m.Preview.Viewport.Height = msg.Height - m.Preview.VerticalMarginHeight()
|
||||
m.Feeds.List.SetWidth(msg.Width)
|
||||
m.Feeds.List.SetHeight(msg.Height)
|
||||
|
||||
case getEntriesSuccessMsg:
|
||||
var entries []list.Item
|
||||
for _, e := range msg {
|
||||
entries = append(entries, e)
|
||||
}
|
||||
m.list.SetItems(entries)
|
||||
|
||||
case invalidJwtMsg:
|
||||
m.Auth.Jwt = new(string)
|
||||
m.page = LOGIN
|
||||
return m, nil
|
||||
|
||||
case loginSuccessMsg:
|
||||
*m.Auth.Jwt = msg.string
|
||||
m.page = ENTRIES
|
||||
return m, m.getEverything()
|
||||
|
||||
case registerSuccessMsg:
|
||||
*m.Auth.Jwt = msg.string
|
||||
m.page = ENTRIES
|
||||
return m, m.getEverything()
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func loginUpdate(m Model, msg tea.Msg) (Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlT:
|
||||
m.page = REGISTER
|
||||
}
|
||||
}
|
||||
form, cmd := m.Auth.LoginForm.Update(msg)
|
||||
if f, ok := form.(*huh.Form); ok {
|
||||
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")
|
||||
cmds = append(cmds, login(username, password))
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
func registerUpdate(m Model, msg tea.Msg) (Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlT:
|
||||
m.page = LOGIN
|
||||
}
|
||||
}
|
||||
|
||||
// Process the form
|
||||
// LOGIN
|
||||
registerForm, cmd := m.Auth.RegisterForm.Update(msg)
|
||||
if f, ok := registerForm.(*huh.Form); ok {
|
||||
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")
|
||||
cmds = append(cmds, register(username, password, email))
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
func entriesUpdate(m Model, msg tea.Msg) (Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
var cmd tea.Cmd
|
||||
|
||||
var blurredNow = false
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyEnter:
|
||||
if m.queryInput.Focused() {
|
||||
// Get entries with query
|
||||
m.queryInput.Blur()
|
||||
blurredNow = true
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case key.Matches(msg, m.queryInput.KeyMap.DeleteCharacterBackward) && m.queryInput.Focused():
|
||||
words := strings.Split(m.queryInput.Value(), " ")
|
||||
if len(words) > 0 && (strings.HasPrefix(words[len(words)-1], "tag:") || strings.HasPrefix(words[len(words)-1], "feed:")) {
|
||||
m.deleteWordBackward()
|
||||
}
|
||||
}
|
||||
if m.queryInput.Focused() == false {
|
||||
switch {
|
||||
case key.Matches(msg, m.keys.GoToFeeds):
|
||||
m.page = FEEDS
|
||||
|
||||
case key.Matches(msg, m.keys.IgnoreToggle):
|
||||
var e = m.list.SelectedItem().(Entry)
|
||||
cmds = append(cmds, ignorePost(m.Auth.Jwt, e))
|
||||
|
||||
case key.Matches(msg, m.keys.ReadToggle):
|
||||
var e = m.list.SelectedItem().(Entry)
|
||||
cmds = append(cmds, toggleRead(m.Auth.Jwt, e))
|
||||
|
||||
case key.Matches(msg, m.keys.ReadLaterToggle):
|
||||
var e = m.list.SelectedItem().(Entry)
|
||||
cmds = append(cmds, toggleReadLater(m.Auth.Jwt, e))
|
||||
|
||||
case key.Matches(msg, m.keys.BookmarkToggle):
|
||||
var e = m.list.SelectedItem().(Entry)
|
||||
cmds = append(cmds, toggleBookmark(m.Auth.Jwt, e))
|
||||
|
||||
case key.Matches(msg, m.keys.Query):
|
||||
m.queryInput.Focus()
|
||||
m.queryInput.SetValue("")
|
||||
|
||||
case key.Matches(msg, m.keys.PreviewPost) && m.queryInput.Focused() == false && blurredNow == false:
|
||||
var e = m.list.SelectedItem().(Entry)
|
||||
m.Preview.Entry = e
|
||||
m.Preview.Viewport.SetContent(e.Content)
|
||||
m.page = PREVIEW
|
||||
|
||||
}
|
||||
}
|
||||
m, cmd = m.handleSearchCompletion()
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
m.queryInput, cmd = m.queryInput.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
func feedsUpdate(m Model, msg tea.Msg) (Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, feeds.FeedsKeyMaps().AddFeed):
|
||||
m.Feeds.AddFeed.Run()
|
||||
}
|
||||
}
|
||||
registerForm, cmd := m.Auth.RegisterForm.Update(msg)
|
||||
if f, ok := registerForm.(*huh.Form); ok {
|
||||
m.Auth.RegisterForm = f
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
if m.Auth.RegisterForm.State == huh.StateCompleted {
|
||||
url := m.Auth.RegisterForm.GetString("url")
|
||||
tags := m.Auth.RegisterForm.Get("tags").([]string)
|
||||
cmds = append(cmds, addFeed(m.Auth.Jwt, url, tags))
|
||||
}
|
||||
m.Feeds.List, cmd = m.Feeds.List.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
func tagsUpdate(m Model, msg tea.Msg) (Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
func previewUpdate(m Model, msg tea.Msg) (Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
var cmd tea.Cmd
|
||||
m.Preview.Viewport, cmd = m.Preview.Viewport.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
func ignoredUpdate(m Model, msg tea.Msg) (Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type updateFunc func(Model, tea.Msg) (Model, tea.Cmd)
|
||||
|
||||
func getUpdateMap() map[VexPage]updateFunc {
|
||||
|
||||
updateMap := make(map[VexPage]updateFunc)
|
||||
updateMap[LOGIN] = loginUpdate
|
||||
updateMap[REGISTER] = registerUpdate
|
||||
updateMap[ENTRIES] = entriesUpdate
|
||||
updateMap[FEEDS] = feedsUpdate
|
||||
updateMap[TAGS] = tagsUpdate
|
||||
updateMap[IGNORED] = ignoredUpdate
|
||||
updateMap[PREVIEW] = previewUpdate
|
||||
return updateMap
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
updateMap := getUpdateMap()
|
||||
var cmds []tea.Cmd
|
||||
var cmd tea.Cmd
|
||||
|
||||
m, cmd = m.GlobalUpdate(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
m, cmd = updateMap[m.page](m, msg)
|
||||
cmds = append(cmds, cmd)
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
@@ -1,33 +1,22 @@
|
||||
package main
|
||||
|
||||
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 lipgloss.JoinVertical(lipgloss.Left, m.queryInput.View(), m.list.View())
|
||||
}
|
||||
func (m Model) FeedsView() string {
|
||||
return fmt.Sprintf("%s ", *m.auth.jwt)
|
||||
return m.Feeds.View()
|
||||
}
|
||||
func (m Model) TagsView() string {
|
||||
return ""
|
||||
@@ -35,15 +24,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"
|
||||
}
|
||||
28
tui/entry.go
28
tui/entry.go
@@ -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"`
|
||||
}
|
||||
18
tui/go.mod
18
tui/go.mod
@@ -1,31 +1,43 @@
|
||||
module vex.tui
|
||||
module github.com/zoryia/vex/tui
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/badoux/checkmail v1.2.4
|
||||
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/google/uuid v1.6.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/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/gorilla/css v1.0.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/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
|
||||
)
|
||||
|
||||
36
tui/go.sum
36
tui/go.sum
@@ -1,19 +1,39 @@
|
||||
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/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/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/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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
@@ -22,9 +42,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
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 +56,21 @@ 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/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/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/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 +79,5 @@ 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=
|
||||
|
||||
268
tui/main.go
268
tui/main.go
@@ -1,268 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
huh "github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
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
|
||||
page VexPage
|
||||
query string
|
||||
feeds []Feed
|
||||
entries []Entry
|
||||
tags []string
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "Pikachu"
|
||||
ti.Focus()
|
||||
ti.CharLimit = 156
|
||||
ti.Width = 56
|
||||
return &Model{textInput: ti, auth: Auth{loginForm: getLoginForm(), registerForm: getRegisterForm(), jwt: new(string)}, page: LOGIN}
|
||||
}
|
||||
|
||||
func (m Model) getEverything() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return tea.Batch(getEntries(m.auth.jwt)) // getTags, getFeeds)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) initList(width int, height int) {
|
||||
m.list = list.New([]list.Item{}, list.NewDefaultDelegate(), width, height)
|
||||
m.list.Title = "Posts"
|
||||
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: "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))
|
||||
|
||||
}
|
||||
|
||||
func (m Model) handleSearchCompletion() (Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
queryWords := strings.Split(m.textInput.Value(), " ")
|
||||
if len(queryWords) == 0 {
|
||||
return m, cmd
|
||||
}
|
||||
lastWord := queryWords[len(queryWords)-1]
|
||||
if lastWord == "tag:" {
|
||||
var feed string
|
||||
huh.NewSelect[string]().
|
||||
Title("Pick a feed.").
|
||||
Options(
|
||||
huh.NewOption("United States", "US"),
|
||||
huh.NewOption("Germany", "DE"),
|
||||
huh.NewOption("Brazil", "BR"),
|
||||
huh.NewOption("Canada", "CA"),
|
||||
).
|
||||
Value(&feed).Run()
|
||||
m.textInput.SetValue(m.textInput.Value() + feed)
|
||||
m.textInput.CursorEnd()
|
||||
}
|
||||
|
||||
if lastWord == "feed:" {
|
||||
var feed string
|
||||
var feeds = []string{"Devops", "System", "Angular"}
|
||||
huh.NewSelect[string]().
|
||||
Title("Pick a feed.").
|
||||
Options(
|
||||
huh.NewOptions(feeds...)...,
|
||||
).
|
||||
Value(&feed).Run()
|
||||
m.textInput.SetValue(m.textInput.Value() + feed)
|
||||
m.textInput.CursorEnd()
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) deleteWordBackward() {
|
||||
if m.textInput.Position() == 0 || len(m.textInput.Value()) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: wtf are other echo modes, dont care
|
||||
//if m.textInput.EchoMode != textinput.EchoNormal {
|
||||
// m.deleteBeforeCursor()
|
||||
// return
|
||||
//}
|
||||
|
||||
// Linter note: it's critical that we acquire the initial cursor position
|
||||
// here prior to altering it via SetCursor() below. As such, moving this
|
||||
// call into the corresponding if clause does not apply here.
|
||||
oldPos := m.textInput.Position() //nolint:ifshort
|
||||
|
||||
m.textInput.SetCursor(oldPos - 1)
|
||||
// ECHO character?
|
||||
for m.textInput.Value()[m.textInput.Position()] == ' ' {
|
||||
if m.textInput.Position() <= 0 {
|
||||
break
|
||||
}
|
||||
// ignore series of whitespace before cursor
|
||||
m.textInput.SetCursor(m.textInput.Position() - 1)
|
||||
}
|
||||
|
||||
for m.textInput.Position() > 0 {
|
||||
if m.textInput.Value()[m.textInput.Position()] != ' ' {
|
||||
m.textInput.SetCursor(m.textInput.Position() - 1)
|
||||
} else {
|
||||
if m.textInput.Position() > 0 {
|
||||
// keep the previous space
|
||||
m.textInput.SetCursor(m.textInput.Position() + 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if oldPos > len(m.textInput.Value()) {
|
||||
m.textInput.SetValue(m.textInput.Value()[:m.textInput.Position()])
|
||||
} else {
|
||||
m.textInput.SetValue(m.textInput.Value()[:m.textInput.Position()] + m.textInput.Value()[oldPos:])
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.initList(msg.Width, msg.Height)
|
||||
|
||||
case invalidJwtMsg:
|
||||
m.auth.jwt = new(string)
|
||||
m.page = LOGIN
|
||||
return m, nil
|
||||
case loginSuccessMsg:
|
||||
*m.auth.jwt = msg.string
|
||||
m.page = FEEDS
|
||||
return m, m.getEverything()
|
||||
|
||||
case registerSuccessMsg:
|
||||
*m.auth.jwt = msg.string
|
||||
m.page = FEEDS
|
||||
return m, m.getEverything()
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyCtrlC, tea.KeyEsc:
|
||||
return m, tea.Quit
|
||||
case tea.KeyCtrlT:
|
||||
if m.page == LOGIN {
|
||||
m.page = REGISTER
|
||||
} else if m.page == REGISTER {
|
||||
m.page = LOGIN
|
||||
}
|
||||
|
||||
}
|
||||
switch {
|
||||
case key.Matches(msg, m.textInput.KeyMap.DeleteCharacterBackward): //TODO: add only when query
|
||||
words := strings.Split(m.textInput.Value(), " ")
|
||||
if len(words) > 0 && (strings.HasPrefix(words[len(words)-1], "tag:") || strings.HasPrefix(words[len(words)-1], "feed:")) {
|
||||
m.deleteWordBackward()
|
||||
}
|
||||
}
|
||||
}
|
||||
var cmds []tea.Cmd
|
||||
|
||||
// Process the form
|
||||
// LOGIN
|
||||
if m.page == LOGIN {
|
||||
form, cmd := m.auth.loginForm.Update(msg)
|
||||
if f, ok := form.(*huh.Form); ok {
|
||||
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")
|
||||
cmds = append(cmds, login(username, password))
|
||||
}
|
||||
}
|
||||
|
||||
if m.page == REGISTER {
|
||||
|
||||
// Process the form
|
||||
// LOGIN
|
||||
registerForm, cmd := m.auth.registerForm.Update(msg)
|
||||
if f, ok := registerForm.(*huh.Form); ok {
|
||||
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")
|
||||
cmds = append(cmds, register(username, password, email))
|
||||
}
|
||||
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
_ = msg
|
||||
m, cmd = m.handleSearchCompletion()
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func main() {
|
||||
tea.LogToFile("vex.log", "")
|
||||
m := New()
|
||||
p := tea.NewProgram(m)
|
||||
if _, err := p.Run(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
35
tui/models/entry.go
Normal file
35
tui/models/entry.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
Id uuid.UUID `json:"id"`
|
||||
ArticleTitle string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Link string `json:"link"`
|
||||
Date time.Time `json:"date"`
|
||||
|
||||
Authors []string `json:"authors"` // 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 ?)
|
||||
}
|
||||
30
tui/models/feed.go
Normal file
30
tui/models/feed.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Feed struct {
|
||||
Id uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Url string `json:"link"`
|
||||
FaviconUrl string `json:"faviconUrl"`
|
||||
Tags []string `json:"tags"`
|
||||
AddedDate time.Time `json:"addedDate"`
|
||||
SubmitterId uuid.UUID `json:"submitterId"`
|
||||
}
|
||||
|
||||
func (f Feed) FilterValue() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f Feed) Title() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f Feed) Description() string {
|
||||
return fmt.Sprintf("%s", "my desc") // TODO: real description (tags, submitter, error status, last sync)
|
||||
}
|
||||
@@ -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)}
|
||||
|
||||
}
|
||||
21
tui/pages/feeds/keymaps.go
Normal file
21
tui/pages/feeds/keymaps.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package feeds
|
||||
|
||||
import "github.com/charmbracelet/bubbles/key"
|
||||
|
||||
type ListKeyMap struct {
|
||||
GoToPosts key.Binding
|
||||
AddFeed key.Binding
|
||||
}
|
||||
|
||||
func FeedsKeyMaps() *ListKeyMap {
|
||||
return &ListKeyMap{
|
||||
GoToPosts: key.NewBinding(
|
||||
key.WithKeys("p"),
|
||||
key.WithHelp("p", "Go to posts"),
|
||||
),
|
||||
AddFeed: key.NewBinding(
|
||||
key.WithKeys("a"),
|
||||
key.WithHelp("a", "Add feed"),
|
||||
),
|
||||
}
|
||||
}
|
||||
65
tui/pages/feeds/model.go
Normal file
65
tui/pages/feeds/model.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package feeds
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/zoryia/vex/tui/models"
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
List list.Model
|
||||
AddFeed *huh.Form
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
return m.List.View()
|
||||
}
|
||||
|
||||
func initFeedsList() list.Model {
|
||||
feeds := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0)
|
||||
feeds.Title = "Feeds"
|
||||
feeds.SetFilteringEnabled(false)
|
||||
feeds.DisableQuitKeybindings()
|
||||
feeds.AdditionalShortHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
FeedsKeyMaps().AddFeed,
|
||||
FeedsKeyMaps().GoToPosts,
|
||||
}
|
||||
}
|
||||
feeds.AdditionalFullHelpKeys = func() []key.Binding {
|
||||
return []key.Binding{
|
||||
FeedsKeyMaps().AddFeed,
|
||||
FeedsKeyMaps().GoToPosts,
|
||||
}
|
||||
}
|
||||
feeds.SetItems([]list.Item{
|
||||
models.Feed{Id: uuid.UUID{}, Tags: []string{"Devops", "Kubernetes"}, Name: "zwindler", Url: "zwindler.blog", FaviconUrl: "zwindler.blog.favicon"},
|
||||
models.Feed{Id: uuid.UUID{}, Tags: []string{"Devops", "Kubernetes"}, Name: "zwindler", Url: "zwindler.blog", FaviconUrl: "zwindler.blog.favicon"},
|
||||
models.Feed{Id: uuid.UUID{}, Tags: []string{"Devops", "Kubernetes"}, Name: "zwindler", Url: "zwindler.blog", FaviconUrl: "zwindler.blog.favicon"},
|
||||
})
|
||||
return feeds
|
||||
}
|
||||
|
||||
func AddFeedForm(tags []string) *huh.Form {
|
||||
return huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Feed Url").
|
||||
Key("url"),
|
||||
huh.NewMultiSelect[string]().
|
||||
Title("Tags").
|
||||
Key("tags").
|
||||
Options(
|
||||
|
||||
huh.NewOptions(tags...)...,
|
||||
),
|
||||
)).WithWidth(40)
|
||||
}
|
||||
|
||||
func New() Model {
|
||||
return Model{List: initFeedsList(), AddFeed: AddFeedForm([]string{})}
|
||||
|
||||
}
|
||||
13
tui/pages/pages.go
Normal file
13
tui/pages/pages.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package pages
|
||||
|
||||
const (
|
||||
LOGIN = "LOGIN"
|
||||
REGISTER = "REGISTER"
|
||||
ENTRIES = "ENTRIES"
|
||||
FEEDS = "FEEDS"
|
||||
TAGS = "TAGS"
|
||||
IGNORED = "IGNORED"
|
||||
PREVIEW = "PREVIEW"
|
||||
)
|
||||
|
||||
type VexPage string
|
||||
88
tui/pages/preview/model.go
Normal file
88
tui/pages/preview/model.go
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user