Compare commits

..

3 commits

12 changed files with 285 additions and 68 deletions

View file

@ -4,6 +4,13 @@ Quick tool to write gemtext posts for my gemlog.
=> gemini://travisshears.com/gemlog => gemini://travisshears.com/gemlog
## Dev
To run command locally:
```shell
$ go run .
```
## Entites ## Entites

View file

@ -32,9 +32,12 @@ func (m ActionListPageModel) InitActionListPage() tea.Cmd {
return nil return nil
} }
func (m ActionListPageModel) Update(msg tea.Msg, ctx *context) (ActionListPageModel, tea.Cmd) { func (m ActionListPageModel) Update(msg tea.Msg, active bool, ctx *context) (ActionListPageModel, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
if !active {
return m, nil
}
switch msg.String() { switch msg.String() {
case "up", "k": case "up", "k":
if m.cursor > 0 { if m.cursor > 0 {
@ -44,27 +47,20 @@ func (m ActionListPageModel) Update(msg tea.Msg, ctx *context) (ActionListPageMo
if m.cursor < len(actions)-1 { if m.cursor < len(actions)-1 {
m.cursor++ m.cursor++
} }
case "enter", " ": case "enter", " ", "l":
action := actions[m.cursor] action := actions[m.cursor]
switch action { switch action {
case Write: case Write:
return m, gemlog.WritePostCMD(ctx.config) return m, gemlog.WritePostCMD(ctx.config)
case Read: case Read, Delete, Edit:
switchPageCmd := func() tea.Msg { switchPageCmd := func() tea.Msg {
return SwitchPages{Page: EntryList} return SwitchPages{Page: EntryList}
} }
loadGemLogsCmd := gemlog.LoadGemlogCMD(ctx.config) actionToTakeCmd := func() tea.Msg {
return m, tea.Batch(switchPageCmd, loadGemLogsCmd) return SelectActionToTake{ActionToTake: action}
// case Edit: }
// m.ui.page = EntryList loadGemLogsCmd := gemlog.LoadGemlogsCMD(ctx.config)
// m.ui.entryListPage.cursor = 0 return m, tea.Batch(switchPageCmd, loadGemLogsCmd, actionToTakeCmd)
// m.ui.entryListPage.actionToTake = Edit
// return m, gemlog.LoadGemlogCMD(ctx.config)
// case Delete:
// m.ui.page = EntryList
// m.ui.entryListPage.cursor = 0
// m.ui.entryListPage.actionToTake = Delete
// return m, gemlog.LoadGemlogCMD(m.context.config)
} }
} }
} }

View file

@ -0,0 +1,20 @@
meta {
name: Get Gemlog
type: http
seq: 5
}
get {
url: http://eisenhorn:5023/gemlog/d198245cbc0d67891f3d3d6dc301c242
body: json
auth: basic
}
auth:basic {
username: gemlog-cli
password: {{pw}}
}
settings {
encodeUrl: true
}

View file

@ -0,0 +1,33 @@
meta {
name: Update Gemlog
type: http
seq: 4
}
put {
url: http://eisenhorn:5023/gemlog/d198245cbc0d67891f3d3d6dc301c242?rev=2-e997af4bb2b1b18ce224ebcb46d145dc
body: json
auth: basic
}
params:query {
rev: 2-e997af4bb2b1b18ce224ebcb46d145dc
}
auth:basic {
username: gemlog-cli
password: {{pw}}
}
body:json {
{
"title": "Bruno Test Updated",
"slug": "bruno-test",
"date": "2025-09-30T21:04:45.092Z",
"gemtxt": "this is a test\n\nfrom bruno\nwith someupdated data"
}
}
settings {
encodeUrl: true
}

45
entry.go Normal file
View file

@ -0,0 +1,45 @@
package main
import (
"gemini_site/gemlog"
tea "github.com/charmbracelet/bubbletea"
)
type EntryPageModel struct {
entry gemlog.GemlogEntry
}
func initialEntryPageModel() EntryPageModel {
return EntryPageModel{}
}
func (m EntryPageModel) Update(msg tea.Msg, active bool, ctx *context) (EntryPageModel, tea.Cmd) {
switch msg := msg.(type) {
case gemlog.GemLogLoaded:
m.entry = msg.Log
case tea.KeyMsg:
if !active {
return m, nil
}
switch msg.String() {
case "left", "h":
cmd := func() tea.Msg {
return SwitchPages{Page: EntryList}
}
return m, cmd
}
}
return m, nil
}
func (m EntryPageModel) View() string {
s := m.entry.Slug
s += "\n\n"
s += m.entry.Gemtxt
s += "\n\n-------------------------\n"
s += "\n\nPress h or left arrow to go back"
return s
}

View file

@ -13,23 +13,26 @@ type EntryListPageModel struct {
cursor int cursor int
} }
func InitialEntryListPageModel() EntryListPageModel { type SelectActionToTake struct{ ActionToTake Action }
func initialEntryListPageModel() EntryListPageModel {
return EntryListPageModel{ return EntryListPageModel{
cursor: 0, cursor: 0,
actionToTake: Read, actionToTake: Read,
} }
} }
func (m EntryListPageModel) InitEntryListPage() tea.Cmd { func (m EntryListPageModel) Update(msg tea.Msg, active bool, ctx *context) (EntryListPageModel, tea.Cmd) {
return nil
}
func (m EntryListPageModel) Update(msg tea.Msg, ctx *context) (EntryListPageModel, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case SelectActionToTake:
m.actionToTake = msg.ActionToTake
case gemlog.GemLogsLoaded: case gemlog.GemLogsLoaded:
m.entries = msg.Logs m.entries = msg.Logs
return m, nil return m, nil
case tea.KeyMsg: case tea.KeyMsg:
if !active {
return m, nil
}
switch msg.String() { switch msg.String() {
case "up", "k": case "up", "k":
if m.cursor > 0 { if m.cursor > 0 {
@ -39,28 +42,31 @@ func (m EntryListPageModel) Update(msg tea.Msg, ctx *context) (EntryListPageMode
if m.cursor < len(actions)-1 { if m.cursor < len(actions)-1 {
m.cursor++ m.cursor++
} }
// case "enter", " ": case "left", "h":
cmd := func() tea.Msg {
return SwitchPages{Page: ActionList}
}
return m, cmd
// action := actions[m.cursor] case "enter", " ", "l":
// switch action { id := m.entries[m.cursor].ID
// case Write: rev := m.entries[m.cursor].Rev
// return m, gemlog.WritePostCMD(ctx.config) switch m.actionToTake {
// case Read: // TODO: handle edit
// m.ui.page = EntryList case Read:
// m.ui.entryListPage.cursor = 0 loadCmd := gemlog.LoadGemlogCMD(ctx.config, id)
// m.ui.entryListPage.actionToTake = Read navCmd := func() tea.Msg {
// return m, gemlog.LoadGemlogCMD(ctx.config) return SwitchPages{Page: Entry}
// case Edit: }
// m.ui.page = EntryList return m, tea.Sequence(loadCmd, navCmd)
// m.ui.entryListPage.cursor = 0 case Delete:
// m.ui.entryListPage.actionToTake = Edit delCmd := gemlog.DeleteGemlogCMD(ctx.config, id, rev)
// return m, gemlog.LoadGemlogCMD(ctx.config) loadCmd := gemlog.LoadGemlogsCMD(ctx.config)
// case Delete: navCmd := func() tea.Msg {
// m.ui.page = EntryList return SwitchPages{Page: ActionList}
// m.ui.entryListPage.cursor = 0 }
// m.ui.entryListPage.actionToTake = Delete return m, tea.Sequence(delCmd, loadCmd, navCmd)
// return m, gemlog.LoadGemlogCMD(m.context.config) }
// }
} }
} }
@ -76,5 +82,7 @@ func (m EntryListPageModel) View() string {
} }
s += fmt.Sprintf("%s %s : %s\n", cursor, entry.Date, entry.Slug) s += fmt.Sprintf("%s %s : %s\n", cursor, entry.Date, entry.Slug)
} }
s += "\n\n Press h or left arrow to go back"
return s return s
} }

View file

@ -3,3 +3,4 @@ package gemlog
type Notification string type Notification string
type ErrorMsg struct{ err error } type ErrorMsg struct{ err error }
type GemLogsLoaded struct{ Logs []GemlogListEntry } type GemLogsLoaded struct{ Logs []GemlogListEntry }
type GemLogLoaded struct{ Log GemlogEntry }

View file

@ -21,6 +21,7 @@ type GemlogListEntry struct {
Slug string `json:"slug"` Slug string `json:"slug"`
Date time.Time `json:"date"` Date time.Time `json:"date"`
ID string `json:"id"` ID string `json:"id"`
Rev string `json:"rev"`
} }
// NewUUID generates a new UUID v4 using crypto/rand // NewUUID generates a new UUID v4 using crypto/rand

View file

@ -51,6 +51,7 @@ func listGemLogs(config *Config) ([]GemlogListEntry, error) {
Title string `json:"title"` Title string `json:"title"`
Slug string `json:"slug"` Slug string `json:"slug"`
Date time.Time `json:"date"` Date time.Time `json:"date"`
Rev string `json:"rev"`
ID string `json:"id"` ID string `json:"id"`
} `json:"value"` } `json:"value"`
} `json:"rows"` } `json:"rows"`
@ -68,6 +69,7 @@ func listGemLogs(config *Config) ([]GemlogListEntry, error) {
Title: row.Value.Title, Title: row.Value.Title,
Slug: row.Value.Slug, Slug: row.Value.Slug,
Date: row.Value.Date, Date: row.Value.Date,
Rev: row.Value.Rev,
} }
entries = append(entries, entry) entries = append(entries, entry)
} }
@ -76,6 +78,81 @@ func listGemLogs(config *Config) ([]GemlogListEntry, error) {
return entries, nil return entries, nil
} }
func ReadGemlogEntry(config *Config, id string) (GemlogEntry, error) {
url := fmt.Sprintf("%s:%d/gemlog/%s", config.CouchDB.Host, config.CouchDB.Port, id)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return GemlogEntry{}, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Add("authorization", genBasicAuthHeader(config.CouchDB.User, config.CouchDB.Password))
req.Header.Add("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return GemlogEntry{}, fmt.Errorf("failed to send request: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return GemlogEntry{}, fmt.Errorf("failed to read response body: %w", err)
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return GemlogEntry{}, fmt.Errorf("unexpected status code %d: %s", res.StatusCode, string(body))
}
var rawData struct {
// ID int `json:"_id"`
// Rev int `json:"_rev"`
Title string `json:"title"`
GemText string `json:"gemtxt"`
Slug string `json:"slug"`
Date time.Time `json:"date"`
}
if err := json.Unmarshal(body, &rawData); err != nil {
return GemlogEntry{}, fmt.Errorf("failed to parse response: %w", err)
}
return GemlogEntry{
Title: rawData.Title,
Slug: rawData.Slug,
Date: rawData.Date,
Gemtxt: rawData.GemText,
Tags: make([]string, 0),
}, nil
}
func DeleteGemlogEntry(config *Config, id string, rev string) error {
url := fmt.Sprintf("%s:%d/gemlog/%s?rev=%s", config.CouchDB.Host, config.CouchDB.Port, id, rev)
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Add("authorization", genBasicAuthHeader(config.CouchDB.User, config.CouchDB.Password))
req.Header.Add("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return fmt.Errorf("unexpected status code %d: %s", res.StatusCode, string(body))
}
return nil
}
func SaveGemlogEntry(config *Config, entry *GemlogEntry) error { func SaveGemlogEntry(config *Config, entry *GemlogEntry) error {
url := fmt.Sprintf("%s:%d/gemlog/", config.CouchDB.Host, config.CouchDB.Port) url := fmt.Sprintf("%s:%d/gemlog/", config.CouchDB.Host, config.CouchDB.Port)

View file

@ -1,15 +0,0 @@
package gemlog
import (
tea "github.com/charmbracelet/bubbletea"
)
func LoadGemlogCMD(config *Config) tea.Cmd {
return func() tea.Msg {
logs, err := listGemLogs(config)
if err != nil {
return ErrorMsg{err}
}
return GemLogsLoaded{Logs: logs}
}
}

38
gemlog/read.go Normal file
View file

@ -0,0 +1,38 @@
package gemlog
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
)
func DeleteGemlogCMD(config *Config, id string, rev string) tea.Cmd {
return func() tea.Msg {
err := DeleteGemlogEntry(config, id, rev)
if err != nil {
return ErrorMsg{err}
}
return Notification(fmt.Sprintf("Gemlog with id: %s deleted", id))
}
}
func LoadGemlogCMD(config *Config, id string) tea.Cmd {
return func() tea.Msg {
log, err := ReadGemlogEntry(config, id)
if err != nil {
return ErrorMsg{err}
}
return GemLogLoaded{Log: log}
}
}
func LoadGemlogsCMD(config *Config) tea.Cmd {
return func() tea.Msg {
logs, err := listGemLogs(config)
if err != nil {
return ErrorMsg{err}
}
return GemLogsLoaded{Logs: logs}
}
}

26
main.go
View file

@ -14,7 +14,7 @@ type Page string
const ( const (
ActionList Page = "actionList" ActionList Page = "actionList"
EntryList Page = "entryList" EntryList Page = "entryList"
// Entry Page = "entry" Entry Page = "entry"
) )
type SwitchPages struct{ Page Page } type SwitchPages struct{ Page Page }
@ -25,6 +25,7 @@ type uiState struct {
page Page page Page
entryListPage EntryListPageModel entryListPage EntryListPageModel
entryPage EntryPageModel
actionListPage ActionListPageModel actionListPage ActionListPageModel
} }
@ -41,11 +42,9 @@ func initialModel(config *gemlog.Config) model {
return model{ return model{
ui: uiState{ ui: uiState{
page: ActionList, page: ActionList,
// entryListPage: entryListPageModel{
// cursor: 0,
// entries: []gemlog.GemlogListEntry{},
// },
actionListPage: initialActionListPageModel(), actionListPage: initialActionListPageModel(),
entryListPage: initialEntryListPageModel(),
entryPage: initialEntryPageModel(),
}, },
context: &context{ context: &context{
config: config, config: config,
@ -61,7 +60,7 @@ func (m model) Init() tea.Cmd {
} }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0) cmds := make([]tea.Cmd, 3)
switch msg := msg.(type) { switch msg := msg.(type) {
case SwitchPages: case SwitchPages:
m.ui.page = msg.Page m.ui.page = msg.Page
@ -76,14 +75,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
} }
actionListM, cmd := m.ui.actionListPage.Update(msg, m.context) actionListM, cmd := m.ui.actionListPage.Update(msg, m.ui.page == ActionList, m.context)
m.ui.actionListPage = actionListM m.ui.actionListPage = actionListM
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
entryListM, cmd := m.ui.entryListPage.Update(msg, m.context) entryListM, cmd := m.ui.entryListPage.Update(msg, m.ui.page == EntryList, m.context)
m.ui.entryListPage = entryListM m.ui.entryListPage = entryListM
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
entrytM, cmd := m.ui.entryPage.Update(msg, m.ui.page == Entry, m.context)
m.ui.entryPage = entrytM
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
@ -96,10 +99,13 @@ func (m model) View() string {
s += m.ui.notification s += m.ui.notification
} }
if m.ui.page == ActionList { switch m.ui.page {
case ActionList:
s += m.ui.actionListPage.View() s += m.ui.actionListPage.View()
} else if m.ui.page == EntryList { case EntryList:
s += m.ui.entryListPage.View() s += m.ui.entryListPage.View()
case Entry:
s += m.ui.entryPage.View()
} }
s += "\nPress q to quit.\n" s += "\nPress q to quit.\n"