Skip to the content.

Developer Guide

Contents


Prerequisites

Tool Version Purpose
LLVM 17+ C compiler (Clang)
CMake 3.16+ Build system
Ninja any Build backend
Visual Studio 2019+ Windows SDK and linker (rc.exe, link.exe)
Qt Creator any IDE (optional)
Git any Version control

Note: Visual Studio (or Build Tools for Visual Studio) is required to provide rc.exe (resource compiler) and the Windows SDK headers. LLVM/Clang is the C compiler; Visual Studio provides the rest of the Windows toolchain.


Building from Source

Command Line

Open a Developer Command Prompt for VS (or run ilammy/msvc-dev-cmd equivalent), 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.

Note: res/app_icon.ico must exist before building. It is included in the repository.

Qt Creator

  1. Open CMakeLists.txt in Qt Creator
  2. Select a Clang kit (configured against the MSVC toolchain)
  3. Add -DCMAKE_C_COMPILER=clang-cl to the CMake arguments
  4. Configure the project
  5. Build → Build All (Ctrl+B)

CMake Variables

Variable Default Description
VERSION_OVERRIDE unset Override version (used by CI workflow)
CMAKE_BUILD_TYPE Debug Release strips debug symbols

When VERSION_OVERRIDE is not set, CMake reads the latest git tag via git tag --sort=-creatordate and uses it as the base version. The build number is always incremented from build_number.txt.


Project Structure

CatiaMenuWin32/
├── src/                    Source files
│   ├── main.h              Central header — all structs, types, prototypes
│   ├── main.c              Entry point, WinMain, MainWndProc, message handling
│   ├── resource.h          All #define IDs for controls, menus, dialogs
│   ├── window.c            Window creation, tab bar, tray, menu popup
│   ├── tabs.c              Tab switching, script button creation, scroll panel
│   ├── paint.c             All GDI rendering — toolbar, buttons, tooltip
│   ├── github.c            HTTPS requests, cert validation, SHA computation
│   ├── sync.c              GitHub sync thread, local dir scanning, manifest
│   ├── runner.c            Script execution, Python detection, Update Deps
│   ├── meta.c              Script header parser (Purpose, Author, etc.)
│   ├── help.c              In-app help window (TreeView + RichEdit)
│   ├── prefs.c             Favourites, hidden scripts, run counts, notes
│   ├── sources.c           Sources dialog — extra repos and local folders
│   ├── settings.c          Settings load/save, Settings dialog, About dialog
│   ├── updater.c           Update checker — GitHub releases API
│   └── quickbar.c          Floating Quick Launch Bar
├── res/
│   ├── resource.rc.in      Resource script template (CMake substitutes version)
│   ├── version.h.in        Version header template
│   ├── app.manifest        Application manifest (DPI awareness, ComCtl32 v6)
│   └── app_icon.ico        Application icon
├── docs/                   Documentation
├── .github/
│   └── workflows/
│       └── release.yml     GitHub Actions CI/CD workflow
├── CMakeLists.txt          Build configuration
├── build_number.txt        Auto-incremented build counter
├── README.md               Project overview
├── LICENSE.txt             MIT license
└── CONTRIBUTORS.md         Auto-generated from git history

Versioning System

Version format: major.minor.patch.build — e.g. v1.2.0.31

Local builds

CMake reads the latest tag via git tag --sort=-creatordate, extracts major.minor.patch, increments build_number.txt, and generates version.h. The resulting binary shows e.g. v1.2.0.32.

CI builds

The workflow passes -DVERSION_OVERRIDE=1.2.0 to CMake (extracted from your tag), CMake increments build_number.txt, the workflow commits the new number back to main, then creates the final tag v1.2.0.31 matching the binary exactly.

version.h (generated)

#define VERSION_MAJOR     1
#define VERSION_MINOR     2
#define VERSION_PATCH     0
#define VERSION_BUILD     31
#define IS_LOCAL_BUILD    1   /* 0 in CI builds */
#define VERSION_STRING_W  L"1.2.0.31"
#define VERSION_DISPLAY_W L"1.2.0.31"

Adding a New Setting

  1. main.h — add field to the Settings struct
  2. settings.c — add GetPrivateProfileInt/String in Settings_Load and WritePrivateProfile* in Settings_Save
  3. res/resource.rc.in — add control to the Settings dialog (IDD_SETTINGS = 300)
  4. src/resource.h — add #define IDC_* for the new control
  5. settings.c SettingsDlgProc — handle in WM_INITDIALOG (populate) and IDOK (read back)
  6. 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 a #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; keep it short — call a dedicated function if the logic is non-trivial
  4. If the item should show a checkmark, pass the current state to AppendMenu with MF_CHECKED / MF_UNCHECKED flags, 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 in 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 in the appropriate .c file.

  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 at minimum 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)

Theme-aware dialogs: to match the app theme, handle WM_ERASEBKGND and return a brush of COL_BG(), and call Window_ApplyDarkMode(hwnd) and Window_ApplyThemeToChildren(hwnd) in WM_INITDIALOG.


Filter and Sort System

Filter

The search box (hwnd_search, IDC_SEARCH) posts EN_CHANGE notifications to MainWndProc. The handler copies the edit text to g.filter_text and calls Tabs_ApplyFilter().

Tabs_ApplyFilterTabs_RebuildButtons — which calls Tabs_ScriptMatchesFilter for every non-hidden script. A script is shown only when the filter is empty or its name / meta.purpose contains g.filter_text (case-insensitive substring match via wcsstr).

Clearing the search box sets g.filter_text[0] = L'\0' and rebuilds — all non-hidden scripts reappear.

Sort

SortMode values are stored in g.cfg.sort_mode (global default, settable in Settings) and in each ScriptFolder.sort_mode (per-tab override, not yet exposed in UI).

Tabs_ApplySort(fi) is called after every sync and after each manual sort change. It calls qsort on g.folders[fi].scripts[] with the matching comparator:

SortMode Comparator Notes
SORT_ORDER (none) GitHub API / disk order — qsort not called
SORT_ALPHA _wcsicmp(a->name, b->name) Case-insensitive A-Z
SORT_DATE wcscmp(b->date, a->date) Descending: newest first
SORT_MOST_USED b->run_count - a->run_count Descending: most runs first

After Tabs_ApplySort, call Tabs_RebuildButtons (or Tabs_Switch) to reflect the new order in the UI.


Adding a New Script Source Type

Script sources are synced in sync.c:

To add a new source type:

  1. Add fields to Settings in main.h
  2. Add load/save in settings.c
  3. Add a UI in sources.c
  4. Add a sync function in sync.c following the pattern of Sync_ExtraRepo
  5. Call it from Sync_Thread and Sync_LoadManifest

CI/CD Workflow

The workflow (.github/workflows/release.yml) runs on windows-latest using LLVM/Clang as the C compiler with the Visual Studio MSVC toolchain providing the Windows SDK and resource compiler.

It triggers on:

Tag release flow

  1. Checkout with full history
  2. Set up MSVC environment (ilammy/msvc-dev-cmd) and add LLVM to PATH
  3. Update CONTRIBUTORS.md from git log
  4. Extract major.minor.patch from the pushed tag
  5. Run cmake -DCMAKE_C_COMPILER=clang-cl -DVERSION_OVERRIDE=major.minor.patch (increments build_number.txt)
  6. Build with Ninja
  7. Code-sign CatiaMenuWin32.exe via skymatic/code-sign-action@v1 using the certificate stored in GitHub Secrets
  8. Commit build_number.txt and CONTRIBUTORS.md back to main
  9. Import GPG key and sign the new tag — deletes the base tag (e.g. v1.2.0), creates a GPG-signed tag with the full build number (e.g. v1.2.0.31)
  10. Create GitHub Release with the signed CatiaMenuWin32.exe, README.md, CONTRIBUTORS.md, LICENSE.txt

Code signing secrets

The following secrets must be configured in the repository (Settings → Secrets → Actions):

Secret Description
CERTIFICATE Base64-encoded PFX file. Generate with: [System.Convert]::ToBase64String([IO.File]::ReadAllBytes('cert.pfx'))
PASSWORD Password protecting the PFX certificate
CERTHASH SHA1 thumbprint of the certificate (from MMC → Certificates)
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 the git_committer_name / git_committer_email values in the workflow (KaiUR / kairathjen@yahoo.com).


Releasing a New Version

git tag v1.3.0
git push origin v1.3.0

That’s it. The workflow handles everything else automatically. The final release tag will be v1.3.0.<buildnum>.

Increment rules:


Thread Safety

The app uses two background threads (sync and updater) plus the UI thread. Thread-safe communication follows two patterns:

Posting to the UI thread

Never call Win32 UI functions from a background thread. Use PostMessage to marshal work to the UI thread:

// From any thread — allocate a buffer, format, post:
PostStatus(L"Sync done.");  // inline helper in main.h

// Custom messages (defined in main.h):
PostMessage(g.hwnd, WM_SYNC_DONE, (WPARAM)result, 0);
PostMessage(g.hwnd, WM_SCRIPT_STARTED, 0, 0);
PostMessage(g.hwnd, WM_SCRIPT_STOPPED, 0, 0);

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

Protecting shared state

g.folders[] and g.folder_count are written by the sync thread and read by the UI thread. Guard every access with g.cs_folders:

EnterCriticalSection(&g.cs_folders);
// read or write g.folders[] here
LeaveCriticalSection(&g.cs_folders);

Atomic handle ownership (g.run_process)

The running-script process handle is shared between Runner_Thread (writer) and Runner_Stop / MainWndProc (readers). Use InterlockedExchangePointer for ownership transfer — exactly one caller gets the non-NULL 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 guarantees no double-close and no double-terminate regardless of which side wins the race.


Repeat-on-Double-Click Architecture

The repeat feature re-runs a script automatically each time it completes. State is stored in AppState g (never in dialogs or local statics):

Field Type Purpose
g.repeat_mode bool true while a script is looping
g.repeat_fi int Folder index of the script to repeat
g.repeat_si int Script index within that folder
g.suppress_lbuttonup bool Suppresses the extra WM_LBUTTONUP that follows a WM_LBUTTONDBLCLK

Message sequence (main window)

Win32 double-click sends: WM_LBUTTONDOWNWM_LBUTTONUP (first click, runs the script via WM_COMMAND) → WM_LBUTTONDBLCLKWM_LBUTTONUP (must be suppressed).

WM_LBUTTONDBLCLK is handled in BtnSubclassProc (paint.c): it sets g.repeat_mode, g.repeat_fi/si, and g.suppress_lbuttonup = true. The next WM_LBUTTONUP checks suppress_lbuttonup, clears it, and returns without running the script.

Repeat trigger

WM_SCRIPT_STOPPED (main.c → MainWndProc) checks g.repeat_mode and calls Runner_Run(g.repeat_fi, g.repeat_si) to start the next iteration.

Cancellation

Quick Bar

QuickBarProc (quickbar.c) receives WM_LBUTTONDBLCLK because CS_DBLCLKS is set on the bar window class. The handler mirrors the main-window logic using QB_GetFav to resolve the hit index to fi/si.

Visual indicator

Paint_ScriptButton (paint.c) receives bool repeat and bool running. Priority: repeat (amber) > running (green) > hot (blue).

QB_Paint (quickbar.c) mirrors this: rep and run are computed per button via QB_GetFav; rep wins over run.

Console-mode guard

Console-mode scripts (run via cmd /k) do not post WM_SCRIPT_STOPPED, so the repeat trigger never fires and the running highlight never shows. Both BtnSubclassProc and QuickBarProc check g.cfg.show_console in the WM_LBUTTONDBLCLK handler and display a status-bar message instead of activating repeat.


Running Script Highlight Architecture

When a background (no-console) script runs, the button that was clicked turns green for the duration of the run.

State fields (AppState g)

Field Type Purpose
g.script_running bool true while a background script is in flight
g.run_fi int Folder index of the running script
g.run_si int Script index within that folder

Flow

  1. Runner_Run (runner.c) invalidates the old running button (if g.script_running is true) before overwriting g.run_fi = fi; g.run_si = si;, then creates the thread. This ensures only the newly launched script’s button turns green.
  2. Runner_Thread posts WM_SCRIPT_STARTED once the process is created.
  3. WM_SCRIPT_STARTED handler (main.c) sets g.script_running = true, invalidates IDC_SCRIPT_BTN_BASE + g.run_si, and invalidates hwnd_qbar — both repaint green.
  4. WM_SCRIPT_STOPPED handler clears g.script_running = false and triggers the same repaints — both return to normal colour.

Paint condition

// tabs.c (main window)
bool running = g.script_running && !g.repeat_mode && g.run_fi == fi && g.run_si == idx;

// quickbar.c
bool run = g.script_running && !g.repeat_mode && g.run_fi == fi_btn && g.run_si == si_btn;

!g.repeat_mode ensures repeat (amber) always takes priority over the green running state.


Code Style