-
Notifications
You must be signed in to change notification settings - Fork 0
Developer Guide
-
LLVM/Clang 17+ — C compiler (
clang-cl.exe, the MSVC-compatible Clang driver) -
Visual Studio 2019+ (or Build Tools for Visual Studio) — provides Windows SDK,
rc.exe, andlink.exe - CMake 3.16+
- Ninja build system
- Git
- Qt Creator (optional, as IDE)
Open a Developer Command Prompt for VS, then:
git clone https://github.com/KaiUR/CatiaMenuWin32
cd CatiaMenuWin32
cmake -S . -B build -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang-cl
cmake --build buildThe executable is output to build/CatiaMenuWin32.exe.
- Open
CMakeLists.txtin Qt Creator - Select a Clang kit configured with the MSVC toolchain
- Add
-DCMAKE_C_COMPILER=clang-clto the CMake arguments - Build → Build All
Tip: Launch Qt Creator from a Developer Command Prompt for VS so that
rc.exeand the Windows SDK are on the PATH.
The workflow (.github/workflows/release.yml) runs on windows-latest using:
-
ilammy/msvc-dev-cmd— sets up the MSVC environment (Windows SDK,rc.exe,link.exe) -
LLVM/Clang (pre-installed on
windows-latest) as the C compiler - Ninja as the build backend
On a tagged release (v*) the workflow additionally:
- Builds with
-DCMAKE_C_COMPILER=clang-cl -DVERSION_OVERRIDE=<version> -
Authenticode-signs
CatiaMenuWin32.exevia PowerShell +signtool.exe(x64) - Commits the incremented
build_number.txtback tomainas a verified bot commit - Imports the GPG key, deletes the base tag, creates a GPG-signed tag with the full build number (e.g.
v1.2.0.31), and publishes the GitHub Release
The following secrets must be set in Settings → Secrets → Actions:
| Secret | Description |
|---|---|
CERTIFICATE |
Base64-encoded PFX file |
PASSWORD |
PFX password |
CERTHASH |
SHA1 thumbprint of the certificate |
CERTNAME |
Common name of the certificate |
The following secrets are used by crazy-max/ghaction-import-gpg@v6 to sign release tags:
| Secret | Description |
|---|---|
GPG_PRIVATE_KEY |
ASCII-armored GPG private key. Export with: gpg --armor --export-secret-keys KEY_ID
|
GPG_PASSPHRASE |
Passphrase protecting the GPG private key |
The imported key is configured as the git signing key; every release tag is created with git tag -s, producing a verified tag on GitHub. The committer identity baked into the key must match KaiUR / kairathjen@yahoo.com as set in the workflow.
src/ C source and header files
res/ Resource files (icons, manifest, resource.rc.in, version.h.in)
docs/ GitHub Pages documentation
.github/ GitHub Actions workflows and issue templates
| File | Purpose |
|---|---|
main.c / main.h
|
Entry point, WndProc, AppState struct |
window.c |
Window creation, menu, toolbar, layout |
tabs.c |
Custom tab bar, script buttons, filter |
paint.c |
GDI painting, script button rendering, tooltips |
sync.c |
GitHub sync thread, manifest, offline cache |
github.c |
HTTPS requests, JSON parsing, SHA verification |
runner.c |
Script execution, Python detection |
meta.c |
Script header metadata parsing |
settings.c |
Settings load/save, Settings dialog |
sources.c |
Script Sources dialog |
prefs.c |
Favourites, hidden scripts, notes, run counts |
help.c |
In-app help window |
updater.c |
Update checker and auto-update |
quickbar.c |
Floating Quick Launch Bar |
- Version is determined from the latest Git tag at CMake configure time
-
build_number.txtincrements by 1 on every CMake configure (local and CI) - Local builds show a
(local)suffix and skip the update check - CI workflow: tag push → build → sign → release → commit
build_number.txtback to main
- Develop on
developbranch - Open a pull request to
main - Merge the PR
- Tag from
main:git tag v1.x.x && git push origin v1.x.x - GitHub Actions builds, signs, and creates the release automatically
-
main.h— add a field to theSettingsstruct -
settings.cSettings_Load— read it withGetPrivateProfileInt/GetPrivateProfileString; provide a sensible default -
settings.cSettings_Save— write it withWritePrivateProfileInt/WritePrivateProfileString -
res/resource.rc.in— add a control to the Settings dialog (IDD_SETTINGS = 300) -
src/resource.h— add anIDC_*define for the new control -
settings.cSettingsDlgProc— populate inWM_INITDIALOG; read back in theIDOKhandler - Use the setting wherever needed in the relevant
.cfile
The hamburger menu is built entirely in Window_ShowMenu (window.c) and commands are dispatched in Handle_Command (main.c).
-
src/resource.h— add#define IDM_MY_ACTION <value>(use the next available number in theIDM_*range) -
window.cWindow_ShowMenu— addAppendMenu(hSubMenu, MF_STRING, IDM_MY_ACTION, L"My Action")to the appropriate sub-menu -
main.cHandle_Command— add acase IDM_MY_ACTION:branch; call a dedicated function for non-trivial logic - For checkmark items, pass
MF_CHECKED/MF_UNCHECKEDtoAppendMenuand toggleg.cfg.*+ callSettings_Savein the handler
Tray menu: if the item should also appear in the system tray right-click menu, add it to
Window_ShowTrayMenuthe same way.
All modal dialogs are defined in res/resource.rc.in and handled by an INT_PTR CALLBACK dialog proc.
-
src/resource.h— add#define IDD_MY_DIALOG <value>and anyIDC_*control IDs -
res/resource.rc.in— add theIDD_MY_DIALOG DIALOGEXblock with controls -
Appropriate
.cfile — implementMyDlgProc(HWND, UINT, WPARAM, LPARAM)handlingWM_INITDIALOG(populate) andIDOK/IDCANCEL(read back / dismiss) -
main.h— declareINT_PTR CALLBACK MyDlgProc(HWND, UINT, WPARAM, LPARAM); -
Caller — open with
DialogBox(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_MY_DIALOG), g.hwnd, MyDlgProc)
For theme-aware dialogs: handle WM_ERASEBKGND with a COL_BG() brush, and call Window_ApplyDarkMode(hwnd) + Window_ApplyThemeToChildren(hwnd) in WM_INITDIALOG.
Script sources are synced in sync.c. To add a new type:
- Add fields to
Settingsinmain.h - Add load/save in
settings.c - Add a UI in
sources.c(following the pattern of extra repos or local dirs) - Add a sync function in
sync.cfollowing the pattern ofSync_ExtraRepo - Call it from
Sync_Threadand updateSync_LoadManifestto populate folders from the new source at startup
The search box posts EN_CHANGE to MainWndProc. The handler copies the edit text to g.filter_text and calls Tabs_ApplyFilter() → Tabs_RebuildButtons(). For each non-hidden script, Tabs_ScriptMatchesFilter checks whether name or meta.purpose contains g.filter_text (case-insensitive substring). When the filter is empty all scripts are shown.
SortMode is stored in g.cfg.sort_mode. Tabs_ApplySort(fi) sorts g.folders[fi].scripts[] in-place with qsort:
| Mode | Order |
|---|---|
SORT_ORDER |
GitHub API / disk order (no-op) |
SORT_ALPHA |
A-Z by name (case-insensitive) |
SORT_DATE |
Descending by meta.date
|
SORT_MOST_USED |
Descending by run_count
|
Call Tabs_RebuildButtons after Tabs_ApplySort to update the UI.
The app has two background threads (sync and updater) plus the UI thread. Three patterns govern cross-thread communication:
Never call Win32 UI functions from a background thread. Use PostMessage to marshal work back to the UI thread:
PostStatus(L"Sync done."); // status bar update
PostMessage(g.hwnd, WM_SYNC_DONE, (WPARAM)result, 0); // sync finished
PostMessage(g.hwnd, WM_SCRIPT_STARTED, 0, 0); // enables Stop button
PostMessage(g.hwnd, WM_SCRIPT_STOPPED, 0, 0); // disables Stop buttonWM_STATUS_SET frees the heap buffer after displaying it. All other custom messages use simple wParam/lParam values.
g.folders[] and g.folder_count are written by the sync thread and read by the UI thread. Every access must be guarded:
EnterCriticalSection(&g.cs_folders);
// read or write g.folders[] here
LeaveCriticalSection(&g.cs_folders);The running-script process handle is shared between Runner_Thread (writer) and Runner_Stop / MainWndProc (readers). InterlockedExchangePointer guarantees exactly one caller owns the handle:
// Runner_Thread — store a duplicate after CreateProcess:
DuplicateHandle(..., pi.hProcess, ..., &dup, ...);
InterlockedExchangePointer((void **)&g.run_process, dup);
// Runner_Stop — atomically claim it:
HANDLE h = (HANDLE)InterlockedExchangePointer((void **)&g.run_process, NULL);
if (h) { TerminateProcess(h, 1); CloseHandle(h); }
// Runner_Thread cleanup — take back if Stop hasn't claimed it:
HANDLE old = (HANDLE)InterlockedExchangePointer((void **)&g.run_process, NULL);
if (old) CloseHandle(old); // normal completion pathThis pattern prevents double-close and double-terminate regardless of which side wins the race.
State is stored in AppState g (see main.h):
| Field | Purpose |
|---|---|
g.repeat_mode |
true while a script is looping |
g.repeat_fi / repeat_si |
Folder / script index of the script to repeat |
g.suppress_lbuttonup |
Suppresses the extra WM_LBUTTONUP that follows WM_LBUTTONDBLCLK
|
Double-click sequence: WM_LBUTTONDOWN → WM_LBUTTONUP (first click, runs script) → WM_LBUTTONDBLCLK (sets repeat state) → WM_LBUTTONUP (suppressed via suppress_lbuttonup).
Repeat trigger: WM_SCRIPT_STOPPED in MainWndProc checks g.repeat_mode and calls Runner_Run(g.repeat_fi, g.repeat_si).
Cancellation: Escape (WM_KEYDOWN) in both MainWndProc and QuickBarProc clears g.repeat_mode and calls Runner_Stop() to terminate any running script. Single-click same script (Handle_Command), single-click different script, or ■ Stop all clear g.repeat_mode before any run is triggered.
Quick Bar: CS_DBLCLKS is set on the bar's window class so it receives WM_LBUTTONDBLCLK. QuickBarProc mirrors the main-window logic using QB_GetFav to resolve the hit index to fi/si.
Visual: Paint_ScriptButton receives bool repeat and bool running. Priority: repeat (amber) > running (green) > hot (blue). When repeat: amber (COL_WARN) border, accent bar, ↻ arrow, text. When running: green (COL_SUCCESS) border, accent bar, text. QB_Paint mirrors this per-button.
Console-mode guard: Both BtnSubclassProc and QuickBarProc check g.cfg.show_console and show a status message instead of activating repeat, because console-mode scripts do not post WM_SCRIPT_STOPPED. The running highlight also only appears in background mode for the same reason.
-
C11, Win32 API only — no external libraries
-
Unicode throughout —
WCHAR,L""literals,_snwprintf_s -
Memory-safe functions only — always use
_s(C11 Annex K) or bounded variants; never use functions flagged byclang-analyzer-security.insecureAPI:Never use Use instead Notes strcpy,wcscpystrcpy_s,wcscpy_salways pass _countof(dest)strcat,wcscatstrcat_s,wcscat_salways pass _countof(dest)strncpy,wcsncpystrncpy_s,wcsncpy_s_svariant guarantees NUL terminationsprintf,swprintfsprintf_s,swprintf_spass _countof(buf)snprintf,_snwprintf_snprintf_s,_snwprintf_suse _TRUNCATEas the count argumentfprintffprintf_svsprintf,vswprintfvsprintf_s,vswprintf_sgetsfgetsmemcpymemcpy_spass destSizethencountmemmovememmove_spass destSizethencountmemset(zeroing secrets)SecureZeroMemoryprevents compiler from eliding the zero -
All GDI painting double-buffered
-
All state in global
AppState gstruct -
Heap memory for scripts — use
Folder_Alloc/Folder_Free/Folder_Pushhelpers; alwaysfreeon every exit path -
Use
COL_BG(),COL_TEXT()etc. — never hardcode RGB values -
Cross-thread communication via
PostMessageonly
Getting Started
Using the App
Scripts
Reference
Development
Legal