mirror of
https://github.com/zoriya/vex.git
synced 2026-06-04 02:56:16 +00:00
269 lines
6.8 KiB
Go
269 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|