name: use-lq description: Read, edit, and manipulate lyx documents (.lyx files) allowed-tools: Bash(lq *)
User Manual
lq is a standalone CLI tool designed to parse, query, and mutate LyX (.lyx) documents.
At its core, lq operates on a simple lifecycle:
- Parse: Reads a
.lyxfile and converts it into a structured Concrete Syntax Tree (CST). The parse is cached by file-content SHA-256 hash in~/.lq/cache/— subsequent reads of the same file deserialize the CST from cache instead of re-parsing. After mutations, the cache is updated with the new CST (write-through), so even back-to-back edits hit the cache after the first parse. - Query: Uses a CSS-like selector engine to find specific nodes in the CST.
- Mutate: Applies changes (insert, set, delete) to the matched nodes.
- Serialize: Converts the modified CST back into a perfectly formatted
.lyxfile.
The LyX-to-CST Mapping
To effectively use the query engine, Users need to understand how LyX syntax maps to the CST nodes:
- Layout Nodes: Structures like
\begin_layout Sectionmap to alayouttag with aSectionargument. Users select them usinglayout[Section]. - Inset Nodes: Structures like
\begin_inset Formulamap to aninsettag with aFormulaargument. Users select them usinginset[Formula]. - Property Nodes: Single-line settings like
\textclass articlemap to property nodes. - Text Nodes: The actual text content inside layouts and insets.
- CST is flat: Layouts like
SectionandStandardare siblings under the document body, not parent-child.
Query Engine
The query engine supports traversing the CST using CSS-like syntax:
- Tags:
layout(e.g., standard paragraphs, sections),inset(e.g., formulas, footnotes, figures),property(e.g.\family roman). - Attributes:
layout[Section],inset[Formula],property[family]. - Descendants: Space-separated paths like
layout[Section] inset[Formula](finds a Formula inside a Section). - Pseudo-classes (must follow a tag e.g.,
layout:contains("text"),inset:first)::first,:last,:nth-child(an+b)(supports formulas like2n+1,odd,even).:not(selector)excludes nodes that have any descendant matching the inner selector (e.g.layout[Standard]:not(inset[Formula])matches Standard layouts that do NOT contain a Formula).:adjacent(selector)matches nodes whose immediately preceding sibling matches the inner selector (skips text/property nodes).:contains("text")searches recursively and case-sensitively node children for text.- Multiple pseudo-classes can be chained (e.g.
:first:contains("foo")).
Context-Aware Strict Validation
lq features strict context validation. It will actively reject mutations that target core CST boundaries like body or document. It will also reject insert commands if you try to put a layout like Section inside an inset like Foot, or if you use an unrecognized layout. Unknown inset types produce a warning but do NOT block the insertion.
Commands
Config
lq init [--layouts-dir <path>] [--refresh <mode>] [--track-changes <on|off>] [--max-cache-entries <n>]- Without flags
- Initializes the user configuration file
~/.lq/config.jsonwith default options. - Or prints the current configuration if it exists.
- Initializes the user configuration file
--layouts-dir <path>: If not provided, auto-detects the highest installed LyX version's layouts directory.--refresh <mode>configures automatic LyX buffer refresh in opened.lyxfiles after mutations:none(default): No refresh. LyX detects external changes via its own polling and prompts the user to reload.reload: Reload the buffer afterlqwrites, fail silently if LyXserver disconnects. Fast, but discards unsaved in-LyX edits.save-reload: Save unsaved edits first, then reload. Preserves everything. Throw an error and abort if LyXserver disconnects
--track-changes <on|off>: Enable or disable tracked changes for all mutation commands. It's on by default, which sets\tracking_changes trueand add an\authorentry in the document header.- Set preserves old text in
\change_deleted+ new in\change_inserted - Delete wraps removed nodes in
\change_deleted - Insert wraps new content in
\change_inserted
- Set preserves old text in
--max-cache-entries <n>: Set the maximum number (default 50) of cached parse results in~/.lq/cache/.
- Without flags
Query
lq schema <file> [--layouts-dir <path>]- Returns a list of all semantically valid layouts for the document's class, as well as global constructs, across 4 categories:
documentLayouts,insetLayouts,insets, andinlineProperties. Global constructs include:- insetLayouts:
Plain Layout - insets:
Note,ERT,Foot,Marginal,Branch,Box,Float,Wrap,Caption,Flex,Phantom,CommandInset,Formula,Graphics,External,Include,listings,Preview,Tabular,space,VSpace,Newline,Newpage,Separator,Line,Quotes,SpecialChar,IPA,IPAMacro,IPADeco,script,Argument,Info,FloatList,Index,Nomenclature,TOC,Ending,Accent - inlineProperties:
change_inserted,change_deleted,change_unchanged
- insetLayouts:
--layouts-dir <path>: overrides the config.
- Returns a list of all semantically valid layouts for the document's class, as well as global constructs, across 4 categories:
lq bib <file> [--search <text>]- Extracts available citation keys from linked
.bibbibliography files and outputs them as JSON. - Only
.bibfiles are supported — other file types (e.g..bst) are ignored. - Each citation includes
key,author,title, andyear. --search <text>: Filters citations by a case-insensitive substring match across all fields. Multiple words are AND'd. Use this to find the right key from a human description without dumping the entire.bibfile.
- Extracts available citation keys from linked
lq dump <file> [<selector>] [--depth <n>]- Outputs the CST as a JSON document.
- Selector: Scope the dump to matching nodes. Omit to dump the whole document.
- Depth:
--depth 0shows only the root node;--depth 1shows direct children;--depth Ndescend N levels from root; omit--depthfor the full CST.
lq read <file> <selector> [--count] [--text-only]- Read matched nodes.
--count: Return only the match count ({"count": {"layout[Section]": 12, "layout[Standard]": 450}}).--text-only(Mutually exclusive with--count): Output the text content of matched nodes with structural annotations. Each matched node gets atag[args]prefix (e.g.layout[Standard]), and insets appear as inline markers (e.g.inset[Foot]). Tracked changes appear as\change_deleted{...}and\change_inserted{...}inline markers. Double newline between nodes.
Mutate
lq set <file> <selector> <new text> [--replace-all] [--find <substring>]- Default behaviour: replaces text content within the targeted nodes while preserves non-text children (insets, properties).
--replace-all: Wipe all children and rebuild from scratch.--find <substring>(Mutually exclusive with--replace-all): Surgical substring replacement — replace only the specified substring within the matched nodes' text. All occurrences are replaced.
lq delete <file> <selector>- Deletes the targeted nodes.
lq undo <file> <selector> [<substring>]- Requires
trackChanges: on. - Reverts tracked changes in matched nodes:
change_deletedblocks are restored (marker removed, text kept);change_insertedblocks are discarded (marker and text removed). - Each marker is undone independently — to fully revert a
set, run undo twice (once for the deleted text, once for the inserted text). <substring>: Text inside thechange_deletedorchange_insertedblock to revert. Omit to revert ALL tracked changes in matched nodes.
- Requires
lq insert <file> <selector> <position> [helper]- Insert new blocks or properties relative to a selector.
- Positions:
before/after: insert a layout as a sibling of the target.prepend/append: insert as children of the target, used for adding insets or text inside a layout.split-after <text>: split a text node right after the exact, case-sensitive substring and insert new content at that point. Only proceeds if the match appears exactly once in current text (text inside\change_deletedblocks is skipped — those represent old/replaced content, not valid targets for new insertions).
- Helpers (must provide exactly one generation strategy):
--layout <name> --text <content>: The safest option. Automatically generates a valid LyX block with the specified text.--cite <key> [--cite-cmd <command>]: Insert a citation inset. Valid--cite-cmdvalues:cite,citet(default),citep,citeauthor,citeyear,citeyearpar,citebyear,footcite,autocite,citetitle,fullcite,footfullcite,nocite,keyonly.--ref <label> [--ref-cmd <command>]: Insert a cross-reference inset. Valid--ref-cmdvalues:ref(default),eqref,pageref,vpageref,vref,nameref,formatted,labelonly.--label <name>: Insert a label inset (CommandInset label) with the given name.--footnote <text>: Insert a footnote inset (Foot) containing aPlain Layoutwith the given text. For complex footnotes (citations, cross-refs, math), use the two-pass approach: create the skeleton with--footnote, then populate withsplit-afterand other helpers.--raw-file <path>: The power-user option for complex structures (e.g. nested formulas, batch insertion, non-default citation/reference params). Read raw LyX syntax from a file and parse it into CST nodes.
Best Practices
Before you start
- Make sure
lqis configured: Always runlq initfirst to set up / confirm configeration. But NEVER change configreation without clear instructions or consent from the user. - Embrace the Git Workflow: You should work in a version-controlled workspace.
git stageandgit restoreis essentially the dry run.git commitfor checkpoints / milestones so you can undo unwanted changes. - Treat LaTeX as Opaque:
lqabstracts away the LaTeX layer. Any raw LaTeX (like equations insideinset[Formula]) is treated as pure string data. Do not try to parse the LaTeX syntax itself; simply target theinset[Formula]node and replace its text content. - Stop for LyXServer errors: If
lqcannot connect LyXServer, stop immediately and ask the user to turn on LyXServer or turn off auto refresh.
Smart query
Navigate large documents strategically with a zoom-in approach:
| You want to… | Use this |
|---|---|
| See the document outline | lq dump <file> --depth 2 |
| Get just section headings | lq read <file> "layout[Section]" --text-only |
| Read body text under a section | lq read <file> "layout[Section]:contains('Theory') layout" --text-only |
| Find a specific paragraph by content | lq read <file> "layout:contains('unique phrase')" --text-only |
| Check selector blast radius & composition | lq read <file> "<selector>" --count |
| Inspect a specific node's CST | lq read <file> "<precise selector>" |
| Deep-debug a node's children | lq dump <file> "<selector>" |
| Find a citation key | lq bib <file> --search "keyword" |
| Revert a tracked change | lq undo <file> "<selector>" "bad text" |
Never: read the whole document at once, bare lq dump, bare lq read "layout" without --text-only, or bare lq bib.
Safe Mutation Workflow
All mutations (insert, set, delete, undo) apply to all matched nodes of a selector. In particular,
insertduplicates the payload once for each matched node.setanddeletecould wipe out the entire document with an overly broad selector (e.g.,layout[Standard]).- If more than 1 node matches, a warning is issued (except for
undo).
When modifying a document, follow this safe workflow:
- Check Schema: Documents vary wildly. A
Beamerpresentation allowsFramelayouts, but anarticledoes not. Runlq schema <file>to know what layouts and insets are legally allowed in the specific document. - Test Blast Radius: Run
lq read <file> <selector> --countfirst. The subtype breakdown (e.g.{"layout[Section]": 8, "layout[Standard]": 120}) tells you the composition — if you meant to target sections but see 120 Standard layouts, your selector is wrong. Narrow before mutating. - Surgical edit (typo fix, rephrase, word change): Use
lq set ... --find "old substring". Keepnew_textscoped to only the changed substring.--findoperates on individual text nodes;new_textis the literal replacement, not merged with surrounding nodes. - Tracked changes: Undo un-wanted changes before appling new changes. Re-editing a node that already has pending tracked changes produces a warning, because the new edit will nest inside existing markers (double-wrap).
HOW-TO
Cross-Referencing: Before inserting a cross-reference, find the exact label names. Labels are stored as text inside
CommandInset labelinsets. Query all labels with:lq read <file> "inset[CommandInset label]"To filter by prefix (e.g., all section labels):
lq read <file> "inset[CommandInset label]:contains('sec:')"Complex references via
--raw-file: When you need non-default params (plural,caps,noprefix,nolink,tuple), write the full inset to a temp file:\begin_inset CommandInset ref LatexCommand vref reference "sec:Section_label" plural "true" caps "false" noprefix "false" nolink "false" tuple "range" \end_insetSee the reference syntax table below for all param defaults.
Citations: Before inserting a citation, find citation keys with:
lq bib <file> --search "author name"Complex citations via
--raw-file: When you needbefore/aftertext, multi-citation lists, orliteralmode, write the full inset to a temp file:\begin_inset CommandInset citation LatexCommand citet key "Einstein1905" literal "false" after "p. 42" \end_insetSee the citation syntax table below for all param defaults.
List Items (Itemize, Enumerate, Description): Do NOT use
\item— it is a LaTeX command, not a.lyxfile format token. LyX never writes\itemto.lyxfiles and would discard it as an "Unknown token". Instead, each list item is a separate paragraph with the list layout:\begin_layout Itemize First bullet point. \end_layout \begin_layout Itemize Second bullet point. \end_layoutTo insert multiple list items at once with
--raw-file:lq insert file.lyx "layout[Standard]:last" after --raw-file /tmp/items.rawFor nested lists, use
\begin_deeper/\end_deeperaround the nested items. For enumerated lists, use\begin_layout Enumerateinstead. For description lists, use\begin_layout Description.
LyX Syntax Reference
Citation Inset (CommandInset citation)
\begin_inset CommandInset citation
LatexCommand citet
key "Einstein1905"
literal "false"
after ""
before ""
\end_inset
| Param | Default | Notes |
|---|---|---|
key |
(required) | BibTeX citation key |
literal |
"false" |
"true" bypasses cite engine formatting |
after |
"" |
Text after citation, e.g. "p. 42" |
before |
"" |
Text before citation, e.g. "see " |
pretextlist |
"" |
Multi-citation preamble |
posttextlist |
"" |
Multi-citation postamble |
Omit params that use the default — LyX only writes non-default values.
Cross-Reference Inset (CommandInset ref)
\begin_inset CommandInset ref
LatexCommand ref
reference "sec:Section_label"
plural "false"
caps "false"
noprefix "false"
nolink "false"
tuple "list"
\end_inset
| Param | Default | Notes |
|---|---|---|
reference |
(required) | Label name |
plural |
"false" |
"Section" → "Sections" |
caps |
"false" |
Capitalize prefix |
noprefix |
"false" |
Hide "Section"/"Figure" prefix |
nolink |
"false" |
No hyperlink |
tuple |
"list" |
"list" or "range" for multi-refs |
The plural/caps/noprefix/nolink/tuple params are LyX-internal — they affect GUI display, not LaTeX output.
Note Insets
The Note inset family has three subtypes. All use \begin_inset Note <subtype>:
| Syntax | LyX UI Name | Output |
|---|---|---|
\begin_inset Note Note |
LyX Note | Internal notes that will not appear in LaTex or PDF output |
\begin_inset Note Comment |
Comment | Internal notes that will appear in LaTex but not in PDF output |
\begin_inset Note Greyedout |
Greyed Out | This note will appear in the output as text in a color |
You should skip these notes when reading the LyX document, and MUST NOT edit existing ones. You can add new notes to store metadata or comments.
More Examples
Use official templates at path/to/lyx/templates/**/*.lyx and official help files at path/to/lyx/Resources/doc/*.lyx to understand more about LyX syntax.