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" "code.ppl.town/justin/journal/config" ) func main() { cfg, created, err := config.LoadOrCreate() if err != nil { fmt.Printf("could not get config: %s", err.Error()) os.Exit(1) } if created { cp, _ := config.Path() fmt.Printf("configuration created at '%s' - please check and re-run\n", cp) os.Exit(0) } p := tea.NewProgram(initialModel(cfg)) if _, err := p.Run(); err != nil { log.Fatal(err) } } type transientMsg struct { msg string until time.Time } type WriteSeekCloser interface { io.WriteSeeker io.Closer Sync() error } type model struct { textarea textarea.Model fh WriteSeekCloser title string transientMsgs []transientMsg } func getWriter(cfg config.Config) (WriteSeekCloser, error) { fp := cfg.JournalDir 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(cfg config.Config) 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(cfg) 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.WindowSizeMsg: m.textarea.SetHeight(msg.Height - 5) m.textarea.SetWidth(msg.Width - 6) 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 m.fh.Sync() } 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 }