import type { KeyEvent } from "../../tui/input/types.js"; import { type GraderDraft, type QuestionSpec } from "./graders.js"; export type FieldName = "id" | "description" | "input" | "files" | "goal"; export type FieldSpec = { name: FieldName; label: string; required: boolean; /** Ctrl+E opens `$EDITOR` on this field when one is set. */ long: boolean; /** Shown in place of the value while the field is empty. */ help: string; }; /** The one place a field is defined. The reducer, the view and the * validator all read this table. */ export declare const FIELDS: FieldSpec[]; /** Where the cursor can be: a field, or one question of one grader. */ export type Row = { kind: "field"; name: FieldName; } | { kind: "grader"; index: number; question: string; }; export declare function sameRow(a: Row, b: Row): boolean; export type Problem = { row: Row; text: string; }; export type FormMode = { kind: "form"; } | { kind: "confirmQuit"; } | { kind: "confirmSave"; } | { kind: "confirmRemove"; index: number; }; /** What `eval edit` loaded: the folder, the parsed `test.json` the fields * are written back over, and the values as loaded for the dirty check. */ export type Editing = { testDir: string; base: Record; loaded: Loaded; /** Graded by harness pairs under `files/`, so no goal is needed. */ harnessGraded: boolean; }; /** The field values as loaded and the graders as they would be written. */ export type Loaded = { values: Record; graders: string; }; export type FormState = { suiteDir: string; values: Record; graders: GraderDraft[]; /** Set while editing an existing test; null when creating one. */ editing: Editing | null; /** A locked field and the text shown in its place. */ locks: Partial>; /** When set, the graders section shows this text and has no rows. */ gradersLock: string | null; cursor: Row; mode: FormMode; /** The footer message: the first problem, or what happened to the editor. */ message: string; /** The first body line on screen; the command keeps the cursor in view. */ scrollTop: number; done: boolean; /** The test folder, once it is written. The command prints it after the * screen closes. */ written: string | null; }; export type FormAction = { kind: "moveUp"; } | { kind: "moveDown"; } | { kind: "appendText"; text: string; } | { kind: "backspace"; } | { kind: "backspaceWord"; } /** An intent: the command opens the editor and reports `editorReturned`. */ | { kind: "editInEditor"; } | { kind: "editorReturned"; text: string | null; } | { kind: "editorFailed"; text: string; } | { kind: "requestQuit"; } | { kind: "confirmQuit"; } | { kind: "cancelQuit"; } /** An intent: the command validates, then reports `saveFailed` or `askSave`. */ | { kind: "requestSave"; } | { kind: "askSave"; } | { kind: "cancelSave"; } /** An intent: the command writes, then reports one of the next two. */ | { kind: "save"; } | { kind: "saveFailed"; problems: Problem[]; } | { kind: "saved"; testDir: string; } | { kind: "addGrader"; } | { kind: "requestRemove"; } | { kind: "confirmRemove"; } | { kind: "cancelRemove"; } | { kind: "pickNext"; } | { kind: "pickPrevious"; } /** Enter on the last row while its grader has no kind picked yet. */ | { kind: "kindNeeded"; }; export declare const EDITOR_EXITED_MESSAGE = "editor exited without saving; the field is unchanged"; export declare function initialForm(suiteDir: string): FormState; /** A form over an existing test: the folder's parent is the suite, the * cursor starts on the first unlocked row, and what was loaded is kept * so the form knows when it is clean. */ export declare function editingForm(args: { testDir: string; base: Record; values: Record; graders: GraderDraft[]; locks: FormState["locks"]; gradersLock: string | null; harnessGraded: boolean; }): FormState; /** Whether anything differs from the start: from empty when creating a * test, from what was loaded when editing one. */ export declare function isDirty(state: FormState): boolean; /** A field in `locks`, or any grader row while the section is locked. */ export declare function isLocked(state: FormState, row: Row): boolean; /** Every row the cursor can visit, in order: the unlocked fields, then * each grader's questions. */ export declare function rowsOf(state: FormState): Row[]; /** The question a grader row names; null for a field row. */ export declare function questionAt(state: FormState, row: Row): QuestionSpec | null; /** The field's value, or the grader's answer (a choice reads its default). */ export declare function valueAt(state: FormState, row: Row): string; /** Whether the cursor is on the last row, where Enter saves instead of * moving on. */ export declare function isLastRow(state: FormState, row: Row): boolean; export declare function isChoiceRow(state: FormState, row: Row): boolean; export declare function isKindRow(row: Row): boolean; /** A kind row whose grader has no kind picked yet: Enter there must not * save, since the user has not said what the grader checks. */ export declare function isUnpickedKindRow(state: FormState, row: Row): boolean; /** A long field, or a long text question: Ctrl+E opens `$EDITOR` on it. */ export declare function isLong(state: FormState, row: Row): boolean; export declare function fieldSpec(name: FieldName): FieldSpec; export declare function reduceForm(state: FormState, action: FormAction): FormState; /** What a typed key does to the row under the cursor; null for a key that * types nothing. Enter and Escape type nothing either: Enter belongs to * the mode's key table, which the caller consults first, and a bare * Escape is what the terminal sends for an Option chord it does not know, * so it is dropped rather than quitting the form. */ export declare function editActionFor(event: KeyEvent): FormAction | null;