import type { ShellCommand } from "../shell/commands.ts"; import type { Did, Platform } from "./state.ts"; export interface PromptRequest { readonly title: string; readonly label: string; /** The submit button's text. */ readonly submit: string; readonly placeholder?: string; readonly autocomplete?: AutoFill; /** Offers a fixed set of answers instead of a text field. */ readonly choices?: readonly { value: string; label: string }[]; } const signInRequest: PromptRequest = { title: "Sign in with atproto", label: "Handle", submit: "Sign in", placeholder: "alice.bsky.social", autocomplete: "username" }; /** Asks the person for one value in trusted chrome (D10). Resolves undefined when they cancel. */ export type Ask = (request: PromptRequest) => Promise; /** Shows the import picker (D20) and opens whatever it resolves to. A no-op if the person cancels. */ export type ImportRepo = () => Promise; /** A shell command that needs no workbench, so a page with no shell can run it too. */ export type PlatformCommand = Omit & { readonly run: () => void }; /** * The platform's named commands. They close over the platform and a way to ask for input, so the * X. None of them reads * the workbench: their state is the platform's. */ export function platformCommands(platform: Platform, ask: Ask, importRepo: ImportRepo): PlatformCommand[] { const asking = (request: PromptRequest | (() => Promise), then: (value: string) => Promise) => () => void (async () => { const value = (await ask(typeof request === "function" ? await request() : request))?.trim(); if (value) await then(value); })(); const newProject = asking({ title: "New project", label: "Name", submit: "Create", placeholder: "my project" }, (name) => platform.newProject(name)); return [ { id: "account.signIn", title: "Sign In", run: asking(signInRequest, (handle) => platform.signIn(handle)), }, { id: "account.switch", title: "Switch Account", run: asking( () => { const current = platform.state.account?.session.did; const choices = platform.state.accounts.filter((a) => a.did !== current).map((a) => ({ value: a.did, label: a.handle ?? a.did })); if (choices.length === 0) return Promise.resolve(signInRequest); return Promise.resolve({ title: "Switch account", label: "Account", submit: "Switch", choices }); }, (value) => (value.startsWith("did:") ? platform.switchAccount(value as Did) : platform.signIn(value)), ), }, { id: "account.signOut", title: "Sign Out", run: () => void platform.signOut() }, { id: "project.new", title: "New Project", run: newProject, }, { id: "project.open", title: "Open Project", keys: "Mod+O", run: asking( async () => { const projects = await platform.projects().catch(() => []); if (projects.length === 0) return { title: "Open project", label: "Project AT-URI", submit: "Open", placeholder: "at://…/dev.nibling.project/…" }; const choices = projects.map((p) => ({ value: p.uri, label: p.record.name })); return { title: "Open project", label: "Project", submit: "Open", choices }; }, (uri) => platform.openProject(uri), ), }, { id: "project.save", title: "Save Project", keys: "Mod+S", // Unsaved files have no project yet: the first save names one and creates it from them. run: () => (platform.state.project ? void platform.saveProject() : newProject()), }, { id: "project.fork", title: "Fork Project", run: () => void platform.forkProject() }, { id: "tangled.import", title: "Import from Tangled", run: () => void importRepo(), }, { id: "tangled.push", title: "Push to Tangled", run: asking({ title: "Push to tangled", label: "Commit message", submit: "Push" }, (message) => platform.pushToTangled(message)), }, ]; } /** * Asks the app to run a platform command by id. Pages dispatch it instead of holding the command table, * and the app entry runs it with the same entry the header and keybindings use. */ export class CommandRequestEvent extends Event { constructor(readonly command: string) { super("nib-command", { bubbles: true, composed: true }); } } declare global { interface DocumentEventMap { "nib-command": CommandRequestEvent; } } /** Runs the commands pages ask for by id, from buttons such as an empty state's "New project". */ export function runRequestedCommands(table: readonly Pick[]): void { const byId = new Map(table.map((c) => [c.id, c])); document.addEventListener("nib-command", (event) => byId.get(event.command)?.run()); }