Search Apps Documentation Source Content File Folder Download Copy Actions Download

todos.gno

3.50 Kb · 150 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/nt/avl/v0"
 13)
 14
 15// item is one entry on the board. `address` is the uverse bech32 type — it is
 16// persistable (unlike a `realm` value), so it is safe to store across txs.
 17type item struct {
 18	id     int
 19	text   string
 20	done   bool
 21	author address
 22}
 23
 24var (
 25	// items is keyed by zero-padded id string so avl iteration is ordered by id.
 26	items  avl.Tree
 27	nextID int
 28)
 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	nextID++
 46	id := nextID
 47	items.Set(idKey(id), &item{
 48		id:     id,
 49		text:   text,
 50		done:   false,
 51		author: author,
 52	})
 53	return id
 54}
 55
 56// Toggle flips the done state of the item with the given id.
 57func Toggle(cur realm, id int) {
 58	if !cur.IsCurrent() {
 59		panic("spoofed realm")
 60	}
 61	v, ok := items.Get(idKey(id))
 62	if !ok {
 63		panic("todos: item not found: #" + strconv.Itoa(id))
 64	}
 65	it := v.(*item)
 66	it.done = !it.done
 67}
 68
 69// Remove deletes the item with the given id.
 70func Remove(cur realm, id int) {
 71	if !cur.IsCurrent() {
 72		panic("spoofed realm")
 73	}
 74	if _, removed := items.Remove(idKey(id)); !removed {
 75		panic("todos: item not found: #" + strconv.Itoa(id))
 76	}
 77}
 78
 79// idKey renders an id as a fixed-width, zero-padded key so avl.Tree's
 80// byte-lexicographic ordering matches numeric id ordering.
 81func idKey(id int) string {
 82	s := strconv.Itoa(id)
 83	const width = 12
 84	if len(s) >= width {
 85		return s
 86	}
 87	return strings.Repeat("0", width-len(s)) + s
 88}
 89
 90// shortAddr renders an address as "g1abc…" (first 6 chars + ellipsis).
 91func shortAddr(a address) string {
 92	s := a.String()
 93	if len(s) <= 6 {
 94		return s
 95	}
 96	return s[:6] + "…"
 97}
 98
 99// counts returns the number of open and done items.
100func counts() (open, done int) {
101	items.Iterate("", "", func(_ string, v interface{}) bool {
102		if v.(*item).done {
103			done++
104		} else {
105			open++
106		}
107		return false
108	})
109	return open, done
110}
111
112// Render returns the board as Markdown. Newest item (highest id) first.
113//
114// Render is NOT a crossing function (no `cur realm` param) — it is read-only
115// and invoked by gnoweb, not via MsgCall.
116func Render(path string) string {
117	open, done := counts()
118
119	var b strings.Builder
120	b.WriteString("# Shared To-Do Board\n\n")
121	b.WriteString(strconv.Itoa(open) + " open · ")
122	b.WriteString(strconv.Itoa(done) + " done · ")
123	b.WriteString(strconv.Itoa(open+done) + " total\n\n")
124
125	if open+done == 0 {
126		b.WriteString("_No items yet. Add one with `Add(text)`._\n")
127		return b.String()
128	}
129
130	// Collect in ascending id order, then emit newest first.
131	var lines []string
132	items.Iterate("", "", func(_ string, v interface{}) bool {
133		it := v.(*item)
134		mark := " "
135		if it.done {
136			mark = "x"
137		}
138		line := "- [" + mark + "] #" + strconv.Itoa(it.id) + " " +
139			it.text + " (" + shortAddr(it.author) + ")"
140		lines = append(lines, line)
141		return false
142	})
143
144	for i := len(lines) - 1; i >= 0; i-- {
145		b.WriteString(lines[i])
146		b.WriteString("\n")
147	}
148
149	return b.String()
150}