todos.gno
2.96 Kb · 114 lines
1// Package todos is a shared, on-chain to-do board realm.
2//
3// Anyone can add an item; the caller's address is recorded as the author.
4// Items can be toggled (open/done) or removed. Render exposes the board as
5// Markdown checkboxes with open/done counts, newest item first.
6package todos
7
8import (
9 "strconv"
10 "strings"
11
12 "gno.land/p/moul/kit/ui/v0"
13 "gno.land/p/moul/kit/store/v0"
14)
15
16// item is one entry on the board. `address` is the uverse bech32 type — it is
17// persistable (unlike a `realm` value), so it is safe to store across txs.
18// It carries no id field: the id belongs to the store, which hands it back on
19// lookup and iteration.
20type item struct {
21 text string
22 done bool
23 author address
24}
25
26// items assigns the item ids. v1 kept its own nextID plus an idKey() that
27// zero-padded to width 12, which stopped ordering Render past 10^12.
28var items = store.Named("todos: item")
29
30// Add appends a new item authored by the caller and returns its id.
31//
32// Crossing function: takes `cur realm` first (gno 0.9 interrealm convention),
33// authenticates with cur.IsCurrent() before trusting cur.Previous().
34func Add(cur realm, text string) int {
35 if !cur.IsCurrent() {
36 panic("spoofed realm")
37 }
38 text = strings.TrimSpace(text)
39 if text == "" {
40 panic("todos: text must not be empty")
41 }
42
43 author := cur.Previous().Address()
44
45 return int(items.Add(&item{
46 text: text,
47 done: false,
48 author: author,
49 }))
50}
51
52// Toggle flips the done state of the item with the given id.
53func Toggle(cur realm, id int) {
54 if !cur.IsCurrent() {
55 panic("spoofed realm")
56 }
57 it := items.MustGet(store.ID(id)).(*item)
58 it.done = !it.done
59}
60
61// Remove deletes the item with the given id.
62func Remove(cur realm, id int) {
63 if !cur.IsCurrent() {
64 panic("spoofed realm")
65 }
66 if _, removed := items.Remove(store.ID(id)); !removed {
67 panic("todos: item #" + strconv.Itoa(id) + " not found")
68 }
69}
70
71// counts returns the number of open and done items.
72func counts() (open, done int) {
73 items.Each(func(_ store.ID, v any) {
74 if v.(*item).done {
75 done++
76 } else {
77 open++
78 }
79 })
80 return open, done
81}
82
83// Render returns the board as Markdown. Newest item (highest id) first.
84//
85// Render is NOT a crossing function (no `cur realm` param) — it is read-only
86// and invoked by gnoweb, not via MsgCall.
87func Render(path string) string {
88 open, done := counts()
89
90 var b strings.Builder
91 b.WriteString("# Shared To-Do Board\n\n")
92 b.WriteString(strconv.Itoa(open) + " open · ")
93 b.WriteString(strconv.Itoa(done) + " done · ")
94 b.WriteString(strconv.Itoa(open+done) + " total\n\n")
95
96 if open+done == 0 {
97 b.WriteString("_No items yet. Add one with `Add(text)`._\n")
98 return b.String()
99 }
100
101 // The store iterates by id, so newest-first is one call rather than a
102 // collect-then-walk-backwards.
103 items.EachReverse(func(id store.ID, v any) {
104 it := v.(*item)
105 mark := " "
106 if it.done {
107 mark = "x"
108 }
109 b.WriteString("- [" + mark + "] #" + id.String() + " " +
110 it.text + " (" + ui.Addr(it.author) + ")\n")
111 })
112
113 return b.String()
114}