Commit
Author: Kevin Schoon [me@kevinschoon.com]
Hash: 421349d614f984def6e3c46d8e144cdeb462dccf
Timestamp: Tue, 31 May 2022 06:34:10 +0000 (2 years ago)

+241 -13 +/-9 browse
implement basic io
1diff --git a/go.mod b/go.mod
2index 49876aa..b84acf3 100644
3--- a/go.mod
4+++ b/go.mod
5 @@ -5,6 +5,7 @@ go 1.18
6 require (
7 github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
8 github.com/ghodss/yaml v1.0.0 // indirect
9+ github.com/mattn/go-sqlite3 v1.14.13 // indirect
10 github.com/russross/blackfriday/v2 v2.1.0 // indirect
11 github.com/urfave/cli/v2 v2.8.1 // indirect
12 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
13 diff --git a/go.sum b/go.sum
14index ed05ce3..d7fec5e 100644
15--- a/go.sum
16+++ b/go.sum
17 @@ -2,6 +2,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKY
18 github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
19 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
20 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
21+ github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
22+ github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
23 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
24 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
25 github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
26 diff --git a/pkg/cmd/app.go b/pkg/cmd/app.go
27index fc82477..ed5b5a7 100644
28--- a/pkg/cmd/app.go
29+++ b/pkg/cmd/app.go
30 @@ -8,6 +8,7 @@ import (
31
32 "github.com/urfave/cli/v2"
33 "kevinschoon.com/hierarchy/pkg/config"
34+ "kevinschoon.com/hierarchy/pkg/hierarchy"
35 "kevinschoon.com/hierarchy/pkg/version"
36 )
37
38 @@ -56,12 +57,29 @@ data structure. It uses SQLite to store note content and metadata.
39 },
40 },
41 {
42- Name: "edit",
43- Usage: "create a new note or edit an existing one",
44- Aliases: []string{"e"},
45+ Name: "edit",
46+ Usage: "create a new note or edit an existing one",
47+ ArgsUsage: "path to a note within the hierarchy",
48+ Aliases: []string{"e"},
49+ Flags: []cli.Flag{
50+ &cli.BoolFlag{
51+ Name: "parents",
52+ Aliases: []string{"p"},
53+ Usage: "create parent notes if they don't already exist",
54+ },
55+ &cli.BoolFlag{
56+ Name: "stdin",
57+ Aliases: []string{"r"},
58+ Usage: "read content from standard input",
59+ },
60+ &cli.BoolFlag{
61+ Name: "append",
62+ Aliases: []string{"a"},
63+ Usage: "append content to a note if reading from stdin",
64+ },
65+ },
66 Action: func(ctx *cli.Context) error {
67- fmt.Println(cfg)
68- return nil
69+ return hierarchy.Edit(*cfg, ctx.Args().First())
70 },
71 },
72 {
73 @@ -76,6 +94,9 @@ data structure. It uses SQLite to store note content and metadata.
74 Value: false,
75 },
76 },
77+ Action: func(ctx *cli.Context) error {
78+ return hierarchy.LoadNotes(*cfg)
79+ },
80 },
81 {
82 Name: "remove",
83 @@ -119,7 +140,7 @@ data structure. It uses SQLite to store note content and metadata.
84 },
85 {
86 Name: "serve",
87- Usage: "launch an HTTP server for viewing notes in a web browser",
88+ Usage: "launch an http server for viewing notes in a web browser",
89 Flags: []cli.Flag{
90 &cli.StringFlag{
91 Name: "address",
92 @@ -128,6 +149,7 @@ data structure. It uses SQLite to store note content and metadata.
93 },
94 },
95 },
96+ // internal commands
97 {
98 Name: "gen_markdown",
99 Usage: "generate a markdown description of this tool",
100 diff --git a/pkg/config/config.go b/pkg/config/config.go
101index 6a39dfa..cfea345 100644
102--- a/pkg/config/config.go
103+++ b/pkg/config/config.go
104 @@ -4,10 +4,19 @@ import (
105 "errors"
106 "io/ioutil"
107 "os"
108+ "path"
109
110 "github.com/ghodss/yaml"
111 )
112
113+ func getDefaultDbPath() string {
114+ home, err := os.UserHomeDir()
115+ if err != nil {
116+ panic(err)
117+ }
118+ return path.Join(home, ".local/share/hierarchy/db.sqlite")
119+ }
120+
121 type Config struct {
122 // Path to a SQLite database
123 Database string `json:"database"`
124 @@ -16,7 +25,9 @@ type Config struct {
125 }
126
127 func Default() *Config {
128- return &Config{}
129+ return &Config{
130+ Database: getDefaultDbPath(),
131+ }
132 }
133
134 func Load(path string, cfg *Config) (*Config, error) {
135 diff --git a/pkg/hierarchy/database.go b/pkg/hierarchy/database.go
136new file mode 100644
137index 0000000..1f649dd
138--- /dev/null
139+++ b/pkg/hierarchy/database.go
140 @@ -0,0 +1,41 @@
141+ package hierarchy
142+
143+ import (
144+ "database/sql"
145+ _ "embed"
146+
147+ _ "github.com/mattn/go-sqlite3"
148+ )
149+
150+ //go:embed migrate/init.sql
151+ var sqlMigrateInit string
152+
153+ func initDatabase(path string) (*sql.DB, error) {
154+ db, err := sql.Open("sqlite3", path)
155+ if err != nil {
156+ return nil, err
157+ }
158+ _, err = db.Exec(sqlMigrateInit)
159+ if err != nil {
160+ return nil, err
161+ }
162+ return db, err
163+ }
164+
165+ func With(path string, fn func(*sql.Tx) error) error {
166+ db, err := initDatabase(path)
167+ if err != nil {
168+ return err
169+ }
170+ tx, err := db.Begin()
171+ if err != nil {
172+ return err
173+ }
174+ err = fn(tx)
175+ if err != nil {
176+ tx.Rollback()
177+ return err
178+ }
179+ return tx.Commit()
180+ }
181+
182 diff --git a/pkg/hierarchy/helpers.go b/pkg/hierarchy/helpers.go
183new file mode 100644
184index 0000000..c74d083
185--- /dev/null
186+++ b/pkg/hierarchy/helpers.go
187 @@ -0,0 +1,26 @@
188+ package hierarchy
189+
190+ import (
191+ "path"
192+ "strings"
193+ )
194+
195+ type NotePath [2]string
196+
197+ func (np NotePath) Parent() string {
198+ return np[0]
199+ }
200+
201+ func (np NotePath) Name() string {
202+ return np[1]
203+ }
204+
205+ func ReadPath(input string) NotePath {
206+ base, name := path.Split(input)
207+ if base == "/" {
208+ base = ""
209+ }
210+ base = strings.TrimLeft(base, "/")
211+ base = strings.TrimRight(base, "/")
212+ return [2]string{base, name}
213+ }
214 diff --git a/pkg/hierarchy/hierarchy.go b/pkg/hierarchy/hierarchy.go
215new file mode 100644
216index 0000000..d8db856
217--- /dev/null
218+++ b/pkg/hierarchy/hierarchy.go
219 @@ -0,0 +1,114 @@
220+ package hierarchy
221+
222+ import (
223+ "database/sql"
224+ "encoding/json"
225+ "fmt"
226+ "io/ioutil"
227+ "os"
228+
229+ "kevinschoon.com/hierarchy/pkg/config"
230+ )
231+
232+ type Note struct {
233+ ID int64
234+ Name string
235+ Content string
236+ Parent *Note
237+ Siblings []*Note
238+ }
239+
240+ type dbNote struct {
241+ ID int64
242+ Name string
243+ Content string
244+ Parent int64
245+ }
246+
247+ func Edit(cfg config.Config, notePath string) error {
248+ np := ReadPath(notePath)
249+ fmt.Println(np)
250+ return With(cfg.Database, func(tx *sql.Tx) error {
251+ raw, err := ioutil.ReadAll(os.Stdin)
252+ if err != nil {
253+ return err
254+ }
255+ var parent *int64
256+ if np.Parent() != "" {
257+ parent = new(int64)
258+ row := tx.QueryRow(`
259+ SELECT id FROM notes
260+ WHERE name = ?
261+ `, np.Parent())
262+ err = row.Scan(&parent)
263+ if err != nil {
264+ return err
265+ }
266+ }
267+ _, err = tx.Exec(`
268+ INSERT INTO notes (name, parent, content)
269+ VALUES (?, ?, ?)
270+ `, np.Name(), parent, string(raw))
271+ return err
272+ })
273+ }
274+
275+ func linkNotes(dbNotes []*dbNote, notes []*Note) []*Note {
276+ var remaining []*dbNote
277+ for _, note := range dbNotes {
278+ if note.Parent == 0 {
279+ notes = append(notes, &Note{
280+ ID: note.ID,
281+ Name: note.Name,
282+ Content: note.Content,
283+ Parent: nil,
284+ })
285+ continue
286+ }
287+ loop:
288+ for _, other := range notes {
289+ if other.ID == note.Parent {
290+ other.Siblings = append(other.Siblings, &Note{
291+ ID: note.ID,
292+ Name: note.Name,
293+ Content: note.Content,
294+ Parent: other,
295+ })
296+ continue loop
297+ }
298+ }
299+ remaining = append(remaining, note)
300+ }
301+ if len(remaining) > 0 {
302+ return linkNotes(remaining, notes)
303+ }
304+ return notes
305+ }
306+
307+ func LoadNotes(cfg config.Config) error {
308+ return With(cfg.Database, func(tx *sql.Tx) error {
309+ var notes []*dbNote
310+ rows, err := tx.Query(`
311+ SELECT
312+ id, name, parent, content
313+ FROM notes
314+ `)
315+ if err != nil {
316+ return err
317+ }
318+ for rows.Next() {
319+ parentId := new(int64)
320+ note := &dbNote{}
321+ err = rows.Scan(&note.ID, &note.Name, &parentId, &note.Content)
322+ if err != nil {
323+ return err
324+ }
325+ if parentId != nil {
326+ note.Parent = *parentId
327+ }
328+ notes = append(notes, note)
329+ }
330+ linked := linkNotes(notes, nil)
331+ return json.NewEncoder(os.Stdout).Encode(linked)
332+ })
333+ }
334 diff --git a/pkg/hierarchy/migrate/init.sql b/pkg/hierarchy/migrate/init.sql
335new file mode 100644
336index 0000000..8fc56ae
337--- /dev/null
338+++ b/pkg/hierarchy/migrate/init.sql
339 @@ -0,0 +1,17 @@
340+ PRAGMA foreign_keys = on;
341+
342+ CREATE TABLE IF NOT EXISTS notes (
343+ id INTEGER PRIMARY KEY,
344+ parent INTEGER,
345+ name VARCHAR NOT NULL UNIQUE,
346+ content VARCHAR,
347+ FOREIGN KEY(parent) REFERENCES notes(id)
348+ );
349+
350+ CREATE TABLE IF NOT EXISTS links (
351+ id INTEGER PRIMARY KEY,
352+ source INTEGER,
353+ note INTEGER,
354+ FOREIGN KEY(source) REFERENCES notes(id),
355+ FOREIGN KEY(note) REFERENCES notes(id)
356+ );
357 diff --git a/schema.sql b/schema.sql
358deleted file mode 100644
359index 7cfadb5..0000000
360--- a/schema.sql
361+++ /dev/null
362 @@ -1,6 +0,0 @@
363- CREATE TABLE notes (
364- id INTEGER PRIMARY KEY,
365- parent INTEGER REFERENCES notes(id),
366- name VARCHAR NOT NULL UNIQUE,
367- content VARCHAR
368- );