From b9d44c3bdaa7a6d2c6b61dc71d6fef98ca765996 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Sun, 4 Jan 2026 01:42:08 +0000 Subject: [PATCH] add view layout and a basic content render --- fstree.go | 70 +++++++++++++++++------- go.mod | 20 +++++-- go.sum | 49 ++++++++++++++--- main.go | 120 ++++++++++++++++++++++++----------------- notes.go | 141 +++++++++++++++++++++++++++++++++++++++++++++++++ notes/notes.go | 116 ---------------------------------------- 6 files changed, 323 insertions(+), 193 deletions(-) delete mode 100644 notes/notes.go diff --git a/fstree.go b/fstree.go index 94431a0..8eb209c 100644 --- a/fstree.go +++ b/fstree.go @@ -45,6 +45,11 @@ func (n *FsNode) FileName() string { return filepath.Base(n.path) } +// ================== messages =================== +type nodeSelected struct { + path string +} + // ==================== FsNode definition ==================== type FsTree struct { root *FsNode @@ -54,6 +59,9 @@ type FsTree struct { errMsg string height int width int + viewStart int + viewEnd int + oldSelected *FsNode } // ==================== Bubble Tea Interface Implementation ==================== @@ -98,6 +106,10 @@ func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case tea.MouseMsg: + m.Y += t.viewStart // adjust for viewport + if m.X >= t.width { + break + } t.errMsg = "" t.hoveredNode = t.lines[m.Y] // hover @@ -112,9 +124,39 @@ func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } } + + t.viewStart, t.viewEnd = t.getViewportBounds(len(t.lines)) + + if t.oldSelected != t.selectedNode { + t.oldSelected = t.selectedNode + return t, func() tea.Msg { + return nodeSelected{path: t.selectedNode.path} + } + } return t, nil } +func (t *FsTree) getViewportBounds(totalLines int) (startLine, endLine int) { + selectedLine := t.selectedNode.line + halfHeight := t.height / 2 + + startLine = selectedLine - halfHeight + if startLine < 0 { + startLine = 0 + } + + endLine = startLine + t.height + if endLine > totalLines { + endLine = totalLines + startLine = endLine - t.height + if startLine < 0 { + startLine = 0 + } + } + + return startLine, endLine +} + func (t *FsTree) View() string { if t.errMsg != "" { return t.errMsg @@ -128,28 +170,15 @@ func (t *FsTree) View() string { t.renderNode(t.root, 0, builder) rendered := builder.String() - // clamp to height around selected node lines := strings.Split(rendered, "\n") - totalLines := len(lines) - selectedLine := t.selectedNode.line - halfHeight := t.height / 2 - startLine := selectedLine - halfHeight - if startLine < 0 { - startLine = 0 - } - endLine := startLine + t.height - if endLine > totalLines { - endLine = totalLines - startLine = endLine - t.height - if startLine < 0 { - startLine = 0 - } - } - - clampedLines := lines[startLine:endLine] + clampedLines := lines[t.viewStart:t.viewEnd] rendered = strings.Join(clampedLines, "\n") + rendered = lipgloss.NewStyle(). + Width(t.width). + Render(rendered) + return rendered } @@ -363,12 +392,13 @@ func (t *FsTree) CreateNode(folder *FsNode, name string, nodeType FsNodeType) er path := filepath.Join(folder.path, name) // materialise it first - if nodeType == FileNode { + switch nodeType { + case FileNode: err := createFile(path, []byte{}) if err != nil { return err } - } else if nodeType == FolderNode { + case FolderNode: err := createFolder(path) if err != nil { return err diff --git a/go.mod b/go.mod index e4fb41f..4bcc88f 100644 --- a/go.mod +++ b/go.mod @@ -5,25 +5,37 @@ go 1.25.5 require ( github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/glamour v0.10.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 + github.com/mattn/go-runewidth v0.0.16 ) require ( + github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.8 // indirect + github.com/yuin/goldmark-emoji v1.0.5 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index d1a20cb..5972c4b 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,82 @@ +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= +github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= +github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= diff --git a/main.go b/main.go index 04e0ffc..481ddf2 100644 --- a/main.go +++ b/main.go @@ -4,8 +4,6 @@ import ( "fmt" "os" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) @@ -37,30 +35,19 @@ type model struct { width int terminalWidth int terminalHeight int + fsTreeWidth int + noteViewWidth int tree *FsTree rootPath string // path to load the tree from - // spinner needs to be state as I need to update the spinner on - // each tick in update func - spinner spinner.Model - loading bool - viewport viewport.Model + loading bool + noteView *NoteView } -// initial model state -func createModel(rootPath string) model { - s := spinner.New() - s.Spinner = spinner.Dot - s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) - - return model{ - width: 30, // char count - terminalWidth: 0, - terminalHeight: 0, - tree: nil, - rootPath: rootPath, - spinner: s, - loading: true, - viewport: viewport.New(0, 0), +func NewModel(rootPath string) *model { + return &model{ + rootPath: rootPath, + loading: true, + noteView: NewNoteView(), } } @@ -71,7 +58,7 @@ type treeLoadedMsg struct { tree *FsTree } -func loadTreeCmd(path string) tea.Cmd { +func (m *model) loadTreeCmd(path string) tea.Cmd { return func() tea.Msg { var targetPath string if path == "" { @@ -88,62 +75,101 @@ func loadTreeCmd(path string) tea.Cmd { } } -func (m model) Init() tea.Cmd { - return tea.Batch(m.spinner.Tick, loadTreeCmd(m.rootPath)) +func (m *model) layout(width, height int) { + m.terminalWidth = width + m.terminalHeight = height + m.fsTreeWidth = width / 4 + m.noteViewWidth = width - m.fsTreeWidth } -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *model) Init() tea.Cmd { + return m.loadTreeCmd(m.rootPath) +} + +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: - m.terminalWidth = msg.Width - m.terminalHeight = msg.Height + m.layout(msg.Width, msg.Height) + + var cmds []tea.Cmd if m.tree != nil { - m.tree.Update(tea.WindowSizeMsg{ + _, cmd := m.tree.Update(tea.WindowSizeMsg{ + Width: m.fsTreeWidth, Height: m.terminalHeight, - Width: m.terminalWidth, }) + cmds = append(cmds, cmd) } - return m, nil + _, cmd := m.noteView.Update(tea.WindowSizeMsg{ + Width: m.noteViewWidth, + Height: m.terminalHeight, + }) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) + case treeLoadedMsg: m.tree = msg.tree m.loading = false - m.tree.Update(tea.WindowSizeMsg{ + _, cmd := m.tree.Update(tea.WindowSizeMsg{ Height: m.terminalHeight, - Width: m.terminalWidth, + Width: m.fsTreeWidth, }) - return m, nil - case spinner.TickMsg: - var cmd tea.Cmd - m.spinner, cmd = m.spinner.Update(msg) return m, cmd - case tea.MouseMsg: - if m.tree != nil { - // pass mouse events within file tree bounds to tree - if msg.X < m.width { - m.tree.Update(msg) - } - } + + case nodeSelected: + // Forward node selection to noteView + _, cmd := m.noteView.Update(loadNote{path: msg.path}) + return m, cmd + + case loadedNote: + // Forward loaded note to noteView + _, cmd := m.noteView.Update(msg) + return m, cmd + case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": return m, tea.Quit } + // Forward keyboard input to tree + var cmd tea.Cmd if m.tree != nil { - m.tree.Update(msg) + _, cmd = m.tree.Update(msg) } + return m, cmd + + case tea.MouseMsg: + // Forward mouse input to tree + var cmd tea.Cmd + if m.tree != nil { + _, cmd = m.tree.Update(msg) + } + return m, cmd } + return m, nil } func (m model) View() string { if m.loading { - return fmt.Sprintf("%s Loading files...", m.spinner.View()) + return "Loading files..." } tree := m.tree.View() + tree = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder()). + Width(40). + Align(lipgloss.Left). + PaddingRight(2). + BorderLeft(false). + BorderTop(false). + BorderBottom(false). + Render(tree) + notes := m.noteView.View() + full := lipgloss.JoinHorizontal( lipgloss.Top, tree, + notes, ) return full } @@ -157,7 +183,7 @@ func main() { } // if rootPath is empty, createModel will use cwd p := tea.NewProgram( - createModel(rootPath), + NewModel(rootPath), tea.WithAltScreen(), // full screen tui tea.WithMouseAllMotion(), ) diff --git a/notes.go b/notes.go index 06ab7d0..cfcbaa7 100644 --- a/notes.go +++ b/notes.go @@ -1 +1,142 @@ +/* +this pakckage basically handles notes which are the individual files for spaced +repetition in mend +- ruinivist, 3Jan26 +*/ + package main + +import ( + "os" + "strings" + + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" +) + +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 + // display layer + err error + loading bool + vp viewport.Model + mdRenderer *glamour.TermRenderer +} + +// ================== messages =================== +type loadNote struct { + path string +} + +type loadedNote struct { + title string + content string + hints []string + err error +} + +func NewNoteView() *NoteView { + mdRenderer, _ := glamour.NewTermRenderer( + glamour.WithAutoStyle(), + ) + return &NoteView{ + loading: true, + mdRenderer: mdRenderer, + vp: viewport.New(0, 0), + } +} + +func (m *NoteView) Init() tea.Cmd { + return nil +} + +func (m *NoteView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.vp.Width = msg.Width + m.vp.Height = msg.Height + return m, nil + + case loadNote: + if m.path == msg.path { + return m, nil //noop + } + m.path = msg.path + m.loading = true + return m, fetchContent(msg.path) + + case loadedNote: + m.loading = false + m.title = msg.title + m.content = msg.content + m.hints = msg.hints + m.err = msg.err + m.vp.SetContent(m.renderNote()) + return m, nil + } + return m, nil +} + +func (m NoteView) View() string { + if m.loading { + return "Loading note..." + } + + if m.err != nil { + return "Error: " + m.err.Error() + } + + return m.vp.View() +} + +func fetchContent(path string) tea.Cmd { + return func() tea.Msg { + data, err := os.ReadFile(path) + if err != nil { + return loadedNote{err: err} + } + + content := string(data) + lines := strings.Split(content, "\n") + + var title string + + // title + if len(lines) > 0 && strings.HasPrefix(lines[0], "# ") { + title = strings.TrimPrefix(lines[0], "# ") + content = strings.Join(lines[1:], "\n") + } + + // hints + hints := extractHints(content) + content = strings.TrimSpace(content) + + return loadedNote{ + title: title, + content: content, + hints: hints, + } + } +} + +func extractHints(content string) []string { + hints := make([]string, 0) + return hints // to add later +} + +func (m NoteView) renderNote() string { + title, err1 := m.mdRenderer.Render(m.title) + content, err2 := m.mdRenderer.Render(m.content) + + if err1 != nil || err2 != nil { + // some error return raw + return m.title + "\n\n" + m.content + } + + return title + "\n\n" + content +} diff --git a/notes/notes.go b/notes/notes.go deleted file mode 100644 index 97c0a4e..0000000 --- a/notes/notes.go +++ /dev/null @@ -1,116 +0,0 @@ -/* -this pakckage basically handles notes which are the individual files for spaced -repetition in mend -- ruinivist, 3Jan26 -*/ - -package notes - -import ( - "os" - "strings" - - tea "github.com/charmbracelet/bubbletea" -) - -type Model struct { - 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 - err error - loading bool -} - -// ================== messages =================== -type contentFetchedMsg struct { - title string - content string - hints []string - err error -} - -func NewNote(path string) Model { - return Model{ - path: path, - loading: true, - } -} - -func (m Model) Init() tea.Cmd { - return fetchContent(m.path) -} - -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case contentFetchedMsg: - m.loading = false - m.title = msg.title - m.content = msg.content - m.hints = msg.hints - m.err = msg.err - return m, nil - } - return m, nil -} - -func (m Model) View() string { - if m.loading { - return "Loading note..." - } - - if m.err != nil { - return "Error: " + m.err.Error() - } - - var sb strings.Builder - sb.WriteString(m.title) - sb.WriteString("\n\n") - sb.WriteString(m.content) - - if len(m.hints) > 0 { - sb.WriteString("\n\nHints:\n") - for _, hint := range m.hints { - sb.WriteString("- ") - sb.WriteString(hint) - sb.WriteString("\n") - } - } - - return sb.String() -} - -func fetchContent(path string) tea.Cmd { - return func() tea.Msg { - data, err := os.ReadFile(path) - if err != nil { - return contentFetchedMsg{err: err} - } - - content := string(data) - lines := strings.Split(content, "\n") - - var title string - - // tit;e - if len(lines) > 0 && strings.HasPrefix(lines[0], "# ") { - title = strings.TrimPrefix(lines[0], "# ") - content = strings.Join(lines[1:], "\n") - } - - // hints - hints := extractHints(content) - content = strings.TrimSpace(content) - - return contentFetchedMsg{ - title: title, - content: content, - hints: hints, - } - } -} - -func extractHints(content string) []string { - hints := make([]string, 0) - return hints // to add later -}