Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121import 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<string | undefined>;
/** Shows the import picker (D20) and opens whatever it resolves to. A no-op if the person cancels. */export type ImportRepo = () => Promise<void>;
/** A shell command that needs no workbench, so a page with no shell can run it too. */export type PlatformCommand = Omit<ShellCommand, "run"> & { 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<PromptRequest>), then: (value: string) => Promise<void>) => () => 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<PlatformCommand, "id" | "run">[]): void { const byId = new Map(table.map((c) => [c.id, c])); document.addEventListener("nib-command", (event) => byId.get(event.command)?.run());}