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 through pyacad.script or pyacad.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#

WriterKeys 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 editorAnything

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.

Invalid JSON: the runtime reads defaults and refuses to save

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, the shortcuts and ribbon sections. The file is read again at every Reload. (A newly added or removed *.lib folder is the exception: that needs a restart.)
  • On the next AutoCAD start: pyacadlib, cpython, runtime<year> and automatedBoot. The engines and the loader read these once.
  • When next used: ai (the next request), and updates (the next start's check).

Every key#

Loading and engines#

KeyType and defaultMeaning
extensionsarray 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.
pyacadlibfolder 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.
cpythonpython3XX.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.
automatedBootstring; 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#

KeyType and defaultMeaning
theme"light" or "dark"; absent Forces pyACAD's theme. Absent (or any other value), pyACAD follows AutoCAD's COLORTHEME. See Light or dark.
shortcuts.allowBareKeysboolean; false Allows chords without Ctrl or Alt. A bare key is then swallowed everywhere in AutoCAD outside a text box.
shortcuts.bindingsobject; empty Bundle key to chord string, or null to disable an author's default. See Shortcuts and keytips.
aliasesobject; 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.showShortcutMarkerboolean; true Appends a marker to the caption of a button that has a chord.
ribbon.shortcutMarkerstring; "●" (U+25CF) That marker. A value starting with \n puts it on its own line. Empty means the default glyph, never "off".
ribbon.showConfigMarkerboolean; false Appends a marker to the caption of a button whose bundle has a config.py (a Shift+Click action).
ribbon.configMarkerstring; U+1F845, a wide upward arrow That marker. Empty means the default.
ribbon.configDotboolean The older name for showConfigMarker, read only when that key is absent. The Settings window removes it on save.

The assistant and updates#

KeyType and defaultMeaning
ai.provider"anthropic" or "openai"; "anthropic" "openai" means any OpenAI-compatible endpoint.
ai.baseUrlstring; the provider's public endpoint For Azure OpenAI, Ollama or a local server.
ai.modelstring; claude-opus-5 or gpt-4oAny model name the endpoint accepts.
ai.maxTokenspositive integer; 8192 The longest answer.
updates.checkboolean; true Ask GitHub once a day whether a newer release exists.
updates.installOnCloseboolean; true Download a newer release in the background and install it when AutoCAD closes.
updates.skipversion 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.

Types are checked, not coerced

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#

%APPDATA%\pyACAD\config.json
{
  "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.

SectionFieldsTakes 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
ShortcutsAllow shortcuts without Ctrl or Alt.Reload
Where extensions are loaded from The extensions list, with Add (a folder picker) and Remove. Reload
Enginespyacadlib 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 CPython field takes a DLL or a folder

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:

script.py
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)
FunctionDoes
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).

Renaming the bundle folder starts from the defaults

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:

script.py
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.

FunctionDoes
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.
hooks\doc-saved.py
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\:

script.py
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#

folders
%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 work

An 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 or startup.py's print() 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:

script.py
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.