This is the abridged developer documentation for qrate # qrate docs qrate is a desktop app for collection catalogs. It edits a project in a spreadsheet grid, checks the data against validators, and exports it to the formats other systems expect. ## Start here [Section titled “Start here”](#start-here) * [Projects](/docs/projects) — create, import, and open a `.qrate` project * [The grid](/docs/grid) — edit cells, search, filter, and undo * [Files and photos](/docs/files-and-photos) — link records to files, and view them * [Diagnostics](/docs/diagnostics) — the Problems panel, spelling, and fixes * [Columns](/docs/columns) — column types, authority lists, and per-column settings * [Export and Google Sheets](/docs/export-and-sync) — CSV, JSON-LD, CSL-JSON, ZIP, and Sheets sync * [The Agent panel](/docs/agent-panel) — how a local AI agent reads a project ## Plugins [Section titled “Plugins”](#plugins) * [Plugins](/docs/plugins) — find, install, and manage plugins * [Develop plugins](/docs/plugins/developing) — create, test, and publish a plugin * [Islandora plugin](/docs/plugins/islandora) — use Islandora vocabularies in qrate # The Agent panel > how a local AI agent reads a project An external AI agent that you run yourself can read the project open in qrate. qrate allows this by default. To stop it, open **Settings ▸ Agent** and switch off **Allow agents to read this app**. The port closes immediately, with no relaunch needed. See [`AGENTS.md`](https://github.com/devnull03/qrate/blob/main/AGENTS.md) for the protocol. qrate listens on your own machine only, behind a token that changes at every launch. A program that could reach this connection could already read your `.qrate` file directly, so the bridge does not widen what a local program can see. It does show unsaved edits, which the file does not. The **Agent** panel, in the right dock, has two tabs. **Terminal** runs qrate’s bundled Pi agent. **Log** lists everything that happened on the bridge. An agent cannot change a cell through the bridge. It can only read data and stage findings that you accept or ignore. ## Start Pi [Section titled “Start Pi”](#start-pi) Open a project, then open **Agent ▸ Terminal**. qrate resumes the Pi session for that project. Use **New** for a clean session, **Stop** to end the process, or **Restart** to resume it. The first time, type `/login openrouter` and follow Pi’s sign-in flow. Your credential is stored by Pi in qrate’s private Pi profile; qrate does not read or store it. Pi starts with OpenRouter and the `openrouter/free` router. The terminal runs only Pi, not a general shell. Pi can use its ordinary coding tools when you explicitly ask for coding work. It asks before every shell command, write, or edit, and before reading outside the open project’s directory. qrate’s metadata tools remain read-only and can only stage proposed findings. ## How to read an entry [Section titled “How to read an entry”](#how-to-read-an-entry) An entry has up to six parts: | Part | What it tells you | | ------------- | ------------------------------------------------------------------------------------- | | `+2:07` | Time since the first entry of this session, in minutes and seconds. Not a clock time. | | `claude-code` | The name the agent gave itself. See [Names are not proof](#names-are-not-proof). | | `rows` | The method the agent called, or `connected` / `disconnected`. | | `3 row(s)` | What the agent asked for. Absent for a method that takes no parameters. | | `3 rows` | What qrate answered, or why it refused. | | `4ms` | How long qrate took to answer. | ## The three kinds of entry [Section titled “The three kinds of entry”](#the-three-kinds-of-entry) **An answered call** shows its result in grey. The result is a size, never your data: `1893 rows × 32 columns`, `3 rows`, `12 diagnostics`. qrate never puts cell contents in this list. **A refused call** shows its reason in red. Read these first. Common reasons: | Reason | What happened | | ------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | `forbidden` | The caller sent a wrong token or no token. qrate makes a new token at each launch. | | `malformed_request` | The caller sent a method or a parameter the protocol does not have. | | `project_unavailable` | No project is open. | | `too_many_rows`, `invalid_search_limit`, `too_many_findings` | The caller asked for more than one call permits. | **A connect or disconnect** shows in blue. The protocol has no session: each call is one request, one answer, and a closed socket. qrate infers both events. `connected` is the first call from a name that passes the token check. `disconnected` is one minute of silence from that name. ## Staged findings [Section titled “Staged findings”](#staged-findings) `stage_findings` is the only method that changes what you see. Its result reads `2 staged, 1 stale`. * **Staged** findings go to the Problems panel, beside your own validators’ findings. A finding that proposes a new value also adds it to that cell’s right-click **Fixes** menu. * **Stale** findings are dropped. A finding is stale when the cell no longer holds the text the agent read. This stops a correction to text nobody reviewed. Staged findings are never written to the `.qrate` file. They are gone when you close the project. A proposal changes a cell only after you click it in the Fixes menu. ## Names are not proof [Section titled “Names are not proof”](#names-are-not-proof) The name in an entry is a label the caller chose, in an `X-Agent` header. qrate cannot verify it. Anything that holds the token can claim any name. Use the name to tell two of your own agents apart, not to decide whether to trust a caller. ## Copy an entry [Section titled “Copy an entry”](#copy-an-entry) Right-click an entry. **Copy** copies that one line. **Copy all** copies the full list. Both give tab-separated text, which pastes into a spreadsheet as columns and into a bug report as a readable line. The list reads top to bottom, oldest first, and follows new entries as they arrive. It holds the most recent 200 entries. It is in memory only, never written to your project, and gone when you quit. # AI tools > Connect an AI agent to these docs over MCP, or read them as plain markdown. These docs are readable by agents as well as people. Nothing here needs an account or a key. ## MCP server [Section titled “MCP server”](#mcp-server) The endpoint at `https://qrate.dvnl.work/mcp` speaks [Model Context Protocol](https://modelcontextprotocol.io) over stateless HTTP. It answers with three tools: `search_docs`, `get_doc`, and `list_docs`. Claude Code: ```sh claude mcp add --transport http qrate-docs https://qrate.dvnl.work/mcp ``` Cursor, in `.cursor/mcp.json`: ```json { "mcpServers": { "qrate-docs": { "url": "https://qrate.dvnl.work/mcp" } } } ``` A client that only speaks stdio can bridge with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote): ```json { "mcpServers": { "qrate-docs": { "command": "npx", "args": ["mcp-remote", "https://qrate.dvnl.work/mcp"] } } } ``` The tool catalog is at [`/mcp-schema.json`](/mcp-schema.json) if you want to read what the server offers before connecting. ## Plain markdown [Section titled “Plain markdown”](#plain-markdown) Add `.md` to any docs URL for the source markdown, without navigation, styles, or scripts. For example [`/docs/columns.md`](/docs/columns.md). ## llms.txt [Section titled “llms.txt”](#llmstxt) * [`/llms.txt`](/llms.txt) — the page index * [`/llms-full.txt`](/llms-full.txt) — every page in one file * [`/llms-small.txt`](/llms-small.txt) — the same, trimmed for smaller context windows ## The Agent panel [Section titled “The Agent panel”](#the-agent-panel) Reading the docs is separate from reading a project. An agent running on your own machine can read the project open in qrate through the [Agent panel](/docs/agent-panel), which is a different mechanism with its own permissions. # Columns > column types, authority lists, and per-column settings Open **Settings ▸ Project ▸ Columns** to configure a column. Settings are stored per project, keyed by the column’s header name, so renaming other columns does not disturb them. ## Column type [Section titled “Column type”](#column-type) A column’s type controls how qrate treats its values: * **Text** — a plain field, checked only by spelling if you enable that. * **Filename** — links each row to a file. See [Files and photos](/docs/files-and-photos) and [Diagnostics](/docs/diagnostics#what-qrate-checks). * **Date** — checked for a valid, unambiguous date format. * **Authority-checked** — checked against LCSH, GeoNames, or Wikidata, depending on what the column holds. ## Description [Section titled “Description”](#description) Add a short description to a column to document what it is for. It shows as a tooltip on the column header, for anyone else who opens the project. ## Spell check [Section titled “Spell check”](#spell-check) Turn spell check on or off per column. The languages ticked in Settings ▸ Spelling take part in automatic language selection; the first one ticked chooses the preferred regional spelling when variants exist. ## Value variants [Section titled “Value variants”](#value-variants) Turn on value-variant review for columns where inconsistent displayed forms matter, such as people, organizations, places, subjects, collection titles, and controlled labels. qrate suggests similar values already present in that column but never merges them automatically. ## Authority lists [Section titled “Authority lists”](#authority-lists) For a column checked against an authority, such as subject headings, qrate flags a value that the authority does not recognize and can suggest the closest match as a fix. GeoNames needs an activated GeoNames web-services account. If GeoNames rejects the account, qrate stops the remaining requests and shows one warning. Change the account in Settings to retry. # Diagnostics > the Problems panel, spelling, and fixes qrate checks project data continuously and lists what it finds in the Problems panel, in the right dock. A finding on a cell also shows as a small marker on that cell in the grid. ## What qrate checks [Section titled “What qrate checks”](#what-qrate-checks) * **Spelling**, in over 60 languages. Tick one or more in Settings ▸ Spelling; each value is checked in whichever ticked language fits it, and changes apply without a restart; short or ambiguous values are left alone instead of being checked as the wrong language. If only English dictionaries are installed, qrate also skips reliably identified non-English text and common non-English apostrophe elisions. * **Capitalization**, when a dictionary knows the word but requires a different case. Capitalization findings are warnings. The Notes tab is reserved for user notes. * **Value variants**, for columns where you opt into reviewing inconsistent displayed forms. This catches punctuation, diacritic, word-order, and close-spelling differences in names, organizations, places, subjects, titles, and other labels. Similarity is a review hint, not a claim that two values identify the same entity. Right-click a column header and select **Review value variants** to enable this check. Select the checked command again to disable it. * **Date formats**, so a malformed or ambiguous date is caught before export. * **File links**, so a row whose linked file cannot be found is reported instead of silently showing a blank preview. See [Files and photos](/docs/files-and-photos). Only a column set to the `Filename` type is checked this way. * **Headings against LCSH, GeoNames, and Wikidata**, so a subject or place heading can be checked against those authorities. * **Plugin validators**, if the project has plugins that add their own checks. See [Plugins](/docs/plugins). Each of these runs as an independent validator. A validator reports its complete set of findings each time it runs, and that set replaces what it reported last time. ## The Problems panel [Section titled “The Problems panel”](#the-problems-panel) Repeated spelling, capitalization, value-variant, date, and authority findings appear in collapsed groups. Each group shows an occurrence count. Expand the group to see its locations. Expanded value-variant groups show the value at each location instead of repeating the summary. The expansion button supports keyboard activation. Click an occurrence to select its cell, row, or column. A group header does not select an arbitrary location. Its context menu can resolve all occurrences. Findings can describe four scopes: | Scope | Location label | Navigation | | ------- | ---------------- | ----------------- | | Cell | `Row N · Column` | Select the cell | | Row | `Row N` | Select the row | | Column | Column name | Select the column | | Dataset | Dataset name | No cell selection | The severity tabs count diagnostic occurrences, not collapsed groups or unique cells. Two distinct misspelled words in one cell count as two occurrences. Repeated copies of the same word in one cell count once. A cell can contain more than one member of a value-variant cluster. The source filter changes both the visible findings and the tab counts. It is a multi-select filter. Uncheck one or more diagnostic sources to hide them. User notes do not appear as a source because the Notes tab already selects them. The All, Errors, and Warnings tabs show computed findings only. The Notes tab shows user notes only. The severity filter changes the visible findings, but each tab keeps its own total. Notes and validators without group metadata remain separate entries. ### Producer contracts [Section titled “Producer contracts”](#producer-contracts) The diagnostics store keeps each finding at its exact location. The panel groups findings only when a producer supplies `DiagnosticGroup` metadata. Group identity includes the dataset, source, severity, and producer key. The summary is display text, not identity. Column validators return `ColumnFinding`. A row index describes a cell. An absent row index describes the whole column. Row-wide and dataset-wide producers publish addressed diagnostics directly. Existing plugin scripts keep their current row-based output format. Spelling keys include the selected dictionary and observed token. Capitalization keys also include the proposed spelling. Value-variant keys include the column and the sorted set of exact displayed cluster members. Similarity pairs form links. Connected values appear together for review, but qrate does not change them until the user chooses a canonical form. Date keys are the exact rejected value. Authority keys are the rejected value, ignoring case. ## Applying a fix [Section titled “Applying a fix”](#applying-a-fix) Some findings offer a suggested correction. Right-click the cell, open **Problems**, choose the finding, and pick a suggestion. The same items appear when you right-click the finding in the Problems panel. Long labels are shortened, and a cell with more than eight findings lists the first eight and points to the Problems panel for the rest. Applying a fix replaces the cell’s whole value, so you always know exactly what you are accepting. Value-variant fixes offer forms that already occur in the same column. qrate never chooses a canonical form, merges records, or changes every matching row automatically. Right-click a spelling or capitalization group to apply one correction to all its occurrences. The group resolver applies the changes as one undo step. Right-click a value-variant group to replace all occurrences with any displayed member. You can also mark the cluster members as distinct for that column. qrate saves this choice with the project. qrate uses the configured subdelimiter to read multiple logical values in one cell. Validators borrow these values as text slices, so splitting does not allocate a list for each cell. Value-variant fixes preserve the other logical values and replace the complete cell through the normal edit path. A fix offered against one version of a cell’s text does not apply once that text has changed. This stops a stale suggestion from silently overwriting a newer edit. Expanded cell occurrences offer the same spelling and fix menus as the grid. Row, column, dataset, and group entries do not offer cell fixes. Each accepted fix uses the existing undoable cell edit path. Validation then refreshes the groups. ## Ignoring a finding [Section titled “Ignoring a finding”](#ignoring-a-finding) Each finding offers two ways to ignore it: * **Ignore this occurrence** hides it in this one cell. qrate remembers the row, so the ignore follows the row when you insert or delete rows above it. * **Ignore in column** hides every finding with the same key in that column. For spelling, the key includes the word, so a different misspelling still shows. A group row in the Problems panel offers **Ignore ▸ In column** for each column it covers. After you ignore something, a notice with **Undo** appears for a few seconds. qrate saves ignores with the project. They never hide findings from another check. Turn on **Show ignored** in the Problems panel to see ignored findings dimmed, and right-click one to **Unignore** it. **Settings ▸ Columns ▸ Ignored problems** lists every ignore that still hides something. Remove one there, or use **Clear all**. ## Design credit [Section titled “Design credit”](#design-credit) qrate’s value-clustering workflow is inspired by OpenRefine’s [Cluster and edit](https://openrefine.org/docs/manual/cellediting#cluster-and-edit) feature and [clustering methods](https://openrefine.org/docs/technical-reference/clustering-in-depth). qrate uses an independent implementation for its diagnostics and cataloging workflow. This acknowledgment does not imply OpenRefine’s endorsement. OpenRefine publishes its source under BSD-3-Clause and its documentation under CC BY 4.0. This implementation does not copy OpenRefine source, documentation text, or UI assets. Any future adaptation must retain the applicable copyright, license, and attribution notices. # Export and Google Sheets > CSV, JSON-LD, CSL-JSON, ZIP, and Sheets sync ## Exporting [Section titled “Exporting”](#exporting) Export the project from the **File** menu as one of: * **CSV** * **JSON-LD** * **CSL-JSON** * **A ZIP archive**, which bundles the exported data with the linked files it points to. Export always reads every row, regardless of any active filter. See [The grid](/docs/grid#filtering). ## Google Sheets [Section titled “Google Sheets”](#google-sheets) Google Sheets export and sync is off by default. Turn it on in **Settings ▸ Google**. Until you do, qrate shows no Google Sheets item in its menus. Once it is on, you can: * **Export to a new Google Sheet.** * **Sync an existing sheet**, so qrate and the sheet stay in step with each other. Sign-in happens on your own machine. qrate never sees your Google password, and it can reach only the sheets it created or that you picked yourself through Google’s file picker. No qrate account or hosted service is involved at any point. # Files and photos > link records to files, and view them A row can link to a file on disk: a photo, document, audio, or video file. qrate never copies these files into the project. It stores the files folder’s path and finds each row’s file by matching it against that folder every time you open the project. ## Linking a row to a file [Section titled “Linking a row to a file”](#linking-a-row-to-a-file) Set the files folder in **Settings ▸ Project**. qrate then matches each row to a file by one of two rules: * **Exact filename.** A column value that matches a file’s name, such as an identifier column, links that row to that file. * **Your own pattern.** Configure a column as the `Filename` type to control which column and which matching rule qrate uses. See [Columns](/docs/columns). If a linked file cannot be found, for example because the files folder moved or the file was renamed, qrate reports it as a diagnostic. See [Diagnostics](/docs/diagnostics). ## Viewing a file [Section titled “Viewing a file”](#viewing-a-file) The Details panel, in the right dock, shows the file linked to the selected row. It previews images, documents, audio, and video directly. Click the preview to open it fullscreen, where you can zoom, pan, page through a multi-page document, and search inside it. PDF previews need PDFium and video frame previews need ffmpeg. qrate looks for both beside its own executable, then for the copies it installed itself, then on your system `PATH`. When one is missing, opening a PDF or a video offers to install it, and **Settings ▸ Components** lists both. Until then, qrate shows a file-type icon instead of a preview, and the rest of the app works as normal. ## Gallery view [Section titled “Gallery view”](#gallery-view) Switch to the gallery from the view menu to browse a collection as thumbnails instead of a grid. See [The grid](/docs/grid#gallery-view). # The grid > edit cells, search, filter, and undo The grid is the main view of a project. Each row is a record. Each column is a field. ## Editing [Section titled “Editing”](#editing) Click a cell to select it. Type to replace its content, or press Enter to edit in place. Copy, cut, and paste work across one cell or a selected range, including between qrate and other spreadsheet apps. Every edit goes on the undo stack, including row and column changes. Press **Ctrl+Z** to undo and **Ctrl+Shift+Z** (or **Ctrl+Y**) to redo. Undo and redo also cover adding, removing, and reordering rows and columns, so a structural change is as safe to try as a cell edit. ## Rows and columns [Section titled “Rows and columns”](#rows-and-columns) Right-click a row or column header for a menu to add, remove, rename, freeze, or reorder it. A frozen row or column stays in view while you scroll. ## Search and replace [Section titled “Search and replace”](#search-and-replace) Open search with **Ctrl+F**. Search moves through matching cells in the grid. Open replace with **Ctrl+H** to replace one match or all matches at once. ## Filtering [Section titled “Filtering”](#filtering) Click the filter icon in a column header to open its filter. It lists every distinct value in that column as a checklist. Uncheck a value to hide the rows that hold it. The grid shows only rows that pass every active column filter at once. Filtering hides rows. It does not change or delete data, and it does not affect export or diagnostics, which both still see every row. ## Gallery view [Section titled “Gallery view”](#gallery-view) Switch the grid to a gallery of thumbnails from the view menu. The gallery shows one tile per row, using the file or photo linked to that row. It is a way to review a collection by image instead of by cell, useful for photo collections in particular. ## Notes [Section titled “Notes”](#notes) Right-click a cell and choose **Add note** to attach a free-text note to it. A cell with a note shows a small corner marker. Notes are for your own reviewing use. They are not exported. # Install > Install qrate from GitHub Releases or Homebrew, with Windows package listings in progress. ## GitHub Releases [Section titled “GitHub Releases”](#github-releases) Choose the recommended base download for your platform. It installs optional tools when you first need them. The download panel also lists the other packages: Recommended for v0.6.0-beta.2 Windows [Download Base installer](/thanks?a=qrate-0.6.0-beta.2-base-setup.exe) 14.3 MB · qrate-0.6.0-beta.2-base-setup.exe macOS [Download Base disk image](/thanks?a=qrate-0.6.0-beta.2-base-universal.dmg) 52.8 MB · qrate-0.6.0-beta.2-base-universal.dmg Linux [Download Base archive](/thanks?a=qrate-0.6.0-beta.2-base-x86_64-linux.tar.gz) 33.8 MB · qrate-0.6.0-beta.2-base-x86\_64-linux.tar.gz Start with a base download. qrate offers to add PDF previews and the assistant when you need them. Check a download against [SHA256SUMS.txt](https://github.com/devnull03/qrate/releases/download/v0.6.0-beta.2/SHA256SUMS.txt). * **Full:** a larger download with optional tools for computers that stay offline. On macOS, install ffmpeg separately for video previews. * **Portable ZIP (Windows):** unpack and run qrate without an installer. * **Managed MSI (Windows):** for IT deployment with Group Policy, SCCM, or Intune. An installed qrate updates to the same type of download it came from. You can also add optional tools later under **Settings ▸ Components**. ## Homebrew (macOS) [Section titled “Homebrew (macOS)”](#homebrew-macos) The [qrate tap](https://github.com/devnull03/homebrew-tap) installs the base app: ```sh brew install --cask devnull03/tap/qrate ``` The tap can lag a prerelease. Use the GitHub download above for the newest beta; stable releases update the tap automatically once that channel is enabled. qrate offers to install optional components when you need them. For video previews, install ffmpeg separately with `brew install ffmpeg`. ## Windows package managers [Section titled “Windows package managers”](#windows-package-managers) The WinGet package will use the per-machine MSI. Its first community manifest still needs to be submitted and accepted. Once it is listed, install it with: ```powershell winget install --id devnull03.qrate --exact ``` The Microsoft Store package is in preparation. Its first listing must be submitted and certified before it can be installed from the Store. Use a GitHub Releases download until then. ## Run from source [Section titled “Run from source”](#run-from-source) Install Rust with rustup. Add the rustfmt and clippy components. Then clone the repository and run it. The first build downloads the dependencies. Later builds use the Cargo cache. ```sh git clone https://github.com/devnull03/qrate cd qrate cargo run ``` ## Optional preview tools in a checkout [Section titled “Optional preview tools in a checkout”](#optional-preview-tools-in-a-checkout) Common image formats work without extra tools. PDF previews use PDFium. Video frame previews use ffmpeg. qrate links neither one. It loads PDFium dynamically and runs ffmpeg as a subprocess. A checkout without them still builds and launches, but PDFs and videos show only a type icon. qrate looks beside its own executable first, then for a copy it installed itself (Settings ▸ Components), then on your PATH. This script downloads PDFium for your platform, and ffmpeg on Windows. On macOS and Linux, install ffmpeg with your package manager. ```sh ./scripts/fetch-binaries.sh ``` Caution Beta build. Keep backups of important collections, and report problems with steps that reproduce them. # Plugins > find, install, and manage plugins A plugin adds checks and commands to qrate. Install a plugin from the official catalog, review an unlisted public GitHub release, or add a local plugin folder. To write a plugin, see [Develop plugins](/docs/plugins/developing). ## Install a plugin [Section titled “Install a plugin”](#install-a-plugin) Open **Plugins ▸ Discover Plugins…** to browse qrate’s signed official catalog. qrate checks the catalog signature and package hash before it installs a plugin. An official listing means that qrate maintainers reviewed the published metadata and exact package bytes. It does not mean that third-party code is safe. Use **Plugins ▸ Install Plugin from Link…** for an unlisted public GitHub repository or release. qrate downloads the release ZIP, checks its static package manifest, and shows its source, permissions, size, and SHA-256 before it asks for confirmation. New package installs are enabled. Optional permissions, such as network access, stay off until you grant them under **Settings ▸ Plugins**. The same page shows whether qrate manages the package and can safely remove it. For an offline installation, open **Plugins ▸ Plugins Folder** and copy a plugin folder there. Manual plugins are unmanaged. qrate does not update or remove them. ## The official plugin registry [Section titled “The official plugin registry”](#the-official-plugin-registry) The [plugin catalog](https://qrate.dvnl.work/plugins/) shows releases from the official plugin registry. qrate reads the signed catalog when you open **Discover Plugins**. The registry stores the release metadata, package hash, and signature. It does not host a plugin’s source code. An official listing means that qrate maintainers reviewed the listed release metadata and package hash. It does not make third-party code safe. Review the source and requested permissions before you install a plugin. To add your own plugin to the registry, read [Develop plugins](/docs/plugins/developing#publish-a-plugin). ## What a plugin can do [Section titled “What a plugin can do”](#what-a-plugin-can-do) * **Check a column.** A plugin reports bad column values in the Problems panel and grid markers. * **Add right-click entries.** A plugin adds commands to a cell, row, or column header menu. * **Add a bar item.** A plugin adds text and commands to the status bar or title bar. * **Suggest values.** A plugin offers completions while you edit a cell. * **Map columns onto a list.** A plugin can offer a list picker in Settings and column menus. * **Declare settings.** A plugin declares settings. qrate renders and stores their values. A plugin can use the network only after you grant that permission. The runtime has no `io`, `os`, or `package` library. ## See also [Section titled “See also”](#see-also) * [Develop plugins](/docs/plugins/developing) — create, test, and publish a plugin. * [API reference](/docs/plugins/api-reference) — hooks, host functions, and declarations. * [Islandora plugin](/docs/plugins/islandora) — an official catalog plugin and a larger worked example. # Plugin API reference Everything a plugin can do goes through two surfaces. The first is the table `init.lua` returns, which qrate calls the descriptor. The second is the `qrate` global, which qrate installs before it sandboxes the virtual machine. There is no third surface. Luau’s `io`, `os`, and `package` are not loaded, so a capability that is not on this page is one no plugin has. This page describes `api_version` 2. For a getting-started walkthrough, see [Plugins](/docs/plugins). ## The descriptor [Section titled “The descriptor”](#the-descriptor) ```lua return { api_version = 2, name = "my-plugin", description = "One line, shown on the Settings page.", permissions = { "net" }, settings = { … }, menu = { … }, bar = { … }, column_map = { … }, exports = { … }, validate = function(column, values, settings) end, on_command = function(command, ctx) end, suggest = function(ctx) end, export = function(id, snapshot) end, } ``` Every field is optional, with one rule: the table must carry at least one of `validate`, `on_command`, `suggest`, or `export`. A descriptor with none of them does nothing, and qrate refuses it. | Field | Value | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `api_version` | The descriptor shape you wrote against. Missing reads as 1. A version above what the running qrate speaks is refused, and the error names both numbers. | | `name` | Overrides the folder name as the plugin’s identity. See [Identity](#identity). | | `description` | One line, shown on the plugin’s Settings page. | | `permissions` | What the plugin asks to be allowed to do. `net` is the only one. | qrate reads the descriptor once, when it loads the plugin. To change a declaration, edit the file and click **Plugins ▸ Reload Plugins**. ### Identity [Section titled “Identity”](#identity) Two names exist, and they are keyed differently. * **The name on disk** is the file stem or the folder name. qrate keys the enable switch, the permission grants, and the plugin’s storage file by it. * **The descriptor `name`**, when you declare one, is what the Problems panel shows, what the plugin’s findings are filed under, and what its stored settings are keyed by. Renaming either one orphans what qrate stored under the old name. Choose both before you publish the plugin. ## Validation [Section titled “Validation”](#validation) ```lua validate = function(column, values, settings) -> { Finding } ``` qrate calls `validate` for one column at a time, after the grid settles. It calls it for **every** column in the project, not only the columns your plugin cares about. Return an empty table for a column you have nothing to say about. `column` carries two fields: | Field | Value | | ----------- | -------------------------------------------------------------------------- | | `name` | The header text. This is also how a finding addresses a column. | | `data_type` | The column’s declared type from the project, or empty when nobody set one. | `values` holds every row’s text for that column, in source order. `settings` is [the settings table](#reading-settings). A finding is a table: ```lua { row = 3, severity = "warning", message = "Country is not a place name" } ``` | Field | Value | | ---------- | --------------------------------------------------------------------------------------------------------------------- | | `row` | 1-based, matching the `values` array you were handed. | | `severity` | `"error"`, `"warning"`, or `"note"`. Missing reads as `"error"`. A spelling qrate does not know degrades to `"note"`. | | `message` | What is wrong, written for the archivist. | **Each run replaces the plugin’s whole set of findings.** qrate does not merge with what the plugin reported last time, and there is no call to clear one finding. A cell the plugin stops reporting stops being marked. This is the same rule every built-in validator follows. `validate` runs off the interface thread, so it may block on a server. It also runs on every edit, so anything expensive belongs in a command or behind [storage](#storage) instead. An error raised inside `validate` goes to the session log, and that run contributes nothing. It does not reach the Problems panel: the panel is a list of what is wrong with the data, not with the code. ## JSON exports [Section titled “JSON exports”](#json-exports) Plugin exports require `api_version = 2`. API version 1 plugins continue to work without changes. Declare each File ▸ Export entry in `exports`: ```lua exports = { { id = "iiif", label = "IIIF Presentation 3…", suggested_name = "manifest.json" }, }, ``` Each `id` is local to the plugin. The `label` appears in the Export menu. The suggested name must be one file name without a path. The plugin returns a JSON value from `export`: ```lua export = function(id, snapshot) return { label = snapshot.title, items = snapshot.rows, } end, ``` The snapshot does not change during the export. It has these fields: | Field | Value | | ---------- | ------------------------------------------------------------------- | | `title` | The project display name. | | `columns` | Every column in source order. | | `rows` | Every row in source order. Each row has one string for each column. | | `settings` | [The settings table](#reading-settings). | Each column has `name`, `data_type`, and `settings`. The column settings contain only this plugin’s stored object for that column. The plugin does not receive a path or file handle. qrate asks the user for a path and writes the returned JSON. The user can cancel before the plugin runs. qrate limits a snapshot to 512 columns, 250,000 rows, two million cells, and 64 MiB of text. qrate also limits the JSON output to 64 MiB. The Lua memory and execution limits still apply. ## Commands [Section titled “Commands”](#commands) A command is a name your plugin declares and qrate hands back to you when the user clicks. Both right-click entries and [bar items](#bar-items) run commands. ### Right-click entries [Section titled “Right-click entries”](#right-click-entries) ```lua menu = { { label = "Restrict to these values", target = "column", command = "restrict" }, { label = "Stop restricting", target = "column", command = "clear", requires_settings = true }, }, ``` | Field | Required | Value | | ------------------- | -------- | --------------------------------------------------------------------------------------- | | `label` | yes | What the entry reads as. | | `target` | yes | `"column"`, `"cell"`, or `"row"` — which menu the entry joins. | | `command` | yes | Handed back to `on_command` unchanged. | | `requires_settings` | no | Show the entry only when this plugin has already stored something for what was clicked. | `requires_settings` is the whole conditional vocabulary. Use it so a “Clear” entry does not offer to undo nothing. ### Handling a click [Section titled “Handling a click”](#handling-a-click) ```lua on_command = function(command, ctx) -> Writes? ``` qrate runs `on_command` off the interface thread, so a slow command does not freeze the grid. `ctx` carries what was clicked: | Field | Value | | ---------- | ------------------------------------------------------------------------ | | `column` | Header text, or `nil` when nothing gives the command a column. | | `row` | 1-based, when the user clicked a single cell. | | `values` | Every row’s text for `column`, in source order. | | `argument` | The option a mapping menu entry carried, when the command came from one. | | `settings` | [The settings table](#reading-settings). | Check `ctx.column` before you read `ctx.values`. A command from a bar item may have no column under it. ### Writing settings back [Section titled “Writing settings back”](#writing-settings-back) `on_command` returns a table of what qrate must store, or nothing at all: ```lua return { column = { allowed = ctx.values }, project = { checked = true } } ``` | Field | Where it lands | | --------- | ----------------------------------------------------------------- | | `column` | This plugin’s object for the clicked column, in the project file. | | `project` | This plugin’s project-scope object, in the `.qrate` file. | | `user` | This plugin’s user-scope object, on this machine. | A field you leave out leaves that scope alone. A field you set **replaces** this plugin’s whole object in that scope. Read the old value, change it, and hand the whole thing back. Do not expect a merge. Storing an empty table is how a command clears a scope. `requires_settings` reads an empty table as nothing stored, so the two agree without a second concept. A `column` write with no column under it is dropped. qrate does not invent a column for it. A command that fails is logged, not shown in the Problems panel. ## Bar items [Section titled “Bar items”](#bar-items) A bar item is text in the status bar or the title bar. Use one when the plugin has something to say about the whole project. Use a right-click entry when the plugin acts on one column. A connection check belongs on the bar. A “restrict this column” command does not. ```lua bar = { { id = "conn", bar = "status", side = "right", text = "Islandora ?", tooltip = "Click to check the Islandora connection", left = { command = "check" }, right = { menu = { { label = "Check now", command = "check" }, { label = "Stop checking", command = "stop" }, } }, }, }, ``` | Field | Required | Value | | --------- | -------- | ----------------------------------------------------------------------------------------- | | `id` | yes | Names the item inside this plugin. [`qrate.status.set`](#retitling-an-item) addresses it. | | `bar` | yes | `"status"` or `"title"`. | | `side` | yes | `"left"` or `"right"`. | | `text` | yes | The first text to show. See [Markup](#markup). | | `tooltip` | no | Plain text. Markup does not apply here. | | `left` | no | What the left mouse button does. | | `right` | no | What the right mouse button does. | An action is either `{ command = "name" }` or `{ menu = { { label = …, command = … }, … } }`. An action that declares both stops the plugin from loading, and so does an unknown `bar` or `side`. An item with no action shows text and does nothing. ### The context a bar command gets [Section titled “The context a bar command gets”](#the-context-a-bar-command-gets) A bar item has no column under it, so qrate fills the column fields from the table selection instead: * A cell or column selection gives `ctx.column`, `ctx.settings.column`, and `ctx.values`. * A row selection, or no selection at all, leaves `ctx.column` as `nil`. ### Retitling an item [Section titled “Retitling an item”](#retitling-an-item) ```lua qrate.status.set(id, text) ``` This changes the text of one of your own declared items. It works from `on_command` and from `validate`. The call buffers the new text, and qrate applies it after your function returns. A plugin cannot retitle another plugin’s item, and an `id` that names no declared item does nothing. ```lua on_command = function(command, ctx) qrate.status.set("conn", "[muted]checking…[/]") local response, err = qrate.http.get(server) qrate.status.set("conn", err and "[red]~~Islandora~~[/]" or "[green]**Islandora** ✓[/]") end, ``` Say what is happening before a slow call. A request can take ten seconds, and a bar that shows nothing in that time reads as a click that did nothing. ### Markup [Section titled “Markup”](#markup) The markup is inline only. A bar holds one line, so there are no links, no lists, and no blocks. | Spelling | Result | | --------------- | ------------- | | `**text**` | Bold | | `*text*` | Italic | | `~~text~~` | Strikethrough | | `__text__` | Underline | | `[name]text[/]` | Color | Note one difference from CommonMark. Here `__` means underline. It is not a second spelling of bold. Unicode passes through untouched, so an icon needs no markup. `⏳ **Islandora**` works. #### Colors [Section titled “Colors”](#colors) The color tags follow the style of the Python Rich library. Open with a color name in brackets. Close with `[/]`, or with `[/name]`. | Tag | Also | Use | | ---------- | ----------- | ---------------------- | | `[red]` | `[danger]` | A failure | | `[green]` | `[success]` | A success | | `[yellow]` | `[warning]` | A warning | | `[blue]` | `[info]` | Information | | `[accent]` | | Emphasis | | `[muted]` | | Text that matters less | Each name resolves through the current theme, so `[red]` is the theme’s red and not a fixed value. The text stays readable when the user changes to a light theme. This is why the list holds roles and not every color. #### Nesting and literal text [Section titled “Nesting and literal text”](#nesting-and-literal-text) Markup nests. `[green]**Islandora** ✓[/]` is bold inside green. Text that does not close renders as itself, which keeps ordinary prose safe: * `2 * 3 things` shows the asterisk. * `[green]never closed` shows the tag. * `[mauve]…` shows the tag, because no theme color carries that name. * `[see note]` shows the brackets, because a tag name cannot hold a space. ### Where items appear [Section titled “Where items appear”](#where-items-appear) qrate groups items by bar and by side. Inside a group, items appear in the order the plugins loaded. Plugin text in the status bar sits to the left of qrate’s own readouts. Plugin text in the title bar sits inboard of the dock buttons. Reloading plugins replaces every item. An item whose plugin no longer declares it goes away. ## Suggestions [Section titled “Suggestions”](#suggestions) ```lua suggest = function(ctx) -> { string } ``` qrate calls `suggest` while the user types in a cell, and puts what you return under the editor. | Field of `ctx` | Value | | -------------- | ---------------------------------------- | | `column` | Header text. | | `row` | 1-based, the cell being edited. | | `prefix` | The text in that cell as it stands. | | `settings` | [The settings table](#reading-settings). | qrate waits for a short pause in typing before it asks, and it drops an answer that a newer keystroke has superseded. A `suggest` that takes as long as one network request is therefore acceptable. ## Settings [Section titled “Settings”](#settings) A plugin declares its knobs, and qrate renders and stores them. The host never reads inside a plugin’s object, which is what lets you add a knob without a qrate release. ### Declaring a knob [Section titled “Declaring a knob”](#declaring-a-knob) ```lua settings = { { key = "server", label = "Server address", type = "text", scope = "project", description = "The site this plugin checks against." }, { key = "password", label = "Password", type = "password", scope = "user" }, }, ``` | Field | Required | Value | | ------------- | -------- | ----------------------------------------------------- | | `key` | yes | Names a field inside this plugin’s object in `scope`. | | `label` | yes | What the row reads as in Settings. | | `description` | no | A line under the row. | | `scope` | yes | `"user"` or `"project"`. | | `type` | yes | `"switch"`, `"text"`, or `"password"`. | qrate puts every declared knob on a Settings page named after the plugin. A `password` knob is masked as it is typed. qrate refuses a `password` in project scope, because the `.qrate` file gets shared and committed. Put credentials in user scope. qrate merges a knob into the plugin’s object by key. This is unlike a command’s write, which replaces the whole object. ### Reading settings [Section titled “Reading settings”](#reading-settings) Every hook receives the same settings table: | Field | What it holds | | --------- | ---------------------------------------------------------------------------------------- | | `column` | This plugin’s object for the column in hand, from the project file. | | `project` | This plugin’s project-scope object, from the `.qrate` file. It travels with the project. | | `user` | This plugin’s user-scope object, stored per machine. | | `app` | App-wide values the plugin must agree with rather than restate. | A plugin sees only its own objects. Another plugin’s settings are invisible. A scope with nothing stored arrives as an empty table, never `nil`, so a read needs no guard. `app` currently holds one field: | Field | Value | | -------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `subdelimiter` | What separates several values inside one cell, such as `;` in `Film; Video`. Empty means the cell holds one indivisible value. | Split cells with `app.subdelimiter`. A plugin that splits them differently disagrees with the column filter the user can see. ## Column mapping [Section titled “Column mapping”](#column-mapping) A plugin that holds a list of terms can offer one picker per column. ```lua column_map = { key = "vocabulary", label = "Islandora vocabulary", description = "Check this column against one of the site's vocabularies.", options = "vocabularies", refresh = "fetch_vocabularies", multiple = false, }, ``` | Field | Required | Value | | ------------- | -------- | ---------------------------------------------------------------------------------------------------- | | `key` | yes | The field written into each column’s own bucket. `validate` reads it back as `settings.column[key]`. | | `label` | yes | What the picker reads as. | | `description` | no | A line under the picker. | | `options` | yes | The key in this plugin’s **project-scope** object that holds the list. | | `refresh` | yes | The command qrate runs when the user clicks Refresh. | | `multiple` | no | Whether one column may carry several options. Default `false`. | Declare it once and qrate renders it twice: as a per-column picker on the plugin’s Settings page, and as a checkable submenu on the column header. Both write to the same place, so the two cannot disagree. No plugin code runs to draw either one. The options are whatever the plugin last stored under `options`, which is what lets a list fetched from a server appear in a menu that qrate must build while the user waits. Store each option as a plain string, or as `{ value = …, label = … }`. Your `refresh` command is what fills that list. Fetch it, then return it as a project-scope write. ## Storage [Section titled “Storage”](#storage) ```lua qrate.storage.get(key) -- the stored value, or nil qrate.storage.set(key, value) -- store it ``` This is the plugin’s own cache. qrate keeps it beside the app’s data and **not** in the project file: what a plugin caches is about this machine, and a project file is a thing people commit. The cache survives a restart. qrate writes it out after a call that changed it. Both directions convert the whole value on every call. Reading a cache of ten thousand entries costs ten thousand conversions each time you ask for it. Read it once into a local and keep it across calls. ## Network [Section titled “Network”](#network) Network access is the only permission that exists. Declare it, and the user grants it in **Settings ▸ Plugins**: ```lua permissions = { "net" }, ``` ```lua local response, err = qrate.http.get(url, { auth = { username = u, password = p } }) ``` `get` answers a response table, or `nil` and a message. An unreachable server is a thing to report, not a thing to crash on. The response carries `status` and `body`. Until the user grants `net`, `qrate.http.get` answers `nil` and a message that says so. Your code needs no second path for the ungranted case. `auth` sends HTTP basic authentication. A blank or missing pair is the same as sending none, so one code path works whether or not the user has filled the credential settings in. Basic authentication lives in the host because Luau has no base64 and no way to build a header. qrate owns the timeout, the TLS policy, and the redirect policy. It also rate limits calls per plugin, and a refused call spends a request too, so a loop against a dead server cannot spin. ### Reading a response [Section titled “Reading a response”](#reading-a-response) ```lua local body, err = qrate.json.decode(response.body) ``` `decode` answers a table, or `nil` and a message. A server that returns an error page or a bot check instead of JSON is common enough to handle rather than raise. JSON `null` arrives as `nil`, so a missing field and an explicitly null one read the same. ## Modules and logging [Section titled “Modules and logging”](#modules-and-logging) ```lua local api = require("api") ``` `require` loads a sibling `.lua` file from this plugin’s own folder, and nothing else. There is no path syntax: qrate reads the folder, and a name resolves only against what it found there. A module evaluates once, however many times you require it. Split a plugin into modules when one file stops being readable, and not before. ```lua print("checking " .. tostring(ctx.column)) ``` `print` writes to qrate’s session log, tagged with the plugin’s name. A packaged build has no console, so this is the only `print` anybody will read. Reach the log with **Help ▸ Copy Debug Info**. ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ----------------- | -------------------------- | | Memory per plugin | 64 MB | | One call into Lua | 2 seconds | | HTTP requests | 120 per minute, per plugin | | One HTTP request | 10 seconds | qrate logs a warning when a call takes longer than 150 ms, naming the plugin, the entry point, and the time. A slow build is diagnosable from an ordinary bug report’s log tail. # Develop plugins > create, test, and publish a plugin Use this guide to create, test, and publish a qrate plugin. For each hook and host function, see the [API reference](/docs/plugins/api-reference). ## How qrate loads a plugin [Section titled “How qrate loads a plugin”](#how-qrate-loads-a-plugin) qrate reads the plugins directory at startup. Open it with **Plugins ▸ Plugins Folder**. qrate accepts two layouts: * `my-plugin.lua` — a single file. * `my-plugin/init.lua` — a folder. `init.lua` can `require` other `.lua` files in that folder. The name on disk is the plugin identity. qrate stores its settings, enable switch, and permission grants under that name. Do not rename a plugin folder after qrate stores data for it. `init.lua` returns the runtime descriptor. Release packages also include `qrate-plugin.json`. qrate reads that manifest without running the plugin. ## Create a plugin [Section titled “Create a plugin”](#create-a-plugin) 1. Clone the [plugin template](https://github.com/devnull03/qrate-plugin-template). 2. Install [luau-lsp](https://github.com/JohnnyMorganz/luau-lsp). 3. Read `types/qrate.lua`. It defines every available API capability. 4. Write your plugin in `init.lua`. 5. Run `npm run check` to validate the package metadata and archive contents. The template includes a working plugin, type definitions, and a release workflow. The `types` folder does not run. It helps your editor complete and typecheck the plugin. ### Minimal plugin [Section titled “Minimal plugin”](#minimal-plugin) This plugin adds a command that marks one column for empty-value checks: ```lua return { api_version = 1, description = "Flags empty cells.", menu = { { label = "Check this column for gaps", target = "column", command = "watch" }, }, on_command = function(command, ctx) return { column = { watched = true } } end, validate = function(column, values, settings) if not settings.column.watched then return {} end local found = {} for row, value in ipairs(values) do if value == "" then found[#found + 1] = { row = row, severity = "warning", message = column.name .. " is empty" } end end return found end, } ``` qrate calls `validate` for every column. Return an empty table for a column that your plugin does not check. The `row` value starts at 1 and matches the `values` array. ## Test a plugin [Section titled “Test a plugin”](#test-a-plugin) 1. Copy or clone the folder into **Plugins ▸ Plugins Folder**. 2. Restart qrate, or select **Plugins ▸ Reload Plugins**. 3. Open **Settings ▸ Plugins** and confirm that qrate lists and enables the plugin. After an edit, select **Plugins ▸ Reload Plugins**. qrate rebuilds the plugin runtime and replaces its contributions. You do not need to restart qrate. qrate reports data problems in the Problems panel. It records plugin code problems in the session log. Read the log with **Help ▸ Copy Debug Info**. A plugin load error also appears in **Settings ▸ Plugins**. ## Publish a plugin [Section titled “Publish a plugin”](#publish-a-plugin) The official registry lists reviewed release packages. It does not host plugin source code. 1. Run `npm run check` for the package you will release. 2. Tag that version in your plugin repository. The template workflow creates a release ZIP and checksum. 3. Check the ZIP and checksum before you submit them. 4. Follow the [plugin submission guide](https://qrate.dvnl.work/plugins/submit/) to open a pull request to the [plugin registry](https://github.com/devnull03/qrate-plugin-registry). The registry pull request must identify one exact release. Do not change that release after the registry lists it. ## Limits [Section titled “Limits”](#limits) | Limit | Value | | ----------------- | -------------------------- | | Memory per plugin | 64 MB | | One call into Lua | 2 seconds | | HTTP requests | 120 per minute, per plugin | | One HTTP request | 10 seconds | qrate warns in the log when a call takes more than 150 ms. qrate calls `validate` after every edit. Put slow work in a command or use a plugin cache. # Islandora plugin > use Islandora vocabularies in qrate Islandora is an open-source repository platform that libraries and archives use to publish digital collections. An Islandora site keeps its controlled vocabularies — subjects, genres, names — as taxonomies. The Islandora plugin connects a qrate column to those vocabularies. It checks each value against the site, and it offers terms from the site while you type. The official plugin catalog lists the plugin as an example of what a plugin can do. ## What the plugin does [Section titled “What the plugin does”](#what-the-plugin-does) **It checks column values against a vocabulary.** Map a column to one or more vocabularies. The plugin then reports every value the site does not hold as an error in the Problems panel. The message names the vocabularies it consulted, for example `"Still image" is not a term in the Islandora typeofresource vocabulary`. **It offers terms while you type.** Type two or more characters in a mapped cell. A completion list appears under the cell editor with matching terms from the site. One character is too little to narrow a vocabulary, so the plugin stays quiet until the second character. **It accepts several vocabularies for one column.** A value passes if any one of the mapped vocabularies holds it. This suits a column that draws from more than one list. **It splits a cell the same way qrate does.** The plugin uses the sub-delimiter from **Settings ▸ Project ▸ Columns**, so a multi-value cell means the same thing to the check as to the column filter. A blank part of a cell is not an error. Missing data is a different check. **It downloads nothing.** The plugin asks the server which of a batch of values the server recognizes. One request covers up to 50 values. A vocabulary of a hundred thousand terms therefore costs the same as a vocabulary of ten. **It remembers answers.** qrate stores each verdict beside its own data, not in the project file. Only a value nobody has checked before costs a request. A re-check after one typo is free. **It never turns a column red because the server failed.** If the site does not answer, the plugin reports nothing for that run and writes the reason to the log. A first pass over a wide sheet checks up to 200 new values per run. The rest wait for a later run, which the next edit starts. ## Install the plugin [Section titled “Install the plugin”](#install-the-plugin) Open the [Islandora plugin page](https://qrate.dvnl.work/plugins/org.islandora.vocabularies/) and select **Open in qrate**. qrate opens a review screen for the official catalog release. Review the source, version, package hash, and requested permissions. Then select **Install**. If you installed qrate from source, the button does not open qrate. Use the Discover Plugins command and select Islandora instead. You can also open the install screen directly: [Open Islandora in qrate](qrate://plugin/install?source=registry\&id=org.islandora.vocabularies). The [Islandora plugin repository](https://github.com/devnull03/qrate-islandora-plugin) has its source code and release history. ## Manual install [Section titled “Manual install”](#manual-install) 1. Open **Plugins ▸ Plugins Folder**. 2. Put the plugin folder there. To clone it, run `git clone https://github.com/devnull03/qrate-islandora-plugin islandora`. 3. Name the folder `islandora`. qrate keys the plugin’s stored settings by the folder name. 4. Restart qrate, or reload the plugins. ## Turn it on [Section titled “Turn it on”](#turn-it-on) 1. Open **Settings ▸ Plugins**. Turn on **Enabled** for `islandora`. 2. Turn on **Network access** for `islandora`. The plugin reaches no server until you do. 3. Open **Settings ▸ islandora**. Set **Islandora server** to the base URL of your site, for example `https://islandora.example.org`. 4. Click **Refresh from server**. The plugin reads the list of vocabularies the site publishes. 5. Map each column under **Mapping**. Pick the vocabularies the column must be checked against. You can also map a column from the grid. Right-click a column header and open the **Vocabularies** submenu. A tick marks each vocabulary the column already uses. The submenu and the settings page write the same value. ## Settings [Section titled “Settings”](#settings) | Setting | Where it is stored | What it is for | | ---------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | | Islandora server | The project | The base URL. It travels with the project file. | | Username | This computer | An account, only if the site refuses anonymous readers. | | Password | This computer | The password for that account. qrate keeps it out of the project file. | | Vocabulary names | The project | Comma-separated machine names, for example `subject, genre`. Use this only if the site will not list its vocabularies. | The plugin has no sub-delimiter setting of its own. It uses the one in **Settings ▸ Project ▸ Columns**. ## The status bar item [Section titled “The status bar item”](#the-status-bar-item) The plugin adds an item on the right side of the status bar. It reads `Islandora ?` until you use it. Click the item to test the connection. The item then shows one of these: * `Islandora ✓` in green — the site answered. * `Islandora ✗` in red — the site did not answer. The log holds the reason. * `Islandora no server set` in red — the **Islandora server** setting is empty. After a refresh, the item shows how many vocabularies the plugin found. ## Column header menu [Section titled “Column header menu”](#column-header-menu) Right-click a column header for two more commands: * **Refresh Islandora vocabularies** — read the list of vocabularies from the site again. * **Forget cached Islandora terms** — discard the stored verdicts for the mapped vocabularies. Use this after somebody adds a term on the site, so qrate asks again instead of trusting an old answer. ## What the site must allow [Section titled “What the site must allow”](#what-the-site-must-allow) The plugin uses Drupal’s JSON:API, which a stock Islandora site enables. If `/jsonapi` answers 404, switch the module on with `drush en jsonapi`. Drupal guards one call. An anonymous reader needs the **`access taxonomy overview`** permission to list vocabularies. Without it, Drupal answers with an empty list instead of refusing, so the mapping tool finds nothing. You have three ways forward: * Grant that permission on the site. * Fill in **Username** and **Password**. * Type the machine names into **Vocabulary names**. The mapping tool works from that list instead. ## Related pages [Section titled “Related pages”](#related-pages) * [Diagnostics](/docs/diagnostics) — the Problems panel, where the plugin reports what it finds. * [Columns](/docs/columns) — column types and the sub-delimiter. * [Plugins](/docs/plugins) — what a plugin can do, and how to write one. # Projects > create, import, and open a `.qrate` project A qrate project is one `.qrate` file. It is a portable SQLite database. It holds the collection grid, column settings, notes, and other project metadata. Copy or move this one file to move the project. Linked files, such as photos and documents, stay where they are. qrate never copies them into the project. If you move a linked-files folder, open **Settings ▸ Project** and point the files-folder path at its new location. ## Create a project [Section titled “Create a project”](#create-a-project) The launcher offers three ways to start: 1. **Blank project.** Start with an empty grid and add columns yourself. 2. **Import a spreadsheet and its folder.** qrate reads a CSV, Excel workbook, or OpenDocument spreadsheet as the grid. If you also give it a files folder, qrate links rows to files in that folder by filename. 3. **Start from a Google Sheet link.** qrate reads the sheet once to build the project. See [Export and Google Sheets](/docs/export-and-sync) for how to keep the two in sync afterward. ## Open a project [Section titled “Open a project”](#open-a-project) The launcher lists recent projects. Pick one to open it, or browse to any `.qrate` file. ## Project settings [Section titled “Project settings”](#project-settings) **Settings ▸ Project** holds settings scoped to this one project: the linked-files folder, column configuration, and plugin settings. **Settings ▸ App** holds settings that apply to every project you open, such as the interface theme.