feat(tui): add bubbletea terminal UI with entity list, detail, and capture
Adds `nib tui` command and `make tui` target. Scrollable entity list with j/k navigation, enter for detail view, `a` to capture new entries using the existing parse grammar, and `d` to delete.
This commit is contained in:
@@ -2,7 +2,7 @@ BINARY := nib
|
|||||||
MODULE := github.com/lerko/nib
|
MODULE := github.com/lerko/nib
|
||||||
GOFLAGS := -trimpath
|
GOFLAGS := -trimpath
|
||||||
|
|
||||||
.PHONY: build dev watch test lint fmt vet clean run cert help
|
.PHONY: build dev tui watch test lint fmt vet clean run cert help
|
||||||
|
|
||||||
## —— Build ——————————————————————————————————
|
## —— Build ——————————————————————————————————
|
||||||
|
|
||||||
@@ -12,6 +12,9 @@ build: ## Build production binary
|
|||||||
dev: ## Build and run with default serve
|
dev: ## Build and run with default serve
|
||||||
go run . serve
|
go run . serve
|
||||||
|
|
||||||
|
tui: ## Launch the terminal UI
|
||||||
|
go run . tui
|
||||||
|
|
||||||
watch: ## Live-reload dev server (requires air)
|
watch: ## Live-reload dev server (requires air)
|
||||||
air
|
air
|
||||||
|
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/lerko/nib/internal/tui"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tuiCmd = &cobra.Command{
|
||||||
|
Use: "tui",
|
||||||
|
Short: "launch the terminal UI",
|
||||||
|
RunE: runTUI,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(tuiCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTUI(_ *cobra.Command, _ []string) error {
|
||||||
|
store, err := openStore()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
return tui.Run(store)
|
||||||
|
}
|
||||||
@@ -11,15 +11,36 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0 // indirect
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // 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.19 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
modernc.org/libc v1.65.7 // indirect
|
modernc.org/libc v1.65.7 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|||||||
@@ -1,8 +1,32 @@
|
|||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
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/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
|
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.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
|
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/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
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/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
@@ -11,8 +35,20 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
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/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||||
@@ -20,11 +56,15 @@ github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNs
|
|||||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
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=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
@@ -32,9 +72,14 @@ golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
|||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.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/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
"github.com/lerko/nib/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type entitiesLoadedMsg struct {
|
||||||
|
entities []*db.Entity
|
||||||
|
}
|
||||||
|
|
||||||
|
type entityCreatedMsg struct {
|
||||||
|
entity *db.Entity
|
||||||
|
}
|
||||||
|
|
||||||
|
type entityDeletedMsg struct {
|
||||||
|
id string
|
||||||
|
}
|
||||||
|
|
||||||
|
type errMsg struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadEntities(store *db.Store, params db.ListParams) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
entities, err := store.List(params)
|
||||||
|
if err != nil {
|
||||||
|
return errMsg{err}
|
||||||
|
}
|
||||||
|
return entitiesLoadedMsg{entities}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEntity(store *db.Store, e *db.Entity) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
if err := store.Create(e); err != nil {
|
||||||
|
return errMsg{err}
|
||||||
|
}
|
||||||
|
return entityCreatedMsg{e}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteEntity(store *db.Store, id string) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
if _, err := store.SoftDelete(id); err != nil {
|
||||||
|
return errMsg{err}
|
||||||
|
}
|
||||||
|
return entityDeletedMsg{id}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
"github.com/lerko/nib/internal/db"
|
||||||
|
"github.com/lerko/nib/internal/display"
|
||||||
|
)
|
||||||
|
|
||||||
|
type detailModel struct {
|
||||||
|
entity *db.Entity
|
||||||
|
scroll int
|
||||||
|
height int
|
||||||
|
width int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDetailModel() detailModel {
|
||||||
|
return detailModel{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *detailModel) setEntity(e *db.Entity) {
|
||||||
|
d.entity = e
|
||||||
|
d.scroll = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *detailModel) setSize(width, height int) {
|
||||||
|
d.width = width
|
||||||
|
d.height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d detailModel) update(msg tea.KeyMsg) detailModel {
|
||||||
|
switch msg.String() {
|
||||||
|
case "up", "k":
|
||||||
|
if d.scroll > 0 {
|
||||||
|
d.scroll--
|
||||||
|
}
|
||||||
|
case "down", "j":
|
||||||
|
d.scroll++
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d detailModel) view(width int) string {
|
||||||
|
if d.entity == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
e := d.entity
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
glyph := display.DisplayGlyph(e.Glyph, e.CardType)
|
||||||
|
header := fmt.Sprintf("%s %s", glyph, display.FormatID(e.ID))
|
||||||
|
b.WriteString(detailHeaderStyle.Render(header))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
|
if e.Title != nil {
|
||||||
|
b.WriteString(detailBodyStyle.Render("title: " + *e.Title))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(detailBodyStyle.Render(e.Body))
|
||||||
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
if len(e.Tags) > 0 {
|
||||||
|
tagParts := make([]string, len(e.Tags))
|
||||||
|
for i, t := range e.Tags {
|
||||||
|
tagParts[i] = tagStyle.Render("#" + t)
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(detailBodyStyle.Render(strings.Join(tagParts, " ")))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\n")
|
||||||
|
meta := fmt.Sprintf("created %s", e.CreatedAt.Format(time.DateTime))
|
||||||
|
if e.ModifiedAt != e.CreatedAt {
|
||||||
|
meta += fmt.Sprintf("\nmodified %s", e.ModifiedAt.Format(time.DateTime))
|
||||||
|
}
|
||||||
|
if e.TimeAnchor != nil {
|
||||||
|
meta += fmt.Sprintf("\nanchored @%s", *e.TimeAnchor)
|
||||||
|
}
|
||||||
|
if e.Pinned {
|
||||||
|
meta += "\npinned"
|
||||||
|
}
|
||||||
|
if e.CardType != nil {
|
||||||
|
meta += fmt.Sprintf("\ncard %s", *e.CardType)
|
||||||
|
}
|
||||||
|
if e.CompletedAt != nil {
|
||||||
|
meta += fmt.Sprintf("\ndone %s", e.CompletedAt.Format(time.DateTime))
|
||||||
|
}
|
||||||
|
b.WriteString(idStyle.Render(meta))
|
||||||
|
|
||||||
|
lines := strings.Split(b.String(), "\n")
|
||||||
|
if d.scroll > 0 && d.scroll < len(lines) {
|
||||||
|
lines = lines[d.scroll:]
|
||||||
|
}
|
||||||
|
if d.height > 0 && len(lines) > d.height {
|
||||||
|
lines = lines[:d.height]
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
"github.com/lerko/nib/internal/db"
|
||||||
|
"github.com/lerko/nib/internal/parse"
|
||||||
|
)
|
||||||
|
|
||||||
|
type inputModel struct {
|
||||||
|
ti textinput.Model
|
||||||
|
active bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInputModel() inputModel {
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Placeholder = "capture a thought…"
|
||||||
|
ti.Prompt = inputPromptStyle.Render("› ")
|
||||||
|
ti.CharLimit = 500
|
||||||
|
return inputModel{ti: ti}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *inputModel) focus() {
|
||||||
|
i.active = true
|
||||||
|
i.ti.Focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *inputModel) reset() {
|
||||||
|
i.active = false
|
||||||
|
i.ti.SetValue("")
|
||||||
|
i.ti.Blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i inputModel) submit() *db.Entity {
|
||||||
|
val := i.ti.Value()
|
||||||
|
if val == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := parse.Parse(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e := &db.Entity{
|
||||||
|
Body: parsed.Body,
|
||||||
|
Title: parsed.Title,
|
||||||
|
Glyph: db.Glyph(parsed.Glyph),
|
||||||
|
Tags: parsed.Tags,
|
||||||
|
}
|
||||||
|
if parsed.TimeAnchor != nil {
|
||||||
|
e.TimeAnchor = parsed.TimeAnchor
|
||||||
|
}
|
||||||
|
if parsed.CardSuffix != nil {
|
||||||
|
ct := db.CardType(*parsed.CardSuffix)
|
||||||
|
e.CardType = &ct
|
||||||
|
}
|
||||||
|
if parsed.Pin {
|
||||||
|
e.Pinned = true
|
||||||
|
}
|
||||||
|
if parsed.Description != nil {
|
||||||
|
e.Description = parsed.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i inputModel) updateKey(msg tea.KeyMsg) inputModel {
|
||||||
|
i.ti, _ = i.ti.Update(msg)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i inputModel) view(width int) string {
|
||||||
|
return i.ti.View()
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/bubbles/key"
|
||||||
|
|
||||||
|
type keyMap struct {
|
||||||
|
Up key.Binding
|
||||||
|
Down key.Binding
|
||||||
|
Enter key.Binding
|
||||||
|
Back key.Binding
|
||||||
|
Add key.Binding
|
||||||
|
Delete key.Binding
|
||||||
|
Quit key.Binding
|
||||||
|
Help key.Binding
|
||||||
|
PageUp key.Binding
|
||||||
|
PageDn key.Binding
|
||||||
|
Top key.Binding
|
||||||
|
Bottom key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = keyMap{
|
||||||
|
Up: key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up")),
|
||||||
|
Down: key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down")),
|
||||||
|
Enter: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "view")),
|
||||||
|
Back: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "back")),
|
||||||
|
Add: key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "add")),
|
||||||
|
Delete: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "delete")),
|
||||||
|
Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")),
|
||||||
|
Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")),
|
||||||
|
PageUp: key.NewBinding(key.WithKeys("pgup", "ctrl+u"), key.WithHelp("pgup", "page up")),
|
||||||
|
PageDn: key.NewBinding(key.WithKeys("pgdown", "ctrl+d"), key.WithHelp("pgdn", "page down")),
|
||||||
|
Top: key.NewBinding(key.WithKeys("home", "g"), key.WithHelp("home", "top")),
|
||||||
|
Bottom: key.NewBinding(key.WithKeys("end", "G"), key.WithHelp("end", "bottom")),
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
"github.com/lerko/nib/internal/db"
|
||||||
|
"github.com/lerko/nib/internal/display"
|
||||||
|
)
|
||||||
|
|
||||||
|
type listModel struct {
|
||||||
|
entities []*db.Entity
|
||||||
|
cursor int
|
||||||
|
offset int
|
||||||
|
height int
|
||||||
|
width int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListModel() listModel {
|
||||||
|
return listModel{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *listModel) setEntities(entities []*db.Entity) {
|
||||||
|
l.entities = entities
|
||||||
|
if l.cursor >= len(entities) {
|
||||||
|
l.cursor = max(0, len(entities)-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *listModel) setSize(width, height int) {
|
||||||
|
l.width = width
|
||||||
|
l.height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l listModel) selected() *db.Entity {
|
||||||
|
if len(l.entities) == 0 || l.cursor >= len(l.entities) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return l.entities[l.cursor]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l listModel) update(msg tea.KeyMsg) listModel {
|
||||||
|
switch msg.String() {
|
||||||
|
case "up", "k":
|
||||||
|
if l.cursor > 0 {
|
||||||
|
l.cursor--
|
||||||
|
if l.cursor < l.offset {
|
||||||
|
l.offset = l.cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "down", "j":
|
||||||
|
if l.cursor < len(l.entities)-1 {
|
||||||
|
l.cursor++
|
||||||
|
visible := l.visibleCount()
|
||||||
|
if l.cursor >= l.offset+visible {
|
||||||
|
l.offset = l.cursor - visible + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "home", "g":
|
||||||
|
l.cursor = 0
|
||||||
|
l.offset = 0
|
||||||
|
case "end", "G":
|
||||||
|
l.cursor = max(0, len(l.entities)-1)
|
||||||
|
visible := l.visibleCount()
|
||||||
|
if l.cursor >= visible {
|
||||||
|
l.offset = l.cursor - visible + 1
|
||||||
|
}
|
||||||
|
case "pgup", "ctrl+u":
|
||||||
|
l.cursor = max(0, l.cursor-l.visibleCount())
|
||||||
|
if l.cursor < l.offset {
|
||||||
|
l.offset = l.cursor
|
||||||
|
}
|
||||||
|
case "pgdown", "ctrl+d":
|
||||||
|
l.cursor = min(len(l.entities)-1, l.cursor+l.visibleCount())
|
||||||
|
visible := l.visibleCount()
|
||||||
|
if l.cursor >= l.offset+visible {
|
||||||
|
l.offset = l.cursor - visible + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l listModel) view(width int) string {
|
||||||
|
if len(l.entities) == 0 {
|
||||||
|
return statusStyle.Render("no entities")
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
visible := l.visibleCount()
|
||||||
|
end := min(l.offset+visible, len(l.entities))
|
||||||
|
|
||||||
|
for i := l.offset; i < end; i++ {
|
||||||
|
e := l.entities[i]
|
||||||
|
line := renderEntity(e, width-4)
|
||||||
|
|
||||||
|
if i == l.cursor {
|
||||||
|
b.WriteString(selectedItemStyle.Render(" " + line))
|
||||||
|
} else {
|
||||||
|
b.WriteString(listItemStyle.Render(line))
|
||||||
|
}
|
||||||
|
if i < end-1 {
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l listModel) visibleCount() int {
|
||||||
|
if l.height <= 0 {
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
return l.height
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderEntity(e *db.Entity, maxWidth int) string {
|
||||||
|
glyph := glyphStyle.Render(display.DisplayGlyph(e.Glyph, e.CardType))
|
||||||
|
id := idStyle.Render("[" + display.FormatID(e.ID) + "]")
|
||||||
|
|
||||||
|
body := e.Body
|
||||||
|
if e.Title != nil {
|
||||||
|
body = *e.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags string
|
||||||
|
if len(e.Tags) > 0 {
|
||||||
|
tagParts := make([]string, len(e.Tags))
|
||||||
|
for i, t := range e.Tags {
|
||||||
|
tagParts[i] = tagStyle.Render("#" + t)
|
||||||
|
}
|
||||||
|
tags = " " + strings.Join(tagParts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
line := fmt.Sprintf("%s %s%s %s", glyph, body, tags, id)
|
||||||
|
|
||||||
|
if maxWidth > 0 && len(stripAnsi(line)) > maxWidth {
|
||||||
|
body = truncate(body, maxWidth-20)
|
||||||
|
line = fmt.Sprintf("%s %s%s %s", glyph, body, tags, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncate(s string, maxLen int) string {
|
||||||
|
if maxLen <= 3 {
|
||||||
|
return "…"
|
||||||
|
}
|
||||||
|
runes := []rune(s)
|
||||||
|
if len(runes) <= maxLen {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return string(runes[:maxLen-1]) + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripAnsi(s string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
inEsc := false
|
||||||
|
for _, r := range s {
|
||||||
|
if r == '\x1b' {
|
||||||
|
inEsc = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inEsc {
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
|
||||||
|
inEsc = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
"github.com/lerko/nib/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type viewState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
stateList viewState = iota
|
||||||
|
stateDetail
|
||||||
|
stateInput
|
||||||
|
)
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
store *db.Store
|
||||||
|
state viewState
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
|
||||||
|
list listModel
|
||||||
|
detail detailModel
|
||||||
|
input inputModel
|
||||||
|
|
||||||
|
status string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newModel(store *db.Store) model {
|
||||||
|
return model{
|
||||||
|
store: store,
|
||||||
|
state: stateList,
|
||||||
|
list: newListModel(),
|
||||||
|
detail: newDetailModel(),
|
||||||
|
input: newInputModel(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return loadEntities(m.store, db.DefaultListParams())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
m.list.setSize(m.width, m.contentHeight())
|
||||||
|
m.detail.setSize(m.width, m.contentHeight())
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case entitiesLoadedMsg:
|
||||||
|
m.list.setEntities(msg.entities)
|
||||||
|
m.status = ""
|
||||||
|
m.err = nil
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case entityCreatedMsg:
|
||||||
|
m.state = stateList
|
||||||
|
m.input.reset()
|
||||||
|
m.status = "created"
|
||||||
|
return m, loadEntities(m.store, db.DefaultListParams())
|
||||||
|
|
||||||
|
case entityDeletedMsg:
|
||||||
|
m.status = "deleted"
|
||||||
|
return m, loadEntities(m.store, db.DefaultListParams())
|
||||||
|
|
||||||
|
case errMsg:
|
||||||
|
m.err = msg.err
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case tea.KeyMsg:
|
||||||
|
if m.state == stateInput {
|
||||||
|
return m.updateInput(msg)
|
||||||
|
}
|
||||||
|
return m.updateKeys(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) updateKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
switch {
|
||||||
|
case msg.String() == "q" || msg.String() == "ctrl+c":
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case msg.String() == "a" && m.state == stateList:
|
||||||
|
m.state = stateInput
|
||||||
|
m.input.focus()
|
||||||
|
return m, m.input.ti.Focus()
|
||||||
|
|
||||||
|
case msg.String() == "esc":
|
||||||
|
if m.state == stateDetail {
|
||||||
|
m.state = stateList
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case msg.String() == "enter" && m.state == stateList:
|
||||||
|
if e := m.list.selected(); e != nil {
|
||||||
|
m.detail.setEntity(e)
|
||||||
|
m.state = stateDetail
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case msg.String() == "d" && m.state == stateList:
|
||||||
|
if e := m.list.selected(); e != nil {
|
||||||
|
return m, deleteEntity(m.store, e.ID)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m.state {
|
||||||
|
case stateList:
|
||||||
|
m.list = m.list.update(msg)
|
||||||
|
case stateDetail:
|
||||||
|
m.detail = m.detail.update(msg)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) updateInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc":
|
||||||
|
m.state = stateList
|
||||||
|
m.input.reset()
|
||||||
|
return m, nil
|
||||||
|
case "enter":
|
||||||
|
if e := m.input.submit(); e != nil {
|
||||||
|
return m, createEntity(m.store, e)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.input = m.input.updateKey(msg)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
var content string
|
||||||
|
|
||||||
|
switch m.state {
|
||||||
|
case stateList:
|
||||||
|
content = m.list.view(m.width)
|
||||||
|
case stateDetail:
|
||||||
|
content = m.detail.view(m.width)
|
||||||
|
case stateInput:
|
||||||
|
content = m.list.view(m.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
header := titleStyle.Render("nib")
|
||||||
|
footer := m.footerView()
|
||||||
|
|
||||||
|
return header + "\n" + content + "\n" + footer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) footerView() string {
|
||||||
|
if m.state == stateInput {
|
||||||
|
return m.input.view(m.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.err != nil {
|
||||||
|
return errorStyle.Render("error: " + m.err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.status != "" {
|
||||||
|
return statusStyle.Render(m.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return helpStyle.Render("a:add enter:view d:delete q:quit ?:help")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) contentHeight() int {
|
||||||
|
return m.height - 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
|
var (
|
||||||
|
subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"}
|
||||||
|
highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
|
||||||
|
dim = lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}
|
||||||
|
|
||||||
|
titleStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(highlight).
|
||||||
|
PaddingLeft(1)
|
||||||
|
|
||||||
|
statusStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(dim).
|
||||||
|
PaddingLeft(1)
|
||||||
|
|
||||||
|
listItemStyle = lipgloss.NewStyle().
|
||||||
|
PaddingLeft(2)
|
||||||
|
|
||||||
|
selectedItemStyle = lipgloss.NewStyle().
|
||||||
|
PaddingLeft(1).
|
||||||
|
Bold(true).
|
||||||
|
Foreground(highlight).
|
||||||
|
SetString("›")
|
||||||
|
|
||||||
|
glyphStyle = lipgloss.NewStyle().
|
||||||
|
Width(2)
|
||||||
|
|
||||||
|
tagStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"})
|
||||||
|
|
||||||
|
idStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(dim)
|
||||||
|
|
||||||
|
inputPromptStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(highlight).
|
||||||
|
Bold(true)
|
||||||
|
|
||||||
|
detailHeaderStyle = lipgloss.NewStyle().
|
||||||
|
Bold(true).
|
||||||
|
Foreground(highlight).
|
||||||
|
MarginBottom(1)
|
||||||
|
|
||||||
|
detailBodyStyle = lipgloss.NewStyle().
|
||||||
|
PaddingLeft(2).
|
||||||
|
PaddingTop(1)
|
||||||
|
|
||||||
|
helpStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(dim).
|
||||||
|
PaddingLeft(1)
|
||||||
|
|
||||||
|
errorStyle = lipgloss.NewStyle().
|
||||||
|
Foreground(lipgloss.Color("#FF0000")).
|
||||||
|
PaddingLeft(1)
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
"github.com/lerko/nib/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(store *db.Store) error {
|
||||||
|
m := newModel(store)
|
||||||
|
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user