Something went wrong. Try again.
Educational demo of a single node, sqlite, Datastar, CQRS app with low latency UX.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275package main
import ( "context" "net/http" "strings")
// ---- Unified command registry ----//// One declaration per command is the single source of truth for three surfaces:// 1. global keyboard dispatch (POST /cmd/key resolves a combo → Command.Run)// 2. the ⌘K command palette (palette rows show each command's key hint)// 3. the ? shortcut cheatsheet (buildHelpView iterates this same list)//// Keyboard bindings therefore live next to the action they trigger, so the help// dialog and palette hints can never drift from what the keys actually do.
// cmdCtx is the state a command needs to decide availability and behaviour. It is// a plain snapshot (no DB) so resolveKey/availableCommands stay cheap to call on// every keypress and every projection.type cmdCtx struct { st SessionState vw viewer}
// Command is a named, keyboard-bound action. Dynamic, parameterised actions// (filter by a specific label, select issue N) are NOT registry entries — they// stay plain data-cmd strings emitted by the templates/palette.type Command struct { ID string // stable id, e.g. "issue.new" Title string // shown in palette + help Group string // "General" | "Create" | "Filter" | "Navigate" Keys []string // normalized combos: ["c"], ["mod+k"], ["j","arrowdown"] Scroll bool // result-set change → scroll list to top (like cmdScrollTop) Avail func(cmdCtx) bool // nil = always available; gates key dispatch AND help visibility Run func(*app, *Session, *http.Request) map[string]any // mutate state; optional signals to patch in the response Script string // optional JS run via sse.ExecuteScript after Run (focus, etc.)}
// mut adapts a plain mutation (the existing doX shape) into a Command.Run that// patches no signals. Method expressions like (*app).doCompose satisfy the inner// type exactly.func mut(fn func(*app, *Session, *http.Request)) func(*app, *Session, *http.Request) map[string]any { return func(a *app, s *Session, r *http.Request) map[string]any { fn(a, s, r); return nil }}
// focusSearchJS / focusCommentJS target elements that already exist in the DOM// when the shortcut fires (the top search box is always present; the comment box// is present whenever an issue is selected), so an ExecuteScript in the command's// own response lands cleanly. The palette input focuses itself via data-init on// morph-in instead, since it doesn't exist yet when ⌘K is pressed.const ( focusSearchJS = "document.querySelector('.search input')?.focus()" focusCommentJS = "document.querySelector('#detail .comment-form textarea')?.focus()")
// commands is the ordered registry. Order is precedence: resolveKey returns the// first command whose Keys contains the combo and whose Avail passes — which is// how Escape resolves to the right "close" depending on what's open.var commands = []Command{ { ID: "palette.open", Title: "Command palette", Group: "General", Keys: []string{"mod+k"}, Run: func(a *app, s *Session, r *http.Request) map[string]any { opened := false s.withState(func(st *SessionState) { st.PaletteOpen = !st.PaletteOpen st.HelpOpen = false if st.PaletteOpen { st.PaletteQuery = "" st.PaletteIndex = 0 opened = true } }) if opened { // Clear the client-side input text so a reopened palette starts empty. return map[string]any{"palette": ""} } return nil }, }, { ID: "help.open", Title: "Keyboard shortcuts", Group: "General", Keys: []string{"?"}, Run: mut(func(a *app, s *Session, r *http.Request) { s.withState(func(st *SessionState) { st.HelpOpen = true; st.PaletteOpen = false }) }), }, { ID: "overlay.close", Title: "Close / dismiss", Group: "General", Keys: []string{"escape"}, Run: mut(func(a *app, s *Session, r *http.Request) { // Layered close: dismiss the topmost open surface only. s.withState(func(st *SessionState) { switch { case st.PaletteOpen: st.PaletteOpen = false st.PaletteIndex = 0 case st.HelpOpen: st.HelpOpen = false case st.Composing: st.Composing = false case st.Editing: st.Editing = false case st.Selected != 0: st.Selected = 0 } }) }), }, { ID: "search.focus", Title: "Search issues", Group: "General", Keys: []string{"/"}, Script: focusSearchJS, }, { ID: "issue.new", Title: "New issue", Group: "Create", Keys: []string{"c"}, Avail: func(c cmdCtx) bool { return c.vw.CanWrite }, Run: mut((*app).doCompose), }, // --- Navigation. These are handled ENTIRELY on the client (j/k move the list // cursor $cur or the detail-ring cursor $dcur; l/Enter load/activate; h goes // back — see index.html). They carry no Run: the shortcut handler skips these // keys, so they never reach /cmd/key. They live in the registry only so the ? // cheatsheet documents them from the same source of truth. --- {ID: "nav.move", Title: "Move cursor (list / comments)", Group: "Navigate", Keys: []string{"j", "k"}}, {ID: "nav.peek", Title: "Peek issue (stay in list); again to enter it", Group: "Navigate", Keys: []string{"l"}}, {ID: "nav.enter", Title: "Open issue, jump to comments / activate item", Group: "Navigate", Keys: []string{"enter"}}, {ID: "nav.back", Title: "Back / close", Group: "Navigate", Keys: []string{"h"}}, { // Copy the OPEN issue's permalink (the one in the detail, not the list // cursor). Handled client-side in the #app keydown handler — a clipboard // write must stay in the user gesture, so a server round-trip would be // blocked. Listed here (no Run) only so the ? cheatsheet documents it. ID: "link.copy", Title: "Copy issue link", Group: "Create", Keys: []string{"y"}, Avail: func(c cmdCtx) bool { return c.st.Selected > 0 }, }, { ID: "comment.focus", Title: "Comment on issue", Group: "Create", Keys: []string{"r"}, Avail: func(c cmdCtx) bool { return c.st.Selected > 0 && c.vw.CanWrite }, Script: focusCommentJS, }, { ID: "issue.edit", Title: "Edit issue", Group: "Create", Keys: []string{"e"}, Avail: func(c cmdCtx) bool { return c.st.Selected > 0 && c.vw.CanWrite }, Run: func(a *app, s *Session, r *http.Request) map[string]any { return a.startEdit(r.Context(), s, a.viewerFor(r)) }, }, { ID: "status.cycle", Title: "Cycle status (open → closed → all)", Group: "Filter", Keys: []string{"x"}, Scroll: true, Run: mut((*app).doStatusCycle), }, { ID: "sort.dir", Title: "Toggle sort direction", Group: "Filter", Keys: []string{"s"}, Avail: func(c cmdCtx) bool { return c.st.SortField != "relevance" }, Scroll: true, Run: mut((*app).doSortDir), }, { ID: "author.me", Title: "My issues", Group: "Filter", Keys: []string{"m"}, Avail: func(c cmdCtx) bool { return c.vw.CanWrite }, Scroll: true, Run: mut((*app).doAuthorMe), },}
// resolveKey returns the command bound to combo that is available in ctx, or nil.// First match wins (registry order = precedence).func resolveKey(combo string, ctx cmdCtx) *Command { if combo == "" { return nil } for i := range commands { c := &commands[i] for _, k := range c.Keys { if k == combo { if c.Avail == nil || c.Avail(ctx) { return c } } } } return nil}
// chordLabel renders a normalized combo for display: "mod+k" → "⌘ K".func chordLabel(combo string) string { parts := strings.Split(combo, "+") for i, p := range parts { switch p { case "mod": parts[i] = "⌘" case "shift": parts[i] = "⇧" case "alt": parts[i] = "⌥" case "escape": parts[i] = "Esc" case "arrowup": parts[i] = "↑" case "arrowdown": parts[i] = "↓" case "arrowleft": parts[i] = "←" case "arrowright": parts[i] = "→" case "enter": parts[i] = "↵" default: parts[i] = strings.ToUpper(p) } } return strings.Join(parts, " ")}
// chordFor returns the display chord for the command with the given id (first// binding), or "" — lets the palette show a key hint next to actions that also// have a shortcut.func chordFor(id string) string { for i := range commands { if commands[i].ID == id && len(commands[i].Keys) > 0 { return chordLabel(commands[i].Keys[0]) } } return ""}
// ---- mutations introduced by the command system ----
// doStatusCycle advances the status filter open → closed → all → open.func (a *app) doStatusCycle(s *Session, r *http.Request) { s.withState(func(st *SessionState) { switch st.Status { case "open": st.Status = "closed" case "closed": st.Status = "all" default: st.Status = "open" } st.Limit = defaultPageSize })}
// startEdit opens the edit form for the selected issue and returns the edit.*// signal payload to pre-fill it (nil if there's nothing to edit or the viewer// doesn't own the issue). Shared by handleEditStart (the button) and the `e`// shortcut so both go through one ownership-checked path.func (a *app) startEdit(ctx context.Context, sess *Session, vw viewer) map[string]any { state := sess.snapshot() if state.Selected == 0 || !vw.CanWrite || vw.Login == "" { return nil } d, err := a.reads.GetIssue(ctx, state.Selected) if err != nil || d == nil { return nil } if d.Creator != vw.Login { // only the creator may edit (mirrors detail CanEdit) return nil } status := "closed" if d.Open { status = "open" } labels := make([]string, 0, len(d.Labels)) for _, l := range d.Labels { labels = append(labels, l.Name) } sess.withState(func(st *SessionState) { st.Editing = true }) return map[string]any{ "edit": map[string]any{ "title": d.Title, "desc": d.Description, "status": status, "labels": labels, }, }}