Commit
+241 -13 +/-9 browse
1 | diff --git a/go.mod b/go.mod |
2 | index 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 |
14 | index 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 |
27 | index 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 |
101 | index 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 |
136 | new file mode 100644 |
137 | index 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 |
183 | new file mode 100644 |
184 | index 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 |
215 | new file mode 100644 |
216 | index 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(¬e.ID, ¬e.Name, &parentId, ¬e.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 |
335 | new file mode 100644 |
336 | index 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 |
358 | deleted file mode 100644 |
359 | index 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 | - ); |