add view layout and a basic content render
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
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,
|
||||
func NewModel(rootPath string) *model {
|
||||
return &model{
|
||||
rootPath: rootPath,
|
||||
spinner: s,
|
||||
loading: true,
|
||||
viewport: viewport.New(0, 0),
|
||||
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(),
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user