Scripts
Settings and per-tool state
Every key pyACAD reads from config.json, the Settings window that edits them, how a tool remembers its own options, and what lives under %APPDATA%\pyACAD.
pyACAD keeps two kinds of settings, in two places, and they never mix:
- pyACAD's own, in one file,
%APPDATA%\pyACAD\config.json: where extensions load from, the theme, engine paths, shortcuts, ribbon hints, the assistant and updates. - Each tool's own, one small JSON file per tool under
%APPDATA%\pyACAD\settings\, written and read throughpyacad.scriptorpyacad.settings.
Both are per Windows user, not per drawing and not per machine.
config.json#
The file is a single JSON object. It does not need to exist: a fresh install has none, and every key has a default. Unknown keys are ignored, and the runtime's own writers keep them, so you can leave notes or keys for a later version in it.
Who writes it#
| Writer | Keys it touches |
|---|---|
| The Settings window (pyACAD panel) | theme, extensions, pyacadlib,
cpython, shortcuts.allowBareKeys, the four
ribbon keys, ai, updates |
| The Shortcuts editor (pyACAD panel) | shortcuts.bindings only |
| Settings' Skip this version button | updates.skip only, at once |
pyacad extensions add <dir> (the command line) |
extensions |
| The repository's developer deploy script | runtime<year>, extensions, pyacadlib |
| You, in a text editor | Anything |
The installer never writes it. The runtime's writers read the file, change only their own keys, write the result to a temporary file beside it and swap it in, so a crash mid-write cannot leave a half-written file. A key set back to its default is removed rather than written out, so a file only ever holds what someone changed.
If config.json does not parse (a stray comma after a hand edit), the runtime
logs Failed to load config ... - using defaults. and carries on with every
default: no extra extension roots, the detected theme, no shortcut overrides. The Settings
window and the Shortcuts editor then refuse to write over it (is not valid JSON, so nothing
was saved. Fix the file or delete it.), because saving would throw away everything the
file held. pyacad extensions add is less careful: it warns treating as
empty and writes a file holding only extensions. Fix the JSON first.
When changes take effect#
- On Reload:
extensions,theme, theshortcutsandribbonsections. The file is read again at every Reload. (A newly added or removed*.libfolder is the exception: that needs a restart.) - On the next AutoCAD start:
pyacadlib,cpython,runtime<year>andautomatedBoot. The engines and the loader read these once. - When next used:
ai(the next request), andupdates(the next start's check).
Every key#
Loading and engines#
| Key | Type and default | Meaning |
|---|---|---|
extensions | array of folder paths; none | Extension roots: folders holding *.extension (and *.lib)
folders. They are scanned first, in order, then the installer's defaults that exist:
%APPDATA%\pyACAD\extensions, then
%PROGRAMDATA%\pyACAD\extensions. The machine-wide folder is used only when
nobody but administrators can write to it; list it here to load it anyway. |
pyacadlib | folder path; the runtime's own
pyacadlib |
The folder containing the pyacad package. If the path does not exist
the runtime logs it and falls back to the installed copy. A wrong value here is the one
setting that can stop every script importing pyacad. |
cpython | python3XX.dll path or a Python install
folder; detected |
The CPython that engine: cpython bundles run on. When absent: the
PYACAD_CPYTHON environment variable (same forms), then the newest 64-bit
Python 3.7 to 3.13 registered for the user, then for the machine. A folder is searched for
its highest python3XX.dll. |
runtime2026 (one per release year) | folder path; none | Read by the loader, not the runtime, at AutoCAD start: the runtime folder for that
release. The order is the PYACAD_RUNTIME environment variable, this key,
%APPDATA%\pyACAD\<year>\runtime, then
%PROGRAMDATA%\pyACAD\<year>\runtime. Used for development
builds. |
automatedBoot | string; absent | "full" makes an AutoCAD started through COM automation (no one at the
screen) build the ribbon, hooks, shortcuts and notices as a normal session does. Absent,
those stages are skipped there. |
Appearance and the keyboard#
| Key | Type and default | Meaning |
|---|---|---|
theme | "light" or "dark"; absent |
Forces pyACAD's theme. Absent (or any other value), pyACAD follows AutoCAD's
COLORTHEME. See Light or dark. |
shortcuts.allowBareKeys | boolean; false |
Allows chords without Ctrl or Alt. A bare key is then swallowed everywhere in AutoCAD outside a text box. |
shortcuts.bindings | object; empty | Bundle key to chord string, or null to disable an author's default.
See Shortcuts and keytips. |
aliases | object; empty | Bundle key to your own short names for that tool ("BXL" or
"BXL, B2X"), replacing the bundle's alias:; null
drops them. See Aliases. |
ribbon.showShortcutMarker | boolean; true |
Appends a marker to the caption of a button that has a chord. |
ribbon.shortcutMarker | string; "●"
(U+25CF) |
That marker. A value starting with \n puts it on its own line. Empty
means the default glyph, never "off". |
ribbon.showConfigMarker | boolean; false |
Appends a marker to the caption of a button whose bundle has a
config.py (a Shift+Click action). |
ribbon.configMarker | string; U+1F845, a wide upward arrow | That marker. Empty means the default. |
ribbon.configDot | boolean | The older name for showConfigMarker, read only when that key is
absent. The Settings window removes it on save. |
The assistant and updates#
| Key | Type and default | Meaning |
|---|---|---|
ai.provider | "anthropic" or "openai";
"anthropic" |
"openai" means any OpenAI-compatible endpoint. |
ai.baseUrl | string; the provider's public endpoint | For Azure OpenAI, Ollama or a local server. |
ai.model | string; claude-opus-5 or
gpt-4o | Any model name the endpoint accepts. |
ai.maxTokens | positive integer; 8192 |
The longest answer. |
updates.check | boolean; true |
Ask GitHub once a day whether a newer release exists. |
updates.installOnClose | boolean; true |
Download a newer release in the background and install it when AutoCAD closes. |
updates.skip | version string; none | A release the user chose to skip. |
The assistant's API key is not in config.json: it is encrypted for the
current Windows user in secrets.json beside it. See
AI assistant.
Read but not used#
The runtime also parses panes.assignments, panes.extraSlots and
layout.<dialog>.<part>, but nothing in this release acts on them:
dock panes are AutoCAD palettes that remember their own size and position, and no window
stores a layout width yet. Leave them alone if they are in your file.
A value of the wrong JSON type is ignored and the default used, with no log line.
"allowBareKeys": "true" (a string) leaves bare keys off; it has to be
true. Paths are JSON strings, so every backslash is doubled.
A complete example#
{
"extensions": [
"D:\\CAD\\Extensions",
"\\\\fileserver\\cad\\pyacad-office"
],
"theme": "dark",
"cpython": "C:\\Python312",
"shortcuts": {
"allowBareKeys": false,
"bindings": {
"pyACAD.tab/Output.panel/Batch_Plot.pushbutton": "Ctrl+Shift+P",
"MyTools.tab/Blocks.panel/Count.pushbutton": null
}
},
"aliases": {
"pyACAD.tab/Output.panel/Batch_Plot.pushbutton": "BP"
},
"ribbon": {
"showShortcutMarker": true,
"showConfigMarker": true
},
"ai": {
"provider": "openai",
"baseUrl": "http://localhost:11434/v1",
"model": "qwen2.5-coder"
},
"updates": {
"installOnClose": false
}
}The Settings window#
The Settings button on the pyACAD panel (command PYSETTINGS)
opens pyACAD settings, which edits everything in config.json a person is
meant to change. Each section says what its change costs.
| Section | Fields | Takes effect |
|---|---|---|
| Appearance | Theme (Follow AutoCAD, Light, Dark); the shortcut marker on or off and its glyph; the Shift+Click marker on or off and its glyph. | Reload |
| Shortcuts | Allow shortcuts without Ctrl or Alt. | Reload |
| Where extensions are loaded from | The extensions list, with Add (a folder picker) and Remove. |
Reload |
| Engines | pyacadlib folder; CPython. Empty lets pyACAD resolve them. | Restart |
| Assistant | Provider, base URL, model, API key (with Test and Remove), longest answer. | Next request |
| Updates | Status, Check now, Release page, Skip this version; check daily; install on close. | Next start |
On Save, if either engine path changed it toasts Settings saved, The engine paths changed, so AutoCAD needs restarting. Otherwise it asks Reload pyACAD now so the changes show? and reloads on Yes. Cancel or closing the window writes nothing.
The window deliberately does not edit shortcuts.bindings: which tool has which
chord belongs to the Shortcuts editor, and two windows rewriting one section would fight.
The field is labelled CPython executable, but what the engine loads is the
python3XX.dll. Point it at the DLL or at the Python install folder that
contains it, not at python.exe.
Per-tool settings#
Most tools eventually grow an option: an output folder, a tolerance, a choice someone wants remembered. A tool's settings are a Python dict that round-trips through a JSON file, and there are two ways to name that file.
The easy way: pyacad.script#
Inside a bundle's script.py or config.py, let pyACAD name the
file after the bundle:
from pyacad import forms, script
DEFAULTS = {'folder': '', 'pattern': '{drawing} - {layout}.pdf'}
config = script.get_config(DEFAULTS) # always a complete dict
folder = forms.pick_folder('Folder for the PDFs', config['folder'] or None)
if folder:
config['folder'] = folder
script.save_config(config)| Function | Does |
|---|---|
script.get_config(defaults) |
This tool's saved values merged over defaults. Never raises. |
script.save_config(values) |
Writes values as the tool's whole file. Keys you leave out are
gone. |
script.reset_config() |
Deletes the tool's file, so the next get_config returns the
defaults. |
script.bind() |
Pins the tool's identity for code that runs after the script returns (see below). |
The file is %APPDATA%\pyACAD\settings\cfg_<slug>.json, where the slug
is the tool's bundle key lowercased with every run of other characters turned into one
underscore. Batch Plot, whose bundle key is
pyACAD.tab/Output.panel/Batch_Plot.pushbutton, stores its choices in
cfg_pyacad_tab_output_panel_batch_plot_pushbutton.json. Because
script.py and config.py share the bundle key, they share the file,
which is the whole Shift+Click convention (see
Click actions).
The key comes from the folder path (leading NN_ prefixes are dropped, so
renumbering is safe). Rename Batch_Plot.pushbutton or move it to another panel
and the tool looks for a different file. The old file stays on disk, unread.
Callbacks that outlive the script#
get_config and save_config ask the runtime "which tool is
running?" at the moment they are called, and the runtime answers with the most recent
command, even after it finished. Inside script.py that is right. In a modeless
window's click handler, a dock pane handler or a timer it is wrong: the user may have run
another tool since, and you would read or overwrite its settings. Take the answer
while it is still correct:
from pyacad import script
me = script.bind() # at the top of script.py
def on_closed(sender, args): # fires long after script.py returned
me.save_config({'width': sender.Width})A BoundScript has the same get_config, save_config,
reset_config, store_data, load_data and
data_exists methods, all pinned to the tool that called
bind().
The explicit way: pyacad.settings#
Hooks, startup.py and shared library code have no bundle of their own (and
the script functions would answer for whichever tool ran last), so they name the
file themselves. pyacad.settings is pure standard library: it imports no AutoCAD
API and works on either engine, in a unit test, and outside AutoCAD.
| Function | Does |
|---|---|
settings.path_for(tool) |
%APPDATA%\pyACAD\settings\<tool>.json; the file need not
exist. |
settings.merge(defaults, stored) |
Defaults overlaid with stored values; keys not in defaults are
dropped. |
settings.load(tool, defaults) |
The saved values merged over defaults. Never raises. |
settings.save(tool, values) |
Writes indented, key-sorted JSON, creating the folder when needed. |
from pyacad import settings, toast
DEFAULTS = {'remind_purge': True}
if settings.load('mytools_hooks', DEFAULTS)['remind_purge']:
toast.info('Saved', 'Remember to purge before you issue the drawing.')Pick a key that is unique across every extension a user might install: prefix it with your
extension's name. The cfg_ prefix is what pyacad.script uses, so
keep clear of it.
How load and save behave#
- Load never raises. A missing file, an empty file, half-written JSON or a file someone edited into a list all return a fresh copy of the defaults, so a bad preferences file can never stop a tool from running. The next save repairs it.
- The keys come from your defaults. The result holds exactly the keys in
defaults: values from the file where present, defaults elsewhere. A key you remove from a later version of the tool is dropped, and a key you add arrives at its default for everyone. Index the result directly; there is nothing to check. - Renaming a key loses that one value. The old name is dropped and the new one starts at its default.
- Save is atomic. It writes a temporary file beside the target and replaces the target with it, so a crash mid-save leaves the previous file intact.
Data, as opposed to settings#
For things a tool accumulates rather than options a user picks (a list of recent paths, a
cache, results per drawing), pyacad.script has a key/value store under
%APPDATA%\pyACAD\data\:
from pyacad import script
runs = script.load_data('runs', 0, per_document=True) + 1
script.store_data('runs', runs, per_document=True)Each call reads and rewrites one JSON object per tool (with
per_document=True, one per tool and drawing, named after the drawing's file plus
a short hash of its full path). A file that will not parse is copied aside as
.corrupt before the store overwrites it. Per-document data needs an open drawing
and raises RuntimeError without one.
What lives under %APPDATA%\pyACAD#
%APPDATA%\pyACAD\
config.json pyACAD's settings (this page)
secrets.json the assistant's API key, encrypted for your Windows account
settings\ per-tool settings: cfg_*.json and your own keys
data\ per-tool data from script.store_data
logs\
pyACAD.log the runtime log
loader.log the loader's log: which runtime it found
extensions\ the per-user extension root (shipped and AI-made extensions)
2026\runtime\ the runtime the per-user installer put there, one per release
cli\ pyacad.exe, the command line
webview2\ the output window's browser profile
updates\ update state, downloaded installers and their logs
runs\ one folder per pyacad run
last-upgrade.json what the last update kept of your workAn update replaces only the runtime folders, cli and the shipped
extensions\pyACAD.extension; config.json, secrets.json,
settings, data, the logs and every extension of your own are kept. Deleting config.json is a safe reset of
pyACAD's own settings; deleting a file under settings resets one tool.
Logs#
There is no debugger inside AutoCAD. The log is the debugger.
logs\pyACAD.log: everything the runtime does, one timestamped line each, tagged[INFO ]or[ERROR]. Boot and Reload, which extension roots were scanned, skipped bundles and why, theme detection, commands registered, shortcut problems, every script run and every traceback. A hook's orstartup.py'sprint()lands here too, prefixed[hook MyTools:doc-saved]or[startup MyTools].logs\loader.log: the small loader AutoCAD loads first. If the ribbon never appears, this says whether the runtime was found and where.
A script writes to the runtime log through its logger:
from pyacad import script
log = script.get_logger()
log.info('exporting %d layouts' % 12) # [INFO ] [info] [Batch Plot] exporting 12 layouts
log.warning('layout A3-02 has no page setup') # also shown in an open output window
log.set_level('debug') # debug lines are dropped until you do this
log.debug('page setup lookup took 40 ms')Lines are labelled with the tool's title, or with hook ... and
startup ... for those scripts. Warnings and errors also appear in the output
window when one is already open; the logger never opens one by itself. The logs are plain
appended text and are not rotated: delete them when they grow.