Merge pull request #11 from zoriya/tui

restructure and getting entries
This commit is contained in:
2024-05-05 20:09:16 +02:00
committed by GitHub
18 changed files with 929 additions and 333 deletions

1
tui/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
vex.log

View File

@@ -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
View 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
View 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
View 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
View 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...)
}

View File

@@ -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"
}

View File

@@ -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"`
}

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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
View 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
View 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)
}

View File

@@ -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)}
}

View 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
View 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
View 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

View 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)
}