From 360fedbebed3c2897c05813175f1702f8a9a6f3d Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Sat, 4 Oct 2025 19:40:27 +0200 Subject: [PATCH] restructure project following best practices https://go.dev/doc/modules/layout This also enables us to later import just the db interaction part say to the gemini capsule backend go project. --- gemlog/common.go | 6 - gemlog/db.go | 2 +- go.mod | 2 +- actionsList.go => internal/ui/actionsList.go | 7 +- internal/ui/app.go | 151 +++++++++++++++++++ gemlog/read.go => internal/ui/commands.go | 15 +- entry.go => internal/ui/entry.go | 7 +- entryList.go => internal/ui/entryList.go | 12 +- internal/ui/templates.go | 15 ++ {gemlog => internal/ui}/write.go | 20 ++- main.go | 134 +--------------- 11 files changed, 202 insertions(+), 169 deletions(-) delete mode 100644 gemlog/common.go rename actionsList.go => internal/ui/actionsList.go (91%) create mode 100644 internal/ui/app.go rename gemlog/read.go => internal/ui/commands.go (51%) rename entry.go => internal/ui/entry.go (88%) rename entryList.go => internal/ui/entryList.go (88%) create mode 100644 internal/ui/templates.go rename {gemlog => internal/ui}/write.go (84%) diff --git a/gemlog/common.go b/gemlog/common.go deleted file mode 100644 index 363215d..0000000 --- a/gemlog/common.go +++ /dev/null @@ -1,6 +0,0 @@ -package gemlog - -type Notification string -type ErrorMsg struct{ err error } -type GemLogsLoaded struct{ Logs []GemlogListEntry } -type GemLogLoaded struct{ Log GemlogEntry } diff --git a/gemlog/db.go b/gemlog/db.go index 8d1999f..3d21ce4 100644 --- a/gemlog/db.go +++ b/gemlog/db.go @@ -16,7 +16,7 @@ func genBasicAuthHeader(user, password string) string { return fmt.Sprintf("Basic %s", auth) } -func listGemLogs(config *Config) ([]GemlogListEntry, error) { +func ListGemLogs(config *Config) ([]GemlogListEntry, error) { url := fmt.Sprintf("%s:%d/gemlog/_design/gemlog-cli/_view/list", config.CouchDB.Host, config.CouchDB.Port) req, err := http.NewRequest("GET", url, nil) if err != nil { diff --git a/go.mod b/go.mod index 6fdacbb..b9b315d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module gemlog-cli +module git.travisshears.com/travisshears/gemlog-cli go 1.25.0 diff --git a/actionsList.go b/internal/ui/actionsList.go similarity index 91% rename from actionsList.go rename to internal/ui/actionsList.go index a6d068f..98666c2 100644 --- a/actionsList.go +++ b/internal/ui/actionsList.go @@ -1,8 +1,7 @@ -package main +package ui import ( "fmt" - "gemini_site/gemlog" tea "github.com/charmbracelet/bubbletea" ) @@ -51,7 +50,7 @@ func (m ActionListPageModel) Update(msg tea.Msg, active bool, ctx *context) (Act action := actions[m.cursor] switch action { case Write: - return m, gemlog.WritePostCMD(ctx.config) + return m, WritePostCMD(ctx.config) case Read, Delete, Edit: switchPageCmd := func() tea.Msg { return SwitchPages{Page: EntryList} @@ -59,7 +58,7 @@ func (m ActionListPageModel) Update(msg tea.Msg, active bool, ctx *context) (Act actionToTakeCmd := func() tea.Msg { return SelectActionToTake{ActionToTake: action} } - loadGemLogsCmd := gemlog.LoadGemlogsCMD(ctx.config) + loadGemLogsCmd := LoadGemlogsCMD(ctx.config) return m, tea.Batch(switchPageCmd, loadGemLogsCmd, actionToTakeCmd) } } diff --git a/internal/ui/app.go b/internal/ui/app.go new file mode 100644 index 0000000..e775d72 --- /dev/null +++ b/internal/ui/app.go @@ -0,0 +1,151 @@ +package ui + +import ( + "fmt" + "io" + "log/slog" + "os" + + gemlog "git.travisshears.com/travisshears/gemlog-cli/gemlog" + tea "github.com/charmbracelet/bubbletea" +) + +type Page string + +const ( + ActionList Page = "actionList" + EntryList Page = "entryList" + Entry Page = "entry" +) + +type SwitchPages struct{ Page Page } +type Notification string +type ErrorMsg struct{ err error } +type GemLogsLoaded struct{ Logs []gemlog.GemlogListEntry } +type GemLogLoaded struct{ Log gemlog.GemlogEntry } + +type uiState struct { + notification string + errorTxt string + + page Page + entryListPage EntryListPageModel + entryPage EntryPageModel + actionListPage ActionListPageModel +} + +type context struct { + config *gemlog.Config +} + +type model struct { + ui uiState + context *context +} + +func initialModel(config *gemlog.Config) model { + return model{ + ui: uiState{ + page: ActionList, + actionListPage: initialActionListPageModel(), + entryListPage: initialEntryListPageModel(), + entryPage: initialEntryPageModel(), + }, + context: &context{ + config: config, + }, + } +} + +func (m model) Init() tea.Cmd { + cmds := make([]tea.Cmd, 0) + cmds = append(cmds, tea.SetWindowTitle("Gemlog CLI")) + // TODO: add init commands from other pages + return tea.Batch(cmds...) +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + cmds := make([]tea.Cmd, 3) + switch msg := msg.(type) { + case SwitchPages: + m.ui.page = msg.Page + case ErrorMsg: + m.ui.errorTxt = fmt.Sprintf("%s\n\n", fmt.Errorf("%s", msg)) + case Notification: + m.ui.notification = fmt.Sprintf("%s\n\n", string(msg)) + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + } + } + + actionListM, cmd := m.ui.actionListPage.Update(msg, m.ui.page == ActionList, m.context) + m.ui.actionListPage = actionListM + cmds = append(cmds, cmd) + + entryListM, cmd := m.ui.entryListPage.Update(msg, m.ui.page == EntryList, m.context) + 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...) +} + +func (m model) View() string { + if len(m.ui.errorTxt) > 0 { + return m.ui.errorTxt + } + s := "" + if m.ui.notification != "" { + s += m.ui.notification + } + + switch m.ui.page { + case ActionList: + s += m.ui.actionListPage.View() + case EntryList: + s += m.ui.entryListPage.View() + case Entry: + s += m.ui.entryPage.View() + } + + s += "\nPress q to quit.\n" + + return s +} + +var enableLogs bool = false + +func Run() { + if enableLogs { + f, err := tea.LogToFile("debug.log", "debug") + if err != nil { + fmt.Println("fatal:", err) + os.Exit(1) + } + defer f.Close() + } else { + logger := slog.New(slog.NewJSONHandler(io.Discard, nil)) + slog.SetDefault(logger) + } + slog.Info("Starting gemlog cli") + config, err := gemlog.LoadConfig() + if err != nil { + fmt.Printf("Error loading config: %v", err) + os.Exit(1) + } + err = gemlog.CheckDBConnection(config) + if err != nil { + fmt.Printf("Error checking db connection: %v", err) + os.Exit(1) + } + p := tea.NewProgram(initialModel(config)) + if _, err := p.Run(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } +} diff --git a/gemlog/read.go b/internal/ui/commands.go similarity index 51% rename from gemlog/read.go rename to internal/ui/commands.go index 173fc7b..b92c91c 100644 --- a/gemlog/read.go +++ b/internal/ui/commands.go @@ -1,14 +1,15 @@ -package gemlog +package ui import ( "fmt" + gemlog "git.travisshears.com/travisshears/gemlog-cli/gemlog" tea "github.com/charmbracelet/bubbletea" ) -func DeleteGemlogCMD(config *Config, id string, rev string) tea.Cmd { +func DeleteGemlogCMD(config *gemlog.Config, id string, rev string) tea.Cmd { return func() tea.Msg { - err := DeleteGemlogEntry(config, id, rev) + err := gemlog.DeleteGemlogEntry(config, id, rev) if err != nil { return ErrorMsg{err} } @@ -17,9 +18,9 @@ func DeleteGemlogCMD(config *Config, id string, rev string) tea.Cmd { } } -func LoadGemlogCMD(config *Config, id string) tea.Cmd { +func LoadGemlogCMD(config *gemlog.Config, id string) tea.Cmd { return func() tea.Msg { - log, err := ReadGemlogEntry(config, id) + log, err := gemlog.ReadGemlogEntry(config, id) if err != nil { return ErrorMsg{err} } @@ -27,9 +28,9 @@ func LoadGemlogCMD(config *Config, id string) tea.Cmd { } } -func LoadGemlogsCMD(config *Config) tea.Cmd { +func LoadGemlogsCMD(config *gemlog.Config) tea.Cmd { return func() tea.Msg { - logs, err := listGemLogs(config) + logs, err := gemlog.ListGemLogs(config) if err != nil { return ErrorMsg{err} } diff --git a/entry.go b/internal/ui/entry.go similarity index 88% rename from entry.go rename to internal/ui/entry.go index d9f25ad..6ef36b1 100644 --- a/entry.go +++ b/internal/ui/entry.go @@ -1,8 +1,7 @@ -package main +package ui import ( - "gemini_site/gemlog" - + gemlog "git.travisshears.com/travisshears/gemlog-cli/gemlog" tea "github.com/charmbracelet/bubbletea" ) @@ -16,7 +15,7 @@ func initialEntryPageModel() EntryPageModel { func (m EntryPageModel) Update(msg tea.Msg, active bool, ctx *context) (EntryPageModel, tea.Cmd) { switch msg := msg.(type) { - case gemlog.GemLogLoaded: + case GemLogLoaded: m.entry = msg.Log case tea.KeyMsg: if !active { diff --git a/entryList.go b/internal/ui/entryList.go similarity index 88% rename from entryList.go rename to internal/ui/entryList.go index b70bc5f..ce6ba33 100644 --- a/entryList.go +++ b/internal/ui/entryList.go @@ -1,9 +1,9 @@ -package main +package ui import ( "fmt" - "gemini_site/gemlog" + gemlog "git.travisshears.com/travisshears/gemlog-cli/gemlog" tea "github.com/charmbracelet/bubbletea" ) @@ -26,7 +26,7 @@ func (m EntryListPageModel) Update(msg tea.Msg, active bool, ctx *context) (Entr switch msg := msg.(type) { case SelectActionToTake: m.actionToTake = msg.ActionToTake - case gemlog.GemLogsLoaded: + case GemLogsLoaded: m.entries = msg.Logs return m, nil case tea.KeyMsg: @@ -54,14 +54,14 @@ func (m EntryListPageModel) Update(msg tea.Msg, active bool, ctx *context) (Entr switch m.actionToTake { // TODO: handle edit case Read: - loadCmd := gemlog.LoadGemlogCMD(ctx.config, id) + loadCmd := 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) + delCmd := DeleteGemlogCMD(ctx.config, id, rev) + loadCmd := LoadGemlogsCMD(ctx.config) navCmd := func() tea.Msg { return SwitchPages{Page: ActionList} } diff --git a/internal/ui/templates.go b/internal/ui/templates.go new file mode 100644 index 0000000..80039da --- /dev/null +++ b/internal/ui/templates.go @@ -0,0 +1,15 @@ +package ui + +const defaultTemplate = `title: todo +date: 2025-12-31 +slug: todo +tags: cat, dog +--- + +Example text + +=> https://travisshears.com example link + +* Example list item 1 +* Example list item 2 +` diff --git a/gemlog/write.go b/internal/ui/write.go similarity index 84% rename from gemlog/write.go rename to internal/ui/write.go index 3c534f8..1f31516 100644 --- a/gemlog/write.go +++ b/internal/ui/write.go @@ -1,4 +1,4 @@ -package gemlog +package ui import ( "fmt" @@ -7,11 +7,15 @@ import ( "strings" "time" + gemlog "git.travisshears.com/travisshears/gemlog-cli/gemlog" tea "github.com/charmbracelet/bubbletea" "gopkg.in/yaml.v3" ) -func WritePostCMD(config *Config) tea.Cmd { +// TODO: add edit command +// func EditPostCMD(config *gemlog.Config) tea.Cmd {} + +func WritePostCMD(config *gemlog.Config) tea.Cmd { return func() tea.Msg { // Create a temporary file tmpFile, err := os.CreateTemp("/tmp", "gemlog-*.md") @@ -61,7 +65,7 @@ func WritePostCMD(config *Config) tea.Cmd { if err != nil { return ErrorMsg{fmt.Errorf("failed to parse post: %w", err)} } - if err := SaveGemlogEntry(config, &gemlogEntry); err != nil { + if err := gemlog.SaveGemlogEntry(config, &gemlogEntry); err != nil { return ErrorMsg{fmt.Errorf("failed to save gemlog entry: %w", err)} } @@ -71,7 +75,7 @@ func WritePostCMD(config *Config) tea.Cmd { } } -func parsePost(post string) (GemlogEntry, error) { +func parsePost(post string) (gemlog.GemlogEntry, error) { // split post on new lines lines := strings.Split(post, "\n") @@ -85,7 +89,7 @@ func parsePost(post string) (GemlogEntry, error) { } if separatorIndex == -1 { - return GemlogEntry{}, fmt.Errorf("no frontmatter separator '---' found") + return gemlog.GemlogEntry{}, fmt.Errorf("no frontmatter separator '---' found") } // Extract frontmatter and body @@ -103,13 +107,13 @@ func parsePost(post string) (GemlogEntry, error) { } if err := yaml.Unmarshal([]byte(frontmatterYAML), &frontmatter); err != nil { - return GemlogEntry{}, fmt.Errorf("failed to parse frontmatter: %w", err) + return gemlog.GemlogEntry{}, fmt.Errorf("failed to parse frontmatter: %w", err) } // Parse date date, err := time.Parse("2006-01-02", strings.TrimSpace(frontmatter.Date)) if err != nil { - return GemlogEntry{}, fmt.Errorf("failed to parse date: %w", err) + return gemlog.GemlogEntry{}, fmt.Errorf("failed to parse date: %w", err) } // Parse tags (comma-separated) @@ -127,7 +131,7 @@ func parsePost(post string) (GemlogEntry, error) { // Join body lines body := strings.Join(bodyLines, "\n") - return GemlogEntry{ + return gemlog.GemlogEntry{ Title: frontmatter.Title, Date: date, Slug: frontmatter.Slug, diff --git a/main.go b/main.go index cc2d5b3..9572dda 100644 --- a/main.go +++ b/main.go @@ -1,139 +1,9 @@ package main import ( - "fmt" - "gemini_site/gemlog" - "log/slog" - "os" - - tea "github.com/charmbracelet/bubbletea" + ui "git.travisshears.com/travisshears/gemlog-cli/internal/ui" ) -type Page string - -const ( - ActionList Page = "actionList" - EntryList Page = "entryList" - Entry Page = "entry" -) - -type SwitchPages struct{ Page Page } - -type uiState struct { - notification string - errorTxt string - - page Page - entryListPage EntryListPageModel - entryPage EntryPageModel - actionListPage ActionListPageModel -} - -type context struct { - config *gemlog.Config -} - -type model struct { - ui uiState - context *context -} - -func initialModel(config *gemlog.Config) model { - return model{ - ui: uiState{ - page: ActionList, - actionListPage: initialActionListPageModel(), - entryListPage: initialEntryListPageModel(), - entryPage: initialEntryPageModel(), - }, - context: &context{ - config: config, - }, - } -} - -func (m model) Init() tea.Cmd { - cmds := make([]tea.Cmd, 0) - cmds = append(cmds, tea.SetWindowTitle("Gemlog CLI")) - // TODO: add init commands from other pages - return tea.Batch(cmds...) -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - cmds := make([]tea.Cmd, 3) - switch msg := msg.(type) { - case SwitchPages: - m.ui.page = msg.Page - case gemlog.ErrorMsg: - m.ui.errorTxt = fmt.Sprintf("%s\n\n", fmt.Errorf("%s", msg)) - case gemlog.Notification: - m.ui.notification = fmt.Sprintf("%s\n\n", string(msg)) - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c", "q": - return m, tea.Quit - } - } - - actionListM, cmd := m.ui.actionListPage.Update(msg, m.ui.page == ActionList, m.context) - m.ui.actionListPage = actionListM - cmds = append(cmds, cmd) - - entryListM, cmd := m.ui.entryListPage.Update(msg, m.ui.page == EntryList, m.context) - 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...) -} - -func (m model) View() string { - if len(m.ui.errorTxt) > 0 { - return m.ui.errorTxt - } - s := "" - if m.ui.notification != "" { - s += m.ui.notification - } - - switch m.ui.page { - case ActionList: - s += m.ui.actionListPage.View() - case EntryList: - s += m.ui.entryListPage.View() - case Entry: - s += m.ui.entryPage.View() - } - - s += "\nPress q to quit.\n" - - return s -} - func main() { - f, err := tea.LogToFile("debug.log", "debug") - if err != nil { - fmt.Println("fatal:", err) - os.Exit(1) - } - defer f.Close() - slog.Info("Starting gemlog cli") - config, err := gemlog.LoadConfig() - if err != nil { - fmt.Printf("Error loading config: %v", err) - os.Exit(1) - } - err = gemlog.CheckDBConnection(config) - if err != nil { - fmt.Printf("Error checking db connection: %v", err) - os.Exit(1) - } - p := tea.NewProgram(initialModel(config)) - if _, err := p.Run(); err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) - } + ui.Run() }