From 99548e67b7c43beaa2c2617e09932620de48909d Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Thu, 2 Oct 2025 19:56:33 +0200 Subject: [PATCH 1/3] get select edit, read, and delete working --- README.md | 7 +++++++ actionsList.go | 17 +++++------------ entryList.go | 11 ++++++++++- gemlog/db.go | 28 ++++++++++++++++++++++++++++ gemlog/list.go | 12 ++++++++++++ 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 608525d..060133d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,13 @@ Quick tool to write gemtext posts for my gemlog. => gemini://travisshears.com/gemlog +## Dev + +To run command locally: + +```shell +$ go run . +``` ## Entites diff --git a/actionsList.go b/actionsList.go index 430021c..7134156 100644 --- a/actionsList.go +++ b/actionsList.go @@ -49,22 +49,15 @@ func (m ActionListPageModel) Update(msg tea.Msg, ctx *context) (ActionListPageMo switch action { case Write: return m, gemlog.WritePostCMD(ctx.config) - case Read: + case Read, Delete, Edit: switchPageCmd := func() tea.Msg { return SwitchPages{Page: EntryList} } + actionToTakeCmd := func() tea.Msg { + return SelectActionToTake{ActionToTake: action} + } loadGemLogsCmd := gemlog.LoadGemlogCMD(ctx.config) - return m, tea.Batch(switchPageCmd, loadGemLogsCmd) - // case Edit: - // m.ui.page = EntryList - // m.ui.entryListPage.cursor = 0 - // 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) + return m, tea.Batch(switchPageCmd, loadGemLogsCmd, actionToTakeCmd) } } } diff --git a/entryList.go b/entryList.go index af9e8c3..a579e65 100644 --- a/entryList.go +++ b/entryList.go @@ -13,6 +13,8 @@ type EntryListPageModel struct { cursor int } +type SelectActionToTake struct{ ActionToTake Action } + func InitialEntryListPageModel() EntryListPageModel { return EntryListPageModel{ cursor: 0, @@ -26,6 +28,8 @@ func (m EntryListPageModel) InitEntryListPage() tea.Cmd { func (m EntryListPageModel) Update(msg tea.Msg, ctx *context) (EntryListPageModel, tea.Cmd) { switch msg := msg.(type) { + case SelectActionToTake: + m.actionToTake = msg.ActionToTake case gemlog.GemLogsLoaded: m.entries = msg.Logs return m, nil @@ -39,7 +43,12 @@ func (m EntryListPageModel) Update(msg tea.Msg, ctx *context) (EntryListPageMode if m.cursor < len(actions)-1 { m.cursor++ } - // case "enter", " ": + case "enter", " ": + id := m.entries[m.cursor].ID + switch m.actionToTake { + case Delete: + return m, gemlog.DeleteGemlogCMD(ctx.config, id) + } // action := actions[m.cursor] // switch action { diff --git a/gemlog/db.go b/gemlog/db.go index e5f123f..2c9a18c 100644 --- a/gemlog/db.go +++ b/gemlog/db.go @@ -76,6 +76,34 @@ func listGemLogs(config *Config) ([]GemlogListEntry, error) { return entries, nil } +func DeleteGemlogEntry(config *Config, id string) error { + url := fmt.Sprintf("%s:%d/gemlog/%s", config.CouchDB.Host, config.CouchDB.Port, id) + 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 { url := fmt.Sprintf("%s:%d/gemlog/", config.CouchDB.Host, config.CouchDB.Port) diff --git a/gemlog/list.go b/gemlog/list.go index ce51dfc..e28d213 100644 --- a/gemlog/list.go +++ b/gemlog/list.go @@ -1,9 +1,21 @@ package gemlog import ( + "fmt" + tea "github.com/charmbracelet/bubbletea" ) +func DeleteGemlogCMD(config *Config, id string) tea.Cmd { + return func() tea.Msg { + err := DeleteGemlogEntry(config, id) + if err != nil { + return ErrorMsg{err} + } + return Notification(fmt.Sprintf("Gemlog with id: %s deleted", id)) + } +} + func LoadGemlogCMD(config *Config) tea.Cmd { return func() tea.Msg { logs, err := listGemLogs(config) From 8414414f98ee6c7069dadea67bbd47e98da05f9b Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Thu, 2 Oct 2025 20:09:57 +0200 Subject: [PATCH 2/3] refine navigation and loading enteries --- actionsList.go | 4 +-- bruno/Gemlog/Get Gemlog.bru | 20 +++++++++++++ bruno/Gemlog/Update Gemlog.bru | 33 ++++++++++++++++++++++ entry.go | 0 entryList.go | 51 +++++++++++++++------------------- gemlog/common.go | 1 + gemlog/core.go | 1 + gemlog/db.go | 50 ++++++++++++++++++++++++++++++++- gemlog/{list.go => read.go} | 17 ++++++++++-- main.go | 12 ++++---- 10 files changed, 148 insertions(+), 41 deletions(-) create mode 100644 bruno/Gemlog/Get Gemlog.bru create mode 100644 bruno/Gemlog/Update Gemlog.bru create mode 100644 entry.go rename gemlog/{list.go => read.go} (50%) diff --git a/actionsList.go b/actionsList.go index 7134156..674cc5f 100644 --- a/actionsList.go +++ b/actionsList.go @@ -44,7 +44,7 @@ func (m ActionListPageModel) Update(msg tea.Msg, ctx *context) (ActionListPageMo if m.cursor < len(actions)-1 { m.cursor++ } - case "enter", " ": + case "enter", " ", "l": action := actions[m.cursor] switch action { case Write: @@ -56,7 +56,7 @@ func (m ActionListPageModel) Update(msg tea.Msg, ctx *context) (ActionListPageMo actionToTakeCmd := func() tea.Msg { return SelectActionToTake{ActionToTake: action} } - loadGemLogsCmd := gemlog.LoadGemlogCMD(ctx.config) + loadGemLogsCmd := gemlog.LoadGemlogsCMD(ctx.config) return m, tea.Batch(switchPageCmd, loadGemLogsCmd, actionToTakeCmd) } } diff --git a/bruno/Gemlog/Get Gemlog.bru b/bruno/Gemlog/Get Gemlog.bru new file mode 100644 index 0000000..019a53b --- /dev/null +++ b/bruno/Gemlog/Get Gemlog.bru @@ -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 +} diff --git a/bruno/Gemlog/Update Gemlog.bru b/bruno/Gemlog/Update Gemlog.bru new file mode 100644 index 0000000..584bcbc --- /dev/null +++ b/bruno/Gemlog/Update Gemlog.bru @@ -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 +} diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..e69de29 diff --git a/entryList.go b/entryList.go index a579e65..ce76bc0 100644 --- a/entryList.go +++ b/entryList.go @@ -15,18 +15,14 @@ type EntryListPageModel struct { type SelectActionToTake struct{ ActionToTake Action } -func InitialEntryListPageModel() EntryListPageModel { +func initialEntryListPageModel() EntryListPageModel { return EntryListPageModel{ cursor: 0, actionToTake: Read, } } -func (m EntryListPageModel) InitEntryListPage() tea.Cmd { - return nil -} - -func (m EntryListPageModel) Update(msg tea.Msg, ctx *context) (EntryListPageModel, tea.Cmd) { +func (m EntryListPageModel) Update(msg tea.Msg, active bool, ctx *context) (EntryListPageModel, tea.Cmd) { switch msg := msg.(type) { case SelectActionToTake: m.actionToTake = msg.ActionToTake @@ -34,6 +30,9 @@ func (m EntryListPageModel) Update(msg tea.Msg, ctx *context) (EntryListPageMode m.entries = msg.Logs return m, nil case tea.KeyMsg: + if !active { + return m, nil + } switch msg.String() { case "up", "k": if m.cursor > 0 { @@ -43,33 +42,27 @@ func (m EntryListPageModel) Update(msg tea.Msg, ctx *context) (EntryListPageMode if m.cursor < len(actions)-1 { m.cursor++ } + case "left", "h": + cmd := func() tea.Msg { + return SwitchPages{Page: ActionList} + } + return m, cmd + case "enter", " ": id := m.entries[m.cursor].ID + rev := m.entries[m.cursor].Rev switch m.actionToTake { case Delete: - return m, gemlog.DeleteGemlogCMD(ctx.config, id) - } + delCmd := gemlog.DeleteGemlogCMD(ctx.config, id, rev) + loadCmd := gemlog.LoadGemlogsCMD(ctx.config) + navCmd := func() tea.Msg { + return SwitchPages{Page: ActionList} + } - // action := actions[m.cursor] - // switch action { - // case Write: - // return m, gemlog.WritePostCMD(ctx.config) - // case Read: - // m.ui.page = EntryList - // m.ui.entryListPage.cursor = 0 - // m.ui.entryListPage.actionToTake = Read - // return m, gemlog.LoadGemlogCMD(ctx.config) - // case Edit: - // m.ui.page = EntryList - // m.ui.entryListPage.cursor = 0 - // 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) - // } + return m, tea.Sequence(delCmd, loadCmd, navCmd) + case Read: + return m, gemlog.LoadGemlogCMD(ctx.config, id) + } } } @@ -85,5 +78,7 @@ func (m EntryListPageModel) View() string { } 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 } diff --git a/gemlog/common.go b/gemlog/common.go index e7d44f1..363215d 100644 --- a/gemlog/common.go +++ b/gemlog/common.go @@ -3,3 +3,4 @@ package gemlog type Notification string type ErrorMsg struct{ err error } type GemLogsLoaded struct{ Logs []GemlogListEntry } +type GemLogLoaded struct{ Log GemlogEntry } diff --git a/gemlog/core.go b/gemlog/core.go index 4501a97..4b369c6 100644 --- a/gemlog/core.go +++ b/gemlog/core.go @@ -21,6 +21,7 @@ type GemlogListEntry struct { Slug string `json:"slug"` Date time.Time `json:"date"` ID string `json:"id"` + Rev string `json:"rev"` } // NewUUID generates a new UUID v4 using crypto/rand diff --git a/gemlog/db.go b/gemlog/db.go index 2c9a18c..f8bcc8c 100644 --- a/gemlog/db.go +++ b/gemlog/db.go @@ -51,6 +51,7 @@ func listGemLogs(config *Config) ([]GemlogListEntry, error) { Title string `json:"title"` Slug string `json:"slug"` Date time.Time `json:"date"` + Rev string `json:"rev"` ID string `json:"id"` } `json:"value"` } `json:"rows"` @@ -68,6 +69,7 @@ func listGemLogs(config *Config) ([]GemlogListEntry, error) { Title: row.Value.Title, Slug: row.Value.Slug, Date: row.Value.Date, + Rev: row.Value.Rev, } entries = append(entries, entry) } @@ -76,8 +78,54 @@ func listGemLogs(config *Config) ([]GemlogListEntry, error) { return entries, nil } -func DeleteGemlogEntry(config *Config, id string) error { +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:"gemtext"` + 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, + 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) diff --git a/gemlog/list.go b/gemlog/read.go similarity index 50% rename from gemlog/list.go rename to gemlog/read.go index e28d213..173fc7b 100644 --- a/gemlog/list.go +++ b/gemlog/read.go @@ -6,17 +6,28 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -func DeleteGemlogCMD(config *Config, id string) tea.Cmd { +func DeleteGemlogCMD(config *Config, id string, rev string) tea.Cmd { return func() tea.Msg { - err := DeleteGemlogEntry(config, id) + 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) tea.Cmd { +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 { diff --git a/main.go b/main.go index 6b2d5e8..953e0c8 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,7 @@ type Page string const ( ActionList Page = "actionList" EntryList Page = "entryList" - // Entry Page = "entry" + Entry Page = "entry" ) type SwitchPages struct{ Page Page } @@ -40,12 +40,10 @@ type model struct { func initialModel(config *gemlog.Config) model { return model{ ui: uiState{ - page: ActionList, - // entryListPage: entryListPageModel{ - // cursor: 0, - // entries: []gemlog.GemlogListEntry{}, - // }, + page: ActionList, actionListPage: initialActionListPageModel(), + entryListPage: initialEntryListPageModel(), + // entryPage: initialEntryPageModel(), }, context: &context{ config: config, @@ -80,7 +78,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ui.actionListPage = actionListM 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 cmds = append(cmds, cmd) From 02ed95a6125b318d9802941016cee59481a13f96 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Fri, 3 Oct 2025 12:07:48 +0200 Subject: [PATCH 3/3] add page for reading a single gemlog entry --- actionsList.go | 5 ++++- entry.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ entryList.go | 12 ++++++++---- gemlog/db.go | 15 ++++++++------- main.go | 18 +++++++++++++----- 5 files changed, 78 insertions(+), 17 deletions(-) diff --git a/actionsList.go b/actionsList.go index 674cc5f..a6d068f 100644 --- a/actionsList.go +++ b/actionsList.go @@ -32,9 +32,12 @@ func (m ActionListPageModel) InitActionListPage() tea.Cmd { 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) { case tea.KeyMsg: + if !active { + return m, nil + } switch msg.String() { case "up", "k": if m.cursor > 0 { diff --git a/entry.go b/entry.go index e69de29..d9f25ad 100644 --- a/entry.go +++ b/entry.go @@ -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 +} diff --git a/entryList.go b/entryList.go index ce76bc0..b70bc5f 100644 --- a/entryList.go +++ b/entryList.go @@ -48,20 +48,24 @@ func (m EntryListPageModel) Update(msg tea.Msg, active bool, ctx *context) (Entr } return m, cmd - case "enter", " ": + case "enter", " ", "l": id := m.entries[m.cursor].ID rev := m.entries[m.cursor].Rev switch m.actionToTake { + // TODO: handle edit + case Read: + loadCmd := gemlog.LoadGemlogCMD(ctx.config, id) + navCmd := func() tea.Msg { + return SwitchPages{Page: Entry} + } + return m, tea.Sequence(loadCmd, navCmd) case Delete: delCmd := gemlog.DeleteGemlogCMD(ctx.config, id, rev) loadCmd := gemlog.LoadGemlogsCMD(ctx.config) navCmd := func() tea.Msg { return SwitchPages{Page: ActionList} } - return m, tea.Sequence(delCmd, loadCmd, navCmd) - case Read: - return m, gemlog.LoadGemlogCMD(ctx.config, id) } } } diff --git a/gemlog/db.go b/gemlog/db.go index f8bcc8c..356a466 100644 --- a/gemlog/db.go +++ b/gemlog/db.go @@ -104,10 +104,10 @@ func ReadGemlogEntry(config *Config, id string) (GemlogEntry, error) { } var rawData struct { - ID int `json:"_id"` - Rev int `json:"_rev"` + // ID int `json:"_id"` + // Rev int `json:"_rev"` Title string `json:"title"` - GemText string `json:"gemtext"` + GemText string `json:"gemtxt"` Slug string `json:"slug"` Date time.Time `json:"date"` } @@ -117,10 +117,11 @@ func ReadGemlogEntry(config *Config, id string) (GemlogEntry, error) { } return GemlogEntry{ - Title: rawData.Title, - Slug: rawData.Slug, - Date: rawData.Date, - Tags: make([]string, 0), + Title: rawData.Title, + Slug: rawData.Slug, + Date: rawData.Date, + Gemtxt: rawData.GemText, + Tags: make([]string, 0), }, nil } diff --git a/main.go b/main.go index 953e0c8..cc2d5b3 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ type uiState struct { page Page entryListPage EntryListPageModel + entryPage EntryPageModel actionListPage ActionListPageModel } @@ -43,7 +44,7 @@ func initialModel(config *gemlog.Config) model { page: ActionList, actionListPage: initialActionListPageModel(), entryListPage: initialEntryListPageModel(), - // entryPage: initialEntryPageModel(), + entryPage: initialEntryPageModel(), }, context: &context{ config: config, @@ -59,7 +60,7 @@ func (m model) Init() 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) { case SwitchPages: m.ui.page = msg.Page @@ -74,7 +75,7 @@ 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 cmds = append(cmds, cmd) @@ -82,6 +83,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.ui.entryListPage = entryListM 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...) } @@ -94,10 +99,13 @@ func (m model) View() string { s += m.ui.notification } - if m.ui.page == ActionList { + switch m.ui.page { + case ActionList: s += m.ui.actionListPage.View() - } else if m.ui.page == EntryList { + case EntryList: s += m.ui.entryListPage.View() + case Entry: + s += m.ui.entryPage.View() } s += "\nPress q to quit.\n"