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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

v0 source pure

Package toolbox is the directory model behind [r/moul/toolbox](/r/moul/toolbox)(/r/moul/toolbox): a list of small gno...

Readme View source

gno.land/p/moul/toolbox/v0

The directory model behind r/moul/toolbox: Tool, Catalog, BasePath, SlugOf, NewQuery, Index, Card, Nav.

 1import "gno.land/p/moul/toolbox/v0"
 2
 3var c toolbox.Catalog
 4c.Set(toolbox.Tool{
 5    Slug:  "usernames",
 6    Path:  "gno.land/r/moul/toolbox/usernames",
 7    Title: "Usernames",
 8    Desc:  "Resolve a username to an address, or an address to a username.",
 9    Tags:  []string{"identity", "read"},
10})
11
12toolbox.Index(c.List())                       // → the markdown table of the directory
13toolbox.SlugOf("gno.land/r/moul/toolbox",
14               "gno.land/r/moul/toolbox/usernames") // → "usernames"
15toolbox.NewQuery("lookup").Field("q", "username or g1 address").String()

A toolbox is a hub realm listing tools, each tool a realm of its own one level below it. The hub is private = true, which costs it the ability to be imported at all: the type checker refuses any realm importing a private one. So the hub and its tools cannot share a single line of code by importing each other, and everything they must agree about lives here instead.

Nesting is checked on the base path, not the full one. p/moul/nestedpkg decides "is the caller below me" by prefixing one package path with the other. That is exactly right once neither side carries a version, which is the case for a private hub, and it breaks the moment either does: gno.land/r/moul/toolbox/usernames/v0 does not begin with gno.land/r/moul/toolbox/v0. BasePath strips a trailing /vN so the comparison is about the family rather than the version, and IsUnder and SlugOf are built on it: a hub and a tool may each be versioned or not, independently, and all four combinations answer the same. A sibling whose name merely starts with the hub's (toolboxtwo) is not under it, because the comparison is against the base plus a slash.

Five behaviours worth knowing before you use it:

  • Tool is strings all the way down. A tool that registers itself into a public hub hands these values across a realm boundary, and a struct the caller allocated stays the caller's object: persisting it panics with cannot persist object from the private realm. Scalars are copied, so the hub owns what it stores whichever side built the entry.
  • Catalog is a slug-keyed B+ tree (fanout 32), not a sorted slice. The slice is cheaper to read at this size: at n = 100 one cold operation costs 92,571 gas against a fanout-128 tree's 230,754 (gnobench, gno master 1fc4c140e, 2026-09-29). It is still wrong. A slice is one persisted object, so every write rewrites all of it, O(n) per transaction and O(n^2) over n of them, which is free for a compiled-in catalog and is the whole bill for a hub that accepts registration. Removing is worse: shortening a slice in place keeps the peak allocation charged and refunds no deposit. Fanout 32 rather than 128 because a tool can be unregistered and wide nodes lose most on removal. Slug order comes free from the tree rather than from a hand-rolled binary search.
  • A Catalog is a handle. Copying one shares the directory, the way copying a map does, and a write through either copy is seen through both. A copy of a zero value that was never written is a separate empty catalog.
  • Catalog never aliases a Tool.Tags slice, in either direction: it copies on the way in so the caller of Set keeps no handle on stored state, and on the way out so c.List()[0].Tags[0] = "x" cannot reach the directory.
  • NewQuery builds a read, never a write. It emits gnoweb's <gno-form path="...">, whose submission redirects to one of the realm's own Render paths with the fields as query parameters. Nothing is signed and no transaction is built. A form that calls a function is a different element (<gno-form exec="Fn">) and is deliberately not this one with a flag.

Every string is escaped exactly once, with the helper for the slot it lands in, and the escapers are not idempotent: md.Link and md.InlineCode sanitize their own arguments, so Index builds its title link by hand (ui.Cell for link text inside a cell, md.EscapeURL for the href) rather than wrapping twice. Attribute values go through an HTML escaper: gnoweb parses forms with a real tokenizer, so an unescaped quote ends the attribute and whatever follows becomes further attributes of the same tag.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

gno.land/p/moul/toolbox/v0 dependency graph

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.

Overview

Package toolbox is the directory model behind r/moul/toolbox(/r/moul/toolbox): a list of small gno.land utilities, each one a realm of its own, rendered as a single index.

A tool is a Tool: a slug, the realm path that serves it, a title and one line of description. A Catalog holds them sorted by slug and answers the three questions a directory gets asked: what is here, what is this one, and what is tagged X.

Why a package and not just a realm

The hub realm is `private = true`, which buys redeploy-in-place at a stable path and costs the ability to be imported at all: the type checker refuses any realm importing a private one (`ImportPrivateError`). So the hub cannot share a single line of code with its tools by importing them, and they cannot import it either. Everything both sides must agree about (what a tool entry looks like, how the index is rendered, how a query form is spelled) has to live in a `p/`, which is this one.

Nesting is checked on the base path, not on the full one

p/moul/nestedpkg(/p/moul/nestedpkg/v0) decides "is the caller below me" by prefixing one package path with the other. That is exactly right when neither path carries a version, which is the case for a private hub: it is redeployable, so its version could never increment and it does not carry one. It breaks the moment either side does, because `gno.land/r/moul/toolbox/usernames/v0` does not begin with `gno.land/r/moul/toolbox/v0`.

BasePath removes a trailing `/vN` so the comparison is about the family rather than the version, and IsUnder and SlugOf are built on it. A hub and a tool may each be versioned or not, independently, and all four combinations answer the same.

Live: r/moul/toolbox(/r/moul/toolbox).

Constants 1

const Fanout

1const Fanout = 32
source

Fanout is the B+ tree fanout this package uses.

32 rather than 128 because a tool can be unregistered, and wide nodes lose most on removal: a removal shifts every later value in its leaf and rewrites each one. Same choice, and the same reason, as gno.land/p/moul/kit/index(/p/moul/kit/index/v0).

Variables 1

var ErrEmptySlug, ErrBadSlug, ErrEmptyPath, ErrEmptyTitle, ErrUnknownSlug

 1var (
 2	// ErrEmptySlug is returned for a tool with no slug. The slug is the key
 3	// and the URL segment, so there is nothing to list it under.
 4	ErrEmptySlug = errors.New("toolbox: the slug is empty")
 5
 6	// ErrBadSlug is returned for a slug carrying anything but lowercase
 7	// letters, digits and dashes. A slug travels in a URL path and in a
 8	// markdown link, and those are the characters that need no escaping in
 9	// either.
10	ErrBadSlug = errors.New("toolbox: the slug has a character outside [a-z0-9-]")
11
12	// ErrEmptyPath is returned for a tool with no package path. A directory
13	// entry nobody can open is worse than no entry.
14	ErrEmptyPath = errors.New("toolbox: the path is empty")
15
16	// ErrEmptyTitle is returned for a tool with no title.
17	ErrEmptyTitle = errors.New("toolbox: the title is empty")
18
19	// ErrUnknownSlug is returned by [Catalog.Remove] for a slug it does not
20	// hold.
21	ErrUnknownSlug = errors.New("toolbox: no tool with that slug")
22)
source

Errors a Tool or a Catalog returns. A /p/ package returns errors and lets the realm decide whether they are fatal; all of these are a mistake in the realm's own source rather than a value a user chose, so panicking on them is the normal handling.

Functions 9

func BasePath

1func BasePath(pkgPath string) string
source

BasePath returns pkgPath with a trailing version element removed: "gno.land/p/moul/toolbox/v0" becomes "gno.land/p/moul/toolbox".

A path with no version element is returned unchanged, so calling it twice is the same as calling it once.

func Card

1func Card(tool Tool) string
source

Card renders one tool on its own: the title as a link, the description, the package path, and the two ways to reach it.

func Index

1func Index(tools []Tool) string
source

Index renders a catalog as a markdown table: tool, what it does, tags.

Every cell that came from a Tool is escaped exactly once, with the helper for the slot it lands in. A tool description is written by whoever wrote the entry, which in a hub that accepts runtime registration is not necessarily whoever owns the hub, and a pipe or a backtick in one would otherwise rewrite the table around it.

The title link is built by hand rather than with md.Link, which is the one place the obvious call is wrong: md.Link runs its text through sanitize.InlineText itself, so md.Link(ui.Cell(title), url) escapes twice and a title with an asterisk in it renders as a backslash. The slot here is link text INSIDE a table cell, and the helper for that is ui.Cell (InlineText plus the pipe escape InlineText deliberately leaves alone), with the URL escaped once by md.EscapeURL.

func IsUnder

1func IsUnder(hubPath, pkgPath string) bool
source

IsUnder reports whether pkgPath is a tool of the hub at hubPath, ignoring both paths' versions.

The hub itself is not under itself, and neither is a sibling whose name merely starts with the hub's: "toolboxtwo" is not under "toolbox", because the comparison is against the base plus a slash.

func Nav

1func Nav(hubURL, title string) string
source

Nav renders the one line every tool page carries above its content: a link back to the hub, then the tool's own title.

hubURL is a gnoweb path ("/r/moul/toolbox"), not a package path. A tool cannot import the hub to ask, so it states it, and this is the one place the two have to agree.

func SlugOf

1func SlugOf(hubPath, pkgPath string) string
source

SlugOf returns the slug a tool at pkgPath would have under the hub at hubPath, or "" when pkgPath is not under it.

A tool nested deeper than one element has no slug: the directory is one level by design, so "toolbox/a/b" is reported as not a tool rather than silently listed as "a".

func ValidSlug

1func ValidSlug(s string) bool
source

ValidSlug reports whether s is a usable slug: non-empty, and lowercase letters, digits and dashes only.

It is deliberately narrower than what a package path allows. A slug ends up in a URL and in markdown link syntax, and this is the set that survives both untouched, so nothing downstream has to escape it.

func New

1func New() *Catalog
source

New returns an empty catalog. It is the zero value, spelled out for a caller who prefers a constructor.

func NewQuery

1func NewQuery(path string) *Query
source

NewQuery starts a form submitting to the Render path args.

Pass "" to submit to the realm's root page. Do not include a leading colon or the realm path itself: gnoweb supplies both, and a value that looks like a URL is percent-encoded into one path element rather than followed.

Types 3

type Catalog

struct
1type Catalog struct {
2	s *state
3}
source

Catalog is the directory itself: tools keyed by slug, ordered by it.

The zero value is an empty, usable catalog. A Catalog is a HANDLE: copying one shares the directory, and a write through either copy is seen through both, the way copying a map does. A copy of a zero value that was never written is a separate empty catalog.

Why a tree and not a sorted slice

A sorted slice is cheaper to READ at the size a directory actually reaches: at n = 100 one cold operation costs 92,571 gas against a fanout-128 tree's 230,754 (`gnobench`, gno master 1fc4c140e, 2026-09-29). It is still the wrong structure, for two reasons that outlive the gas.

A slice is ONE persisted object, so every write rewrites all of it: O(n) per transaction and O(n^2) over n of them. That costs nothing for a hub whose catalog is compiled in and never written after deploy, and it is the whole bill for one that accepts registration, which is the other half of what this package is for. Removing is worse than writing: shortening a slice in place keeps the peak allocation charged and refunds no deposit at all.

And the ordering comes free. A slug-keyed tree iterates in slug order, which is the order every listing here wants, rather than a hand-rolled binary search that has to be kept correct.

Methods on Catalog

func ByTag

method on Catalog
1func (c *Catalog) ByTag(tag string) []Tool
source

ByTag returns every tool carrying tag, in slug order. An empty tag matches nothing rather than everything: it is almost always an unset field.

func Get

method on Catalog
1func (c *Catalog) Get(slug string) (Tool, bool)
source

Get returns the tool under slug.

func Has

method on Catalog
1func (c *Catalog) Has(slug string) bool
source

Has reports whether slug is in the catalog.

func Len

method on Catalog
1func (c *Catalog) Len() int
source

Len is how many tools the catalog holds.

func List

method on Catalog
1func (c *Catalog) List() []Tool
source

List returns every tool, in slug order.

func Remove

method on Catalog
1func (c *Catalog) Remove(slug string) error
source

Remove deletes the tool under slug. It returns ErrUnknownSlug when there is none, so a realm removing an entry it believes in learns that it is gone.

func Set

method on Catalog
1func (c *Catalog) Set(t Tool) error
source

Set inserts t, replacing any tool already under its slug.

Replacing rather than refusing is what makes a hub's init idempotent: a compiled-in catalog is rebuilt from source on every redeploy, and a second Set of the same slug is a corrected description rather than a collision.

func Tags

method on Catalog
1func (c *Catalog) Tags() []string
source

Tags returns every tag used by any tool, each once, in first-seen order within slug order. Deterministic, which iterating a map would not be.

type Query

struct
1type Query struct {
2	path   string
3	fields []queryField
4}
source

Query builds the gnoweb form a read-only tool needs: a text box whose submission navigates to one of the realm's own Render paths, with the typed values as query parameters.

This is the "template of a query" a toolbox page is for. Rendered by gnoweb,

Example
1NewQuery("lookup").Field("q", "username or g1 address").String()

posts to the realm and redirects to `/r/moul/toolbox/usernames:lookup?q=moul` (`gno.land/pkg/gnoweb/handler_http.go`, `Post`: the `path` attribute travels as the hidden `__gno_path` field and becomes the render args, every other field becomes a query parameter). Nothing is signed and no transaction is built: the result is a `Render` call, which is exactly what `gnokey query vm/qrender` would have done from a terminal.

A form whose fields should CALL a function instead is a different element, `<gno-form exec="Fn">`, and it is not what this type builds. A query form is free and a call is not, so the two are deliberately not one constructor with a flag.

Methods on Query

func Field

method on Query
1func (q *Query) Field(name, placeholder string) *Query
source

Field adds a text input. name is the query parameter the realm will read; placeholder is the grey text in the empty box.

A field with an empty name is ignored: gnoweb rejects the whole form for it, and losing one input beats losing the page.

func Len

method on Query
1func (q *Query) Len() int
source

Len is how many fields the form carries.

func String

method on Query
1func (q *Query) String() string
source

String renders the form as the markdown-embedded HTML gnoweb parses.

A form with no field renders as the empty string rather than as a submit button with nothing to submit.

type Tool

struct
 1type Tool struct {
 2	// Slug is the key, the URL segment and the sort order: "usernames".
 3	Slug string
 4
 5	// Path is the package path serving the tool, with its version if it has
 6	// one: "gno.land/r/moul/toolbox/usernames".
 7	Path string
 8
 9	// Title is the human name: "Usernames".
10	Title string
11
12	// Desc is one line about what the tool does. It is rendered inside a
13	// table cell, so [Index] escapes it rather than trusting it.
14	Desc string
15
16	// Tags group tools in the index. Order is preserved as written.
17	Tags []string
18}
source

Tool is one entry of the directory.

Every field is a string or a slice of strings on purpose. A tool realm that registers itself into a public hub hands these values across a realm boundary, and a struct allocated by the caller would be the caller's object: persisting it panics with `cannot persist object from the private realm` when the caller is private. Scalars are copied, so the hub owns everything it stores whichever side built the entry.

Methods on Tool

func HasTag

method on Tool
1func (t Tool) HasTag(tag string) bool
source

HasTag reports whether t carries tag, compared exactly.

func URL

method on Tool
1func (t Tool) URL() string
source

URL is the gnoweb path of the tool, the package path with the chain domain removed: "/r/moul/toolbox/usernames".

It is derived rather than stored so an entry cannot carry a link that disagrees with its own path.

func Validate

method on Tool
1func (t Tool) Validate() error
source

Validate reports the first thing wrong with t, or nil.

Dependencies

Source Files 10