A charged native Win32 system task manager, built on the six-tab layout of the classic Windows Task Manager. Version 2 adds a live resource dashboard, process investigation tools, and a styled, DPI-aware workspace.
No MFC/ATL/WTL, no .NET, no Qt, no third-party libraries, no package
manager, no GUI designer. Everything is created in code or from
resource.rc. The result is a single executable with no runtime
dependencies beyond what ships with Windows.
- Live CPU, physical memory, commit charge and process/thread summary cards, with CPU and memory sparklines. Refresh, pause/resume and Run New Task are available in the header on every tab.
- Processes: search across image name, description, account and PID. Space-separated
terms must all match;
pid:1234matches a PID exactly. Resource filters show processes using at least 1% CPU or 100 MB of private memory. Search and sorting still work while sampling is paused. While the pointer is over the process list or its scrollbar, rows keep their order so they cannot move away before a click; values still refresh, new processes join at the bottom, and the list re-sorts as soon as the pointer leaves or a column header is clicked. - Mark takes a snapshot of every process; later, the summary reads
Since 12:03:44: 4 started, 1 exited (backgroundTaskHost.exe), 1 grew. New processes are tinted green and grown ones amber (16 MB more private memory or 256 more handles), and the "Changed since mark" filter shows only those. A reused PID counts as a new process. Unmark clears it. - The process inspector shows identity, account, CPU usage, private memory, parent PID, thread/handle counts, CPU time and lifetime I/O where available. It moves below the table in smaller windows. Open File Location checks process identity before revealing its executable in Explorer; Copy Details copies a text summary. Existing end-process safeguards and confirmation remain in place.
- File > Startup Impact lists every startup entry: the Run registry keys (user, machine and 32-bit machine), both Startup folders, services set to start automatically, and scheduled tasks triggered at logon or boot. Each shows whether it is enabled, how many of its processes are running, and the CPU time and private memory they have used, heaviest first. Go to Process opens the running process; Open file location reveals the executable. Processes that cannot be opened are still counted, by name, which can credit a different program of the same name; services are matched by the pid the service manager reports.
- Export CSV captures the current filtered, sorted process snapshot when clicked. Files use UTF-8 with a BOM, escaped text fields, numeric byte counts, and empty cells for unavailable memory. A completed temporary file replaces the destination only after writing succeeds. Text that could be interpreted as a spreadsheet formula is prefixed with an apostrophe.
- CPU/memory graphs use distinct colors; per-processor and kernel-time graph options remain available in View. Networking adds sortable receive/send rates in B/s, KB/s and larger units alongside utilization and link speed.
- Spike Blame: hover the CPU or memory history graph to see the five processes using the most CPU or private memory at that moment, with the time of the sample. Processes that have since exited are marked. Click a column to open its busiest still-running process in the Processes tab. Ctrl+B pins the highest CPU sample on screen. Culprits are recorded every sample on every tab, so a spike can be explained after the fact; pausing freezes them along with the graphs. In the per-CPU grid the cursor runs through every core and the header adds the hovered core's load; culprits stay system wide, since Windows does not measure processes per core. Tiny footprint mode does not blame.
- Applications shows how long a window has been hung, for example
Not Responding (2m 05s), and sorts the longest hang first on the Status column. End Task on a hung window offers to end its process, since a hung window cannot process a close request. Go to Process opens the owning process. DWM's ghost copies of hung windows are not listed. A single slow reply is not a hang: a window must miss two samples in a row, or be flagged by Windows itself. On other tabs a lighter watch keeps hang timers running without messaging any window, and the status bar reads1 app not responding (click to view). - Alternating table rows, CPU/memory cell shading, styled navigation, and scalable fonts and columns make large snapshots easier to scan. PID 0 (idle capacity) is excluded from the process table and its resource totals.
Window sizes are constrained to the current monitor's usable area, including after DPI changes. Very short windows prioritize the process table or performance graphs; process actions remain in the table's context menu. Tab/Shift+Tab traverses both the header and page controls.
| Shortcut | Action |
|---|---|
| Ctrl+F | Open Processes and focus search |
| Ctrl+P | Pause/resume the previous sampling speed |
| Ctrl+B | Pin the CPU peak on the Performance tab and show its culprits |
| Ctrl+N | Run a new task |
| F5 | Refresh once, including while paused |
| Ctrl+Tab / Ctrl+Shift+Tab | Next/previous tab |
Pause prevents scheduled sampling; an in-flight collection may finish. Refreshing or switching tabs while paused collects once. Rates are averages over the interval between adapter samples; disconnects and counter resets start a new baseline. Protected processes can expose fewer details.
This is a Windows-only C11 CMake project. Install CMake 3.25 or newer and one of these compilers:
- Visual Studio 2022 or newer with the Desktop development with C++ workload and a Windows SDK.
- MinGW-w64 with
gccandwindreson PATH.
The supplied presets and convenience scripts also require Ninja on PATH. There are no downloaded libraries or package-manager dependencies.
With MinGW-w64 on PATH:
cmake --preset mingw-release
cmake --build --preset mingw-release --parallel
ctest --preset mingw-releaseUse mingw-debug, msvc-release, or msvc-debug in all three commands for
another configuration. The MSVC Ninja presets require a developer environment;
use an x64 Native Tools prompt, or use build.ps1 below to import it automatically.
Outputs are build/<preset>/bin/<Release|Debug>/taskman.exe; each compiler and
configuration has its own build directory. Ninja presets also generate
compile_commands.json for editors.
CMake also supports a Visual Studio solution without Ninja or a developer prompt:
cmake -S . -B build/vs -G "Visual Studio 17 2022" -A x64
cmake --build build/vs --config Release --parallel
ctest --test-dir build/vs -C Release --output-on-failureSelect the generator for your installed Visual Studio version (for example,
Visual Studio 18 2026). Open the repository folder in a CMake-aware IDE, or
open the generated solution. Ordinary cmake -S . -B build/cmake also works
with CMake's default installed generator.
| CMake option | Effect |
|---|---|
-DTASKMAN_STRICT=ON |
Application warnings become errors (default OFF) |
-DBUILD_TESTING=OFF |
Build only the application (default ON) |
-DCMAKE_BUILD_TYPE=Debug |
Debug configuration for single-configuration generators |
Tests always compile with warnings as errors and with assertions enabled,
including in Release. Builds must use a separate binary directory.
To stage the executable locally: cmake --install build/vs --config Release --prefix build/stage.
build.ps1 and build.cmd configure and build the same CMake targets.
The PowerShell driver uses vswhere to import MSVC's x64 environment when
needed, and falls back to MinGW-w64 when MSVC is absent. It copies the selected
application executable to build/taskman.exe for existing shortcuts.
.\build.ps1
.\build.ps1 test -Toolchain mingw -Strict
.\build.ps1 test -Toolchain msvc -Configuration Debug -StrictFrom cmd.exe use build, build test, or build debug.
| Target | Effect |
|---|---|
(none) / build |
Build the application (Release by default) |
rebuild |
Remove generated build directories, then build |
debug |
Build the application with symbols and optimization disabled |
test |
Build the application and run all ten CTest suites |
run |
Build, then launch the application |
clean |
Remove build/ and legacy tests/.build/ |
| Option | Effect |
|---|---|
-Toolchain msvc / mingw / auto |
Select the compiler (default auto) |
-Configuration Release / Debug |
Select configuration; debug target forces Debug |
-Strict |
Application warnings become errors |
-Verbose |
Show compiler command lines |
mingw32-make, mingw32-make debug, mingw32-make test,
mingw32-make clean, and mingw32-make STRICT=1 remain available. The Makefile
forwards to the CMake driver with MinGW selected; it has no separate source or
link-library lists. DEBUG=1 selects Debug for its other targets.
MSVC builds use /W4, a static CRT (/MT, or /MTd for Debug), a Unicode
Windows entry point, and release link-time optimization when supported.
The embedded manifest comes from res/resource.rc; linker manifest generation
is disabled for targets embedding that resource. MinGW uses gnu11, -O2
in Release, and a Unicode Windows subsystem executable. The application keeps
the original warning set, including conversion, shadowing and prototype checks.
Use CTest as above, build test, or powershell -NoProfile -File tests/run.ps1.
The latter accepts the same -Toolchain, -Configuration, and -Strict options.
Most suites include the implementation under test with Win32 action/UI boundaries
stubbed; the live collector and workspace suites exercise real Windows APIs.
The workspace fixture needs a desktop session and runs serially within CTest.
- Registry strings: malformed byte counts, missing terminators, invalid types, exact fits, insufficient capacities, and extreme saved window rectangles.
- System tool launch boundaries and elevation handoff: real command handlers and startup logic, with shell/token/window API boundaries intercepted.
- Collector failures: event/thread creation errors, shutdown ordering, repeated starts and history wraparound.
- Live collector: real Windows counters and worker events, pause, refresh, stop and three restart cycles.
- Process collection, identity, search/filter combinations, selection restoration, CSV quoting/Unicode/unknown values/write errors, and application action boundaries.
- Network counters, rates, resets and history; service/session action boundaries.
- Real Win32 workspace controls: search, selection, disabled actions, pause/refresh,
all six tabs, keyboard traversal, compact/short-wide layouts, tiny mode,
monitor bounds, and 150%/200% font and column scaling.
The UI fixture lives off-screen and writes captures under
<binary-dir>/tests/.build/.
Tests do not launch system utilities, request UAC, alter privileges, or write application registry settings. The startup test reads existing settings; registry parsing tests use injected registry responses. Test executables and local build artifacts remain in the selected CMake binary directory.
include/ app.h ui.h ntapi.h gdiplusapi.h proc_tree.h resource.h
src/ main.c ui.c settings.c sysinfo.c
src/tabs/ tab_apps.c tab_processes.c tab_services.c
tab_perf.c tab_network.c tab_users.c proc_tree.c
res/ resource.rc app.manifest app.ico
tests/ ten suites + CMakeLists.txt + run.ps1
build/ generated CMake directories and convenience taskman.exe
CMakeLists.txt application sources, resources, compiler settings and libraries
CMakePresets.json MSVC/MinGW Release/Debug presets
build.ps1 CMake front end with automatic compiler discovery
build.cmd cmd.exe shim over build.ps1
Makefile compatibility wrapper selecting MinGW-w64
| File | Contents |
|---|---|
src/main.c |
message loop, main window, menu bar, tab host, status bar, DPI/font handling, tiny footprint, tray CPU meter, New Task dialog, shared UI helpers |
src/ui.c / include/ui.h |
visual styles, live dashboard, chart drawing, navigation, button/table painting, column scaling |
src/sysinfo.c |
collector thread and all system data acquisition |
src/settings.c |
HKCU\Software\ClassicTaskManager persistence |
src/tabs/tab_*.c |
one translation unit per tab |
include/app.h |
shared declarations, the TabPage interface, the Snapshot struct |
include/ntapi.h |
hand-written native (undocumented) NT structures and prototypes |
include/gdiplusapi.h |
hand-written GDI+ flat-API declarations, for antialiased charts |
include/resource.h |
resource and command identifiers |
res/resource.rc / res/app.manifest / res/app.ico |
resources |
Collector thread. sysinfo.c runs one worker thread that samples the
machine on the update-speed interval (High 500 ms, Normal 1000 ms, Low
4000 ms, Paused blocks on the wake event). It fills the back buffer of a
two-entry Snapshot array, flips the front index under an SRWLOCK, and
posts WM_APP_SNAPSHOT_READY to the main window. The UI thread only ever
takes a shared lock and copies out what it needs, so no blocking
enumeration API is ever called on the message-pump thread. CPU and memory
history are pushed into a separate ring buffer on every tick regardless of
which tab is visible, so the Performance graphs stay continuous.
Tab pages. Each tab is a modeless child dialog created from the single
IDD_TABPAGE template with a shared workspace background, so
child-control backgrounds match the surface and IsDialogMessage
gives real Tab-key navigation inside the page. Every tab module exports one
TabPage struct of function pointers (OnCreate, OnLayout, OnSnapshot,
OnCommand, OnNotify, BuildViewMenu, PrimaryControl, ...); the host in
main.c knows nothing else about the tabs. Unimplemented hooks are NULL
and are simply skipped.
Menu. The menu bar is rebuilt in code whenever the active tab changes,
which is how Windows appears only on Applications and how the
tab-specific View items (Select Columns..., CPU History,
Large/Small Icons) come and go. Check marks and radio marks are refreshed
in WM_INITMENUPOPUP.
DPI. GetDpiForWindow, GetDpiForSystem and
SystemParametersInfoForDpi are resolved with GetProcAddress so the same
binary runs on Windows 10 and 11 without a load-time dependency on a newer
export; on failure it falls back to SystemParametersInfo plus a MulDiv
scale. The UI uses the system message-font family at 13 logical pixels,
with a larger type hierarchy for the dashboard and inspector. All
control geometry comes from dialog units via MapDialogRect, or from the
DPX() macro, so 125/150/200 % scaling lays out correctly.
WM_DPICHANGED recreates the font, re-applies it to every page and
relays out. List columns scale with DPI while retaining resized or hidden columns.
Tiny footprint mode. Double-clicking the empty area of the tab control (or the margin around it) collapses the window to just the current page's primary content: the menu bar, tab strip and status bar are hidden and the page is told to lay itself out in "tiny" mode. The normal and tiny window rectangles are remembered separately and both persist.
Antialiasing. GDI cannot antialias lines or polygons, so the chart
strokes and the filled area beneath them are drawn through GDI+ instead:
UI_Chart, UI_ChartLine and UI_Polyline in ui.c open one antialiased
surface per paint and stroke the whole trace in a single pass. gdiplus.dll is
resolved lazily with GetProcAddress against hand-written flat-API
declarations, so there is still no load-time dependency; if any export is
missing every entry point reports "no canvas" and the callers fall back to the
original Polygon/Polyline path. Grid lines stay on plain GDI, because they
are axis-aligned and one pixel wide and antialiasing would only blur them.
UI_Card takes the same route: GDI+ has no rounded-rectangle
primitive, so the outline is built as a path of four quarter-circle arcs,
inset half a pixel so the one pixel stroke lands on a pixel centre instead
of straddling two. Fill and outline are created before either is drawn, so
a failure part way through falls back to RoundRect rather than leaving a
half-drawn card. GdiplusShutdown runs once from WM_DESTROY.
Tray CPU meter. When Options > Hide When Minimized is on, minimizing
hides the window and shows a notification-area icon that is redrawn every
tick: a 16x16 32-bpp top-down DIB section with green bars over black, built
from the last 16 CPU samples and turned into an HICON with
CreateIconIndirect. The TaskbarCreated message is handled so the icon
comes back if Explorer restarts.
Single instance. A named mutex (Local\ClassicTaskManagerSingleInstance);
a second launch finds the existing window, restores and activates it, and
exits.
The executable ships an asInvoker manifest and never asks for elevation
on startup. SE_DEBUG_NAME is requested at startup and failure is ignored —
without it, non-elevated Task Manager still works, it just cannot open
some processes.
- Show processes from all users (Processes tab) carries a UAC shield via
BCM_SETSHIELDand relaunches the app elevated withShellExecuteEx+runas— the same thing the real Task Manager does, because seeing other sessions' processes genuinely requires the elevated token. - Create this task with administrative privileges in the New Task dialog
runs the target with the
runasverb. - Disconnect / Logoff / Send Message on the Users tab and Start / Stop on the Services tab will need elevation for anything outside the current session; they are specified to degrade with a clear message rather than fail silently.
Everything else — the process list, the graphs, the adapter list — works
unelevated with reduced detail for processes the caller cannot open
(System, Registry, Secure System, Memory Compression, protected
processes).
All of it lives behind ntapi.h, is declared by hand (nothing is taken
from <winternl.h>), and is reached through GetProcAddress on
ntdll.dll, so a missing or changed export degrades to a documented
fallback instead of failing to load.
| Call | Why the documented API is not enough |
|---|---|
NtQuerySystemInformation(SystemProcessInformation) |
One call returns image name, PID, parent PID, session, thread and handle counts, base priority, all I/O counters, kernel+user times and WorkingSetPrivateSize for every process. The documented route needs CreateToolhelp32Snapshot plus OpenProcess + GetProcessMemoryInfo + GetProcessTimes + GetProcessIoCounters per process, which is far slower and fails outright on processes that cannot be opened. WorkingSetPrivateSize — the "Memory (Private Working Set)" column — has no documented equivalent at all. |
NtQuerySystemInformation(SystemProcessorPerformanceInformation) |
Per-logical-processor idle/kernel/user times, needed for View > CPU History > One Graph Per CPU. GetSystemTimes only returns machine-wide totals. |
NtQuerySystemInformation(SystemPerformanceInformation) |
Paged/non-paged pool and cache page counts behind the Kernel Memory and Cached figures at the granularity the classic UI shows. |
Fallbacks currently in place: overall CPU comes from the documented
GetSystemTimes, and process/thread/handle counts, commit charge, pool
sizes and the system cache size come from GetPerformanceInfo. The
SYSTEM_PROCESS_INFORMATION path is wired up in milestone 2 with
CreateToolhelp32Snapshot as its documented fallback.
One approximation is worth naming: Free in the Physical Memory (MB)
group is computed as Available - Cached, because GlobalMemoryStatusEx
reports free + zero + standby as one number. Milestone 4 replaces this with
the exact zero/free page count from SystemPerformanceInformation.
Everything lives under HKCU\Software\ClassicTaskManager: window rect
(normal and tiny separately), maximized state, tiny-footprint state, active
tab, update speed, the four Options toggles, Applications view mode,
CPU-history mode, Show Kernel Times, network history mode, and the New Task
MRU list. Per-tab column set/order/widths and sort state join them as each
tab gains its data model.

