diff --git a/fstree.go b/fstree.go index 81f7676..fcf84b9 100644 --- a/fstree.go +++ b/fstree.go @@ -101,11 +101,11 @@ func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: t.errMsg = "" switch m.String() { - case "w": + case "w", "up": _ = t.MoveUp() - case "s": + case "s", "down": _ = t.MoveDown() - case "e": + case "e", "space": _ = t.ToggleSelectedExpand() case "n": // new file return t, func() tea.Msg { return RequestInputMsg{Action: ActionNewFile} } diff --git a/main.go b/main.go index ec0ec04..6a447cb 100644 --- a/main.go +++ b/main.go @@ -202,6 +202,12 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } + // If editing, forward all keys to noteView and ignore global bindings + if m.noteView.IsEditing() { + _, cmd := m.noteView.Update(msg) + return m, cmd + } + switch msg.String() { case "q", "ctrl+c": return m, tea.Quit diff --git a/notes.go b/notes.go index 297cf75..b0d731c 100644 --- a/notes.go +++ b/notes.go @@ -11,6 +11,7 @@ import ( "regexp" "strings" + "github.com/charmbracelet/bubbles/textarea" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" @@ -26,16 +27,20 @@ const ( type NoteView struct { // the actual content - path string - title string // the first line is always # title - content string // strip the first line from file rest is content - hints []string //a hint is anything that matches (hint: ) in content + path string + title string // the first line is always # title + content string // strip the first line from file rest is content + rawContent string // full content for editing + hints []string //a hint is anything that matches (hint: ) in content // display layer err error loading bool vp viewport.Model mdRenderer *glamour.TermRenderer viewState ViewState + // editing + textarea textarea.Model + isEditing bool } // ================== messages =================== @@ -44,10 +49,11 @@ type loadNote struct { } type loadedNote struct { - title string - content string - hints []string - err error + title string + content string + rawContent string + hints []string + err error } func NewNoteView() *NoteView { @@ -55,16 +61,23 @@ func NewNoteView() *NoteView { glamour.WithAutoStyle(), glamour.WithWordWrap(80), ) + ta := textarea.New() + ta.Focus() return &NoteView{ loading: true, mdRenderer: mdRenderer, vp: viewport.New(0, 0), viewState: StateTitleOnly, + textarea: ta, } } func (m *NoteView) Init() tea.Cmd { - return nil + return textarea.Blink +} + +func (m *NoteView) IsEditing() bool { + return m.isEditing } func (m *NoteView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -72,22 +85,38 @@ func (m *NoteView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.vp.Width = msg.Width m.vp.Height = msg.Height + m.textarea.SetWidth(msg.Width) + m.textarea.SetHeight(msg.Height) return m, nil case tea.KeyMsg: + if m.isEditing { + switch msg.String() { + case "ctrl+w": + m.isEditing = false + return m, nil + case "ctrl+s": + return m, saveContent(m.path, m.textarea.Value()) + } + var cmd tea.Cmd + m.textarea, cmd = m.textarea.Update(msg) + return m, cmd + } + switch msg.String() { - case "d": + case "enter": + if m.path != "" && !m.loading { + m.isEditing = true + m.textarea.SetValue(m.rawContent) + m.textarea.Focus() + return m, textarea.Blink + } + case "d", "right": m.viewState = StateContent m.vp.SetContent(m.renderNote()) - case "a": + case "a", "left": m.viewState = StateHints m.vp.SetContent(m.renderNote()) - case "up": - m.vp.ScrollUp(1) - return m, nil - case "down": - m.vp.ScrollDown(1) - return m, nil case "pgup": m.vp.PageUp() return m, nil @@ -104,6 +133,7 @@ func (m *NoteView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil //noop } m.path = msg.path + m.isEditing = false m.loading = true return m, fetchContent(msg.path) @@ -111,13 +141,18 @@ func (m *NoteView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.loading = false m.title = msg.title m.content = msg.content + m.rawContent = msg.rawContent m.hints = msg.hints m.err = msg.err m.viewState = StateTitleOnly m.vp.SetContent(m.renderNote()) + return m, nil case tea.MouseMsg: + if m.isEditing { + return m, nil // or handle mouse in textarea if supported + } var cmd tea.Cmd m.vp, cmd = m.vp.Update(msg) return m, cmd @@ -134,6 +169,10 @@ func (m NoteView) View() string { return "Error: " + m.err.Error() } + if m.isEditing { + return m.textarea.View() + } + return m.vp.View() } @@ -144,7 +183,8 @@ func fetchContent(path string) tea.Cmd { return loadedNote{err: err} } - content := string(data) + rawContent := string(data) + content := rawContent lines := strings.Split(content, "\n") var title string @@ -160,13 +200,24 @@ func fetchContent(path string) tea.Cmd { content = strings.TrimSpace(content) return loadedNote{ - title: title, - content: content, - hints: hints, + title: title, + content: content, + rawContent: rawContent, + hints: hints, } } } +func saveContent(path, content string) tea.Cmd { + return func() tea.Msg { + err := os.WriteFile(path, []byte(content), 0644) + if err != nil { + return loadedNote{err: err} + } + return fetchContent(path)() + } +} + func extractHints(content string) []string { re := regexp.MustCompile(`(?s)\(hint:\s*(.*?)\)`) matches := re.FindAllStringSubmatch(content, -1) @@ -203,7 +254,7 @@ func (m NoteView) renderNote() string { body, err2 = m.mdRenderer.Render(hintsList) } case StateTitleOnly: - body = "\n(Press 'space' to show content, 'h' to show hints)" + body = "'a' to view hints, 'd' to view content." } if err2 != nil {