Skip to content

Tool Module

Tools live in tools/<name>/ and mount at /tools/{key}. The framework handles routing, admin config UI, tags, and visibility — the module only needs a Register func.

Tool DetailExample tool — Convert Text. Left panel lists modes, right panel handles input/output.

Tool SettingsTool settings — runtime config values editable without redeploying.

File Structure

tools/my-tool/
├── handler.go    # Register func + HTTP handler funcs
├── service.go    # business logic (pure Go)
├── repo.go       # external I/O — DB, HTTP, S3 (stub if not needed)
├── view.templ    # templ HTML template
├── config.go     # typed Config struct (if tool has runtime knobs)
├── static.go     # //go:embed declaration (if JS assets)
└── js/
    └── mytool.js # tool-scoped JS, no CDN

Register in main.go

go
app.RegisterTool(
    tool.Tool{
        Key:               "my-tool",
        Name:              "My Tool",
        Description:       "What this tool does.",
        Icon:              "🔧",
        Category:          "Text",
        DefaultVisibility: entity.VisibilityPublic,
        DefaultTags:       []tool.DefaultTag{tags.Text},
    },
    mytool.Config{InitText: "hello"},
    mytool.Register,
)

One call = one card on the home grid. Call again with a different Key (and optionally a different Config) to get a second card backed by the same Register func.

For tools with no runtime config:

go
app.RegisterToolNoConfig(
    tool.Tool{Key: "dashboard", Name: "Dashboard", Icon: "📊", ExternalURL: "https://grafana.example.com"},
    external.Register,
)

tool.Tool fields

FieldDescription
KeyUnique slug, kebab-case. Drives the mount path /tools/{Key}
NameDisplay name shown on the card and page title
DescriptionCard subtitle
IconEmoji or short string shown on the card
CategoryGroups cards visually on the home grid
DefaultVisibilityentity.VisibilityPublic or entity.VisibilityPrivate
DefaultTagsSlice of tool.DefaultTag from tags/defaults.go
ExternalURLIf set, card opens this URL in a new tab

Register Function

go
package mytool

import "github.com/yogasw/wick/pkg/tool"

func Register(r tool.Router) {
    r.GET("/", index)
    r.POST("/", submit)
    r.Static("/static/", StaticFS) // only if you have JS assets
}

All paths are relative to /tools/{key} — never hardcode the full path.

Handlers

Handlers are plain top-level funcs that receive *tool.Ctx:

go
func index(c *tool.Ctx) {
    seed := c.Cfg("init_text")
    c.HTML(IndexBody(c.Meta().Name, c.Base(), seed))
}

func submit(c *tool.Ctx) {
    input := c.Form("input")
    c.HTML(IndexBody(c.Meta().Name, c.Base(), process(input)))
}

Ctx helpers

HelperDescription
c.Base()Absolute base path /tools/{key}
c.Meta()The registered tool.Tool (Key, Name, Icon, …)
c.Cfg(key)Read runtime config value for this instance
c.CfgInt(key)Config value as int
c.CfgBool(key)Config value as bool
c.Missing()required config keys not yet set
c.Form(key)Form field value
c.Query(key)Query string value
c.BindJSON(&v)Decode JSON body
c.HTML(body)Write HTML response
c.JSON(status, v)Write JSON response
c.Redirect(url, code)Redirect

Runtime Config

Declare a Config struct in config.go:

go
package mytool

type Config struct {
    InitText string `wick:"desc=Seed text on first load."`
    APIKey   string `wick:"desc=External API key.;secret;required"`
    MaxItems int    `wick:"desc=Max results.;number"`
    Mode     string `wick:"desc=Processing mode.;dropdown=fast|accurate|balanced"`
}

The framework reflects the struct into configs table rows at boot. Admin edits are live on the next request — no redeploy.

wick tag reference

TagWidgetNotes
desc=...Description shown in admin
requiredc.Missing() returns this key until set
secretPasswordMasked in UI
textareaTextareaMulti-line
dropdown=a|b|cSelectPipe-separated options
checkboxCheckboxAuto-applied for bool fields
numberNumberAuto-applied for int/float fields
email, url, color, date, datetimeTyped inputHTML input type
key=custom_nameOverride auto snake_case key

Field names are auto snake-cased: InitTextinit_text.

JavaScript Assets

go
// static.go
package mytool

import "embed"

//go:embed js
var StaticFS embed.FS

Mount and reference in handler + templ:

go
r.Static("/static/", StaticFS)
html
<script src={ base + "/static/js/mytool.js" }></script>

WARNING

//go:embed js fails if js/ doesn't exist. Create the directory with at least one file before running go build.

Tags

Add shared tags in tags/defaults.go:

go
var MyGroup = tool.DefaultTag{
    Name:        "MyGroup",
    Description: "Tools for X.",
    IsGroup:     true,
    SortOrder:   20,
}

Reference in main.go:

go
DefaultTags: []tool.DefaultTag{tags.MyGroup},

TIP

Check if an existing tag fits before adding a new one — fewer tags keeps the home grid clean.

Built with ❤️ by a developer, for developers.