add view layout and a basic content render

This commit is contained in:
2026-01-04 01:42:08 +00:00
parent 6aed5f916a
commit b9d44c3bda
6 changed files with 323 additions and 193 deletions
+50 -20
View File
@@ -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
+16 -4
View File
@@ -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
)
+43 -6
View File
@@ -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=
+73 -47
View File
@@ -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(),
)
+141
View File
@@ -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: <text>) 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
}
-116
View File
@@ -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: <text>) 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
}