Skip to content

Developer Guide

KaiUR edited this page May 19, 2026 · 13 revisions

Developer Guide

Prerequisites

  • 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, and link.exe
  • CMake 3.16+
  • Ninja build system
  • Git
  • Qt Creator (optional, as IDE)

Building

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 build

The executable is output to build/CatiaMenuWin32.exe.

Qt Creator

  1. Open CMakeLists.txt in Qt Creator
  2. Select a Clang kit configured with the MSVC toolchain
  3. Add -DCMAKE_C_COMPILER=clang-cl to the CMake arguments
  4. Build → Build All

Tip: Launch Qt Creator from a Developer Command Prompt for VS so that rc.exe and the Windows SDK are on the PATH.

CI/CD

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:

  1. Builds with -DCMAKE_C_COMPILER=clang-cl -DVERSION_OVERRIDE=<version>
  2. Authenticode-signs CatiaMenuWin32.exe via PowerShell + signtool.exe (x64)
  3. Commits the incremented build_number.txt back to main as a verified bot commit
  4. 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

Code signing secrets

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

GPG signing secrets

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.

Project Structure

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

Key Source Files

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

Versioning

  • Version is determined from the latest Git tag at CMake configure time
  • build_number.txt increments 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.txt back to main

Releasing

  1. Develop on develop branch
  2. Open a pull request to main
  3. Merge the PR
  4. Tag from main: git tag v1.x.x && git push origin v1.x.x
  5. GitHub Actions builds, signs, and creates the release automatically

Adding a New Setting

  1. main.h — add a field to the Settings struct
  2. settings.c Settings_Load — read it with GetPrivateProfileInt / GetPrivateProfileString; provide a sensible default
  3. settings.c Settings_Save — write it with WritePrivateProfileInt / WritePrivateProfileString
  4. res/resource.rc.in — add a control to the Settings dialog (IDD_SETTINGS = 300)
  5. src/resource.h — add an IDC_* define for the new control
  6. settings.c SettingsDlgProc — populate in WM_INITDIALOG; read back in the IDOK handler
  7. Use the setting wherever needed in the relevant .c file

Adding a New Menu Item

The hamburger menu is built entirely in Window_ShowMenu (window.c) and commands are dispatched in Handle_Command (main.c).

  1. src/resource.h — add #define IDM_MY_ACTION <value> (use the next available number in the IDM_* range)
  2. window.c Window_ShowMenu — add AppendMenu(hSubMenu, MF_STRING, IDM_MY_ACTION, L"My Action") to the appropriate sub-menu
  3. main.c Handle_Command — add a case IDM_MY_ACTION: branch; call a dedicated function for non-trivial logic
  4. For checkmark items, pass MF_CHECKED/MF_UNCHECKED to AppendMenu and toggle g.cfg.* + call Settings_Save in the handler

Tray menu: if the item should also appear in the system tray right-click menu, add it to Window_ShowTrayMenu the same way.


Adding a New Dialog

All modal dialogs are defined in res/resource.rc.in and handled by an INT_PTR CALLBACK dialog proc.

  1. src/resource.h — add #define IDD_MY_DIALOG <value> and any IDC_* control IDs
  2. res/resource.rc.in — add the IDD_MY_DIALOG DIALOGEX block with controls
  3. Appropriate .c file — implement MyDlgProc(HWND, UINT, WPARAM, LPARAM) handling WM_INITDIALOG (populate) and IDOK/IDCANCEL (read back / dismiss)
  4. main.h — declare INT_PTR CALLBACK MyDlgProc(HWND, UINT, WPARAM, LPARAM);
  5. 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.


Adding a New Script Source Type

Script sources are synced in sync.c. To add a new type:

  1. Add fields to Settings in main.h
  2. Add load/save in settings.c
  3. Add a UI in sources.c (following the pattern of extra repos or local dirs)
  4. Add a sync function in sync.c following the pattern of Sync_ExtraRepo
  5. Call it from Sync_Thread and update Sync_LoadManifest to populate folders from the new source at startup

Filter and Sort System

Filter

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.

Sort

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.


Thread Safety

The app has two background threads (sync and updater) plus the UI thread. Three patterns govern cross-thread communication:

Posting to the UI thread

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 button

WM_STATUS_SET frees the heap buffer after displaying it. All other custom messages use simple wParam/lParam values.

Protecting shared state (cs_folders)

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);

Atomic handle ownership (run_process)

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 path

This pattern prevents double-close and double-terminate regardless of which side wins the race.


Repeat-on-Double-Click Architecture

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_LBUTTONDOWNWM_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.


Code Style

  • 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 by clang-analyzer-security.insecureAPI:

    Never use Use instead Notes
    strcpy, wcscpy strcpy_s, wcscpy_s always pass _countof(dest)
    strcat, wcscat strcat_s, wcscat_s always pass _countof(dest)
    strncpy, wcsncpy strncpy_s, wcsncpy_s _s variant guarantees NUL termination
    sprintf, swprintf sprintf_s, swprintf_s pass _countof(buf)
    snprintf, _snwprintf _snprintf_s, _snwprintf_s use _TRUNCATE as the count argument
    fprintf fprintf_s
    vsprintf, vswprintf vsprintf_s, vswprintf_s
    gets fgets
    memcpy memcpy_s pass destSize then count
    memmove memmove_s pass destSize then count
    memset (zeroing secrets) SecureZeroMemory prevents compiler from eliding the zero
  • All GDI painting double-buffered

  • All state in global AppState g struct

  • Heap memory for scripts — use Folder_Alloc / Folder_Free / Folder_Push helpers; always free on every exit path

  • Use COL_BG(), COL_TEXT() etc. — never hardcode RGB values

  • Cross-thread communication via PostMessage only

Clone this wiki locally