Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}