From 3ffb113464106c7a39bcd2e8287cb1014c471a48 Mon Sep 17 00:00:00 2001 From: Justin Hawkins Date: Wed, 22 Jul 2026 14:28:43 +0930 Subject: [PATCH] first commit --- README.md | 1 + go.mod | 28 +++++++ go.sum | 46 ++++++++++++ main.go | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 288 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d84c38 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Journal diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c853880 --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module code.ppl.town/justin/journal + +go 1.26.4 + +require ( + charm.land/bubbles/v2 v2.1.1 + charm.land/bubbletea/v2 v2.0.8 + charm.land/lipgloss/v2 v2.0.5 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..99e76e3 --- /dev/null +++ b/go.sum @@ -0,0 +1,46 @@ +charm.land/bubbles/v2 v2.1.1 h1:7r55WzBxpo/R3z98hGmY7KKPd3ET6vsf0Fb9sDHOV60= +charm.land/bubbles/v2 v2.1.1/go.mod h1:GE6M31gaWZVXzGw73OeuTTgy4lX+OtkH0E5ymnNsHxo= +charm.land/bubbletea/v2 v2.0.8 h1:SxTJMhCAI3lbPmy4SgX5LWZ24AdINr4I6UEqzZvYJuY= +charm.land/bubbletea/v2 v2.0.8/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss= +charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= +charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +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-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 h1:3FmWoGNWK4STvqg0O0Aeav2T7rodWJAPeF0QpH+8gFw= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= diff --git a/main.go b/main.go new file mode 100644 index 0000000..5fa781d --- /dev/null +++ b/main.go @@ -0,0 +1,213 @@ +package main + +// A simple program demonstrating the textarea component from the Bubbles +// component library. + +import ( + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" + "time" + + "charm.land/bubbles/v2/textarea" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" +) + +func main() { + p := tea.NewProgram(initialModel()) + + if _, err := p.Run(); err != nil { + log.Fatal(err) + } +} + +type transientMsg struct { + msg string + until time.Time +} + +type WriteSeekCloser interface { + io.WriteSeeker + io.Closer +} + +type model struct { + textarea textarea.Model + fh WriteSeekCloser + title string + transientMsgs []transientMsg +} + +func getWriter() (WriteSeekCloser, error) { + hd, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + fp := filepath.Join(hd, "journal") + fpS, err := os.Stat(fp) + if err != nil { + return nil, err + } + if !fpS.IsDir() { + return nil, fmt.Errorf("fp: %s is not a dir", fp) + } + + fn := fmt.Sprintf("journal-%s", time.Now().Format("2006-01-02_150405.md")) + + file, err := os.Create(filepath.Join(fp, fn)) + if err != nil { + return nil, err + } + return file, nil +} + +func initialModel() model { + ti := textarea.New() + ti.Placeholder = "What's happening Peter ..." + ti.SetVirtualCursor(false) + ti.SetStyles(textarea.DefaultStyles(true)) // default to dark styles. + ti.Focus() + ti.SetHeight(20) + ti.SetWidth(80) + + fh, err := getWriter() + if err != nil { + panic(err) + } + return model{ + title: time.Now().Format("2006-01-02 15:04"), + textarea: ti, + fh: fh, + } +} + +func (m model) Init() tea.Cmd { + return tea.Batch(textarea.Blink, tea.RequestBackgroundColor) +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + // clean out transient msgs + newMsgs := []transientMsg{} + for _, message := range m.transientMsgs { + if message.until.After(time.Now()) { + newMsgs = append(newMsgs, message) + } + } + + m.transientMsgs = newMsgs + + switch msg := msg.(type) { + case tea.BackgroundColorMsg: + // Update styling now that we know the background color. + m.textarea.SetStyles(textarea.DefaultStyles(msg.IsDark())) + case tea.KeyPressMsg: + switch msg.String() { + case "super+s", "ctrl+s": + var err error + err = m.writeFile() + m.transientMsgs = append(m.transientMsgs, transientMsg{ + msg: "saved", + until: time.Now().Add(time.Second * 2), + }) + if err != nil { + m.transientMsgs = append(m.transientMsgs, transientMsg{ + msg: "saved", + until: time.Now().Add(time.Second * 2), + }) + panic(err) + } + case "esc": + if m.textarea.Focused() { + m.textarea.Blur() + } + case "ctrl+c": + var err error + err = m.writeFile() + if err == nil { + err = m.fh.Close() + } + if err == nil { + return m, tea.Quit + } else { + panic(err) + } + default: + if !m.textarea.Focused() { + cmd = m.textarea.Focus() + cmds = append(cmds, cmd) + } + } + + // We handle errors just like any other message + // case errMsg: + // m.err = msg + // return m, nil + } + + m.textarea, cmd = m.textarea.Update(msg) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) +} + +func (m model) headerView() string { + return m.title + "\n" +} + +func (m model) writeFile() error { + _, err := m.fh.Seek(0, io.SeekStart) + if err != nil { + return fmt.Errorf("seek: %w", err) + } + + m.fh.Write([]byte("# " + m.title + "\n\n")) + + _, err = m.fh.Write([]byte(m.textarea.Value())) + if err != nil { + return fmt.Errorf("write: %w", err) + } + return nil +} + +func (m model) View() tea.View { + footer1 := "(ctrl+c to quit)" + footer2 := "" + + for _, message := range m.transientMsgs { + footer2 += message.msg + ", " + } + footer2 = strings.TrimRight(footer2, ", ") + if len(footer2) > m.textarea.Width() { + footer2 = footer2[:m.textarea.Width()] + } + + var c *tea.Cursor + if !m.textarea.VirtualCursor() { + c = m.textarea.Cursor() + + if c != nil { + // Set the y offset of the cursor based on the position of the textarea + // in the application. + offset := lipgloss.Height(m.headerView()) + c.Y += offset + } + } + + f := strings.Join([]string{ + m.headerView(), + m.textarea.View(), + footer1, + footer2, + }, "\n") + + v := tea.NewView(f) + v.Cursor = c + return v +}