I built a native Windows Todo app in pure C (278 KB, no frameworks)

386 pointsposted 9 months ago
by toxi360

48 Comments

masternight

9 months ago

There is something I like about win32 gui programming. It's a little idiosyncratic, but if you read Raymond Chen's blog you'll see why.

The win32 API has its origins on the 8088 processor and doing things a certain way results in saving 40 bytes of code or uses one less register or something.

I wrote a lot of toy gui apps using mingw and Petzold's book back in the day. Writing custom controls, drawing graphics and text, handling scrolling, hit testing etc was all a lot of fun.

I see in your app you're using strcpy, sprintf. Any kind of serious programming you should be using the length-checked variants. I'm surprised the compiler didn't spew.

You'll also find that the Win32 API has a lot of replacements for what's in the C standard library. If you really want to try and get the executable size down, see if you can write your app using only <Windows.h> and no cstdlib. Instead of memset() you've got ZeroMemory(), instead of memcpy() you've got CopyMemory().

At some point writing raw C code becomes painful. Still, I think doing your first few attempts in raw C is the best way to learn. Managing all the minutiae gives you a great sense of what's going on while you're learning.

If you want to play more with win32 gui programming, I'd have a look at the WTL (Windows Template Library). It's a C++ wrapper around the win32 API and makes it much easier to reason about what's going on.

electroly

9 months ago

Instead of laboriously calling CreateWindow() for every control, traditionally we would lay out a dialog resource in a .rc file (Visual Studio still has the dialog editor to do it visually) and then use CreateDialog() instead of CreateWindow(). This will create all the controls for you. Add an application manifest and you can get modern UI styling and high-DPI support.

broken_broken_

9 months ago

I have done something similar for Linux under 2 KiB in assembly some time ago: https://gaultier.github.io/blog/x11_x64.html

As others have said, doing so in pure C and linking dynamically, you can easily remain under 20 KiB, at least on Linux, but Windows should be even simpler since it ships with much more out of the box as part of the OS.

In any event, I salute the effort! You can try the linking options I mentioned at the end of my article, it should help getting the size down.

Jamesits

8 months ago

"Native Windows look and feel".

Before actually launching it, I hoped it's listview had context menu, and double clicking certain fields would lead to (in-line-ish) dropdown menus or textboxes.

Maybe people don't know how to design programs for the Win32 UI/UX anymore, or maybe I'm too old for this.

eviks

9 months ago

> no frameworks

Checks out: blurry fonts in scaled dpi, no Tab support, can't Ctrl-A select text in text fields and do all the other stuff that pre-modern frameworks offered you, errors on adding a row, ...

> modern

In what way?

AaronAPU

9 months ago

The 6502 programmer in me is dying inside that 278kb now passes as lightweight.

toxi360

9 months ago

Hello friends, I made this app just to try it out and have some fun, haha, but the comments are right, something like this could have been done more sensibly with C++ or other languages, ahaha.

transcriptase

9 months ago

Seeing a lot of chirps in here from people who work on software or websites that load megabytes of JS or C# or in order to send 278kb of telemetry every time the user moves their mouse.

phendrenad2

9 months ago

The fact that you can do everything in C when developing a Windows app always makes me feel all warm and fuzzy. Building up from the lowest-level primitives just makes sense.

Meanwhile, on MacOS, everything is an ObjectiveC Object, so if you want to write an app in pure C you can but it's about 1000x more verbose because you've gone an abstraction level deeper than Apple intended, and you essentially have to puppeteer the Objective C class hierarchy to make anything happen. It's incredibly icky.

I don't know why they can't rebase the ObjectiveC class-based API onto a basic win32-style procedural API (technically win32 is also "class-based" but it's minimal). It's part of why I don't see myself porting any of my C code to MacOS any time soon.

vparikh

9 months ago

Looks like you are linking to static libraries. You should link to DLL not to static libraries - this is will cut down on the application size dramatically.

formerly_proven

9 months ago

If you add a manifest you’ll get post-Windows-2000 GUI styling.

dvdkon

9 months ago

If you're going for a small EXE, I'd recommend telling GCC to optimise for size with "-Os".

Link-Time Optimisation with "-flto" might also help, depending on how the libraries were built.

userbinator

9 months ago

That's more than 10x bigger than I expected, given that all it seems to do is manipulate a list view. Something like this should be doable in under 10KB.

webprofusion

9 months ago

Nostalgia: My first job in 1997 was a windows apps in C++, it was weather software used on ships and oil rigs, we used to ship updates on floppy disk via helicopter.

p0w3n3d

9 months ago

Great respect! I've tried many times, without final result. I'll try to use this for learning purposes!

Btw. I like how Inno Setup used some very old Delphi 2 compiler to create exe so small it would fit without breaking the zip compliancy. I read it somewhere 10+ years ago, so not sure if this is still the case, but still. And the initial dialog was done in pure winapi.h (of course it was winapi.pas which made everything more difficult for me to learn from)

rfl890

9 months ago

There's no application manifest for the common controls so it will look outdated

burnt-resistor

9 months ago

Contains numerous memory leaks, doesn't permit arbitrarily long lists, and saves and restores uninitialized data. Really sloppy.

lucasoshiro

9 months ago

Every time I see something in C for Windows I see people using MinGW, gcc and friends just like they would do in a Unix-like system. But I wouldn't expect that they are tools that Microsoft recommends for developing on Windows.

So, a honest question from a Linux/Mac guy: what is the Windows-y way to do that?

vardump

9 months ago

I think this Win32 app could be done in C under 20 kB.

scripturial

9 months ago

The allure of the perfect notes and todo app. Having gone through phases of various modern todo and note apps over the years, I’ve finally let it go and decided to embrace just using text files. (Neovim for me, not that it matters which text editor one uses)

It’s not that there aren’t cool apps for this stuff, it’s more that I have a trail of data across various todo and notes apps from years of different tools.

One solution to the problem of making things “feel native” is to go all in on letting go of native. Target a different style, be it minimalism, Commodore 64, pixel art, etc… it can be fun that way, especially if it’s mostly just a tool for you.

leecommamichael

9 months ago

Odd that there’s so much conversation about this. Why is that? Genuine question.

mtlynch

9 months ago

Thanks for sharing, OP!

It's probably too late, but this qualifies for "Show HN" if you update the title to have the prefix "Show HN: ".[0]

I think the size of the source is actually more impressive than the size of the binary. I'm impressed that you can implement the whole thing in what looks like about 1 KLOC in just four .c files.

[0] https://news.ycombinator.com/showhn.html

nu11ptr

9 months ago

This is a blast from the past and it is neat to see some bare bones coding projects, but as others have said this is hardly "modern".

bitwize

9 months ago

Hell to the yes!

    int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow);
If you know, you know.

I'm to understand that entire divisions of Microsoft itself no longer know how to code anywhere near this level, which is why many of their flagship applications (looking at you, Microsoft Teams (work or school)) are Electron monstrosities you can watch draw themselves like Windows 1.0 apps -- on modern multicore hardware.

EDIT: more correct function sig

jftuga

9 months ago

Here is one of few Windows apps that I have written, albeit in C#. compinfo is a single, 431 KB windows executable.

It displays basic computer info including user name, computer name, OS, model, serial number (service tag), CPU model, memory, IPv4 address and uptime.

https://github.com/jftuga/compinfo

Disposal8433

9 months ago

> A modern, native Windows Todo application

What's modern about it? Also you could have used C++ instead to remove some potential issues, and those global variables...

Use std::string and std::array or std::list, some anonymous namespaces, remove all the malloc, etc. Your code would be half the size and still compile to the same assembly language without the bugs.

pcunite

9 months ago

Back in the day I used to use UPX to compress my executables to achieve impressively small sizes.

dochtman

9 months ago

Curious what this would look like written in Rust, using the windows-rs bindings.

_bin_

9 months ago

Fun project! I think the smallest I ever shrunk a win32 application was on the order of 2-4kb by writing in ASM. It was a great illustration of why 10x binary size is actually a great trade-off in terms of productivity.

dataflow

9 months ago

You'll definitely want to allow theming on your controls, if for no other reason than to let them feel native like the rest of the OS.

I'd also suggest at ATL, to make your life a bit easier without making it much heavier.

thecaio

9 months ago

Never easy to pull these off, so congrats! App might be modern in the sense that you coded it in 2025, but looks straight out of Windows 98

yapyap

9 months ago

Title says 278 KB, github says 27 KB.

I assume this is a typo in the title, OP if you ask dang nicely I’m sure he would be willing to remove the typo.

nsxwolf

9 months ago

The Readme emojis tell me this was vibe coded.

ghewgill

9 months ago

I think the hard limit of 100 todos is the best feature of this. Why don't other todo apps have this feature?

Koshkin

9 months ago

Incidentally, NASM makes Win32 programming in assembler a breeze.

pshirshov

9 months ago

If only I can do the same across 3 desktop and two phone platforms...

re-lre-l

9 months ago

It should be written UPPER CASE, in Pascal, maybe...

hudo

9 months ago

Less LOC than React/Redux app... Makes you think, what were we doing last 30 years :/

thehias

9 months ago

278kb? you are doing something very wrong, this should be possible in 10kb!

tippytippytango

9 months ago

The pedantry in the comments of a todo app is exquisite. HN never disappoints.

Very nostalgic OP, warms my heart 10/10

toxi360

9 months ago

Right now it's only 27 kb and I've added the manifest file :)

keepamovin

9 months ago

Interesting! I was just looking at raylib for this kind of thing: super light weight, cross platform, reliable, ideally C-based method to get GUI.

Raylib and raygui is truly incredible from my point of view. I succeeded in getting the macOS and Windows builds going on a bunch of cute little novel (not stock standard in the repo) examples in a matter of hours with AI help. I'm inspired by all I can do with this.

For ages I felt "cut off" from the world of Desktop GUI because it was so verbose, and had high friction - need a bunch of tooling, set up, and so on. And then everything was fragile. I like to work quickly, with feedback, and PoCs and results. I think in raylib I have found a method that really achieves this. For instance, check out this tiny little "text_input.c"

  #define RAYGUI_IMPLEMENTATION
  #include <raylib.h>
  #include "deps/raygui.h"

  #define WINDOW_WIDTH 800
  #define WINDOW_HEIGHT 600
  #define MAX_INPUT_CHARS 32

  int main(void) {
      InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Text Input Demo");
      SetTargetFPS(60);

      // Load a larger font for better text appearance
      Font font = LoadFontEx("resources/fonts/arial.ttf", 32, 0, 0);
      if (font.texture.id == 0) {
          font = GetFontDefault(); // Fallback to default font if loading fails
      }

      // Set the font for raygui controls
      GuiSetFont(font);

      // Customize raygui styles (using BGR order for hex values)
      GuiSetStyle(BUTTON, TEXT_ALIGNMENT, TEXT_ALIGN_CENTER);
      GuiSetStyle(BUTTON, BASE_COLOR_NORMAL, 0x50AF4CFF);  // Green (B=80, G=175, R=76, A=255) ‚Üí R=76, G=175, B=80
      GuiSetStyle(BUTTON, TEXT_COLOR_NORMAL, 0xFFFFFFFF);  // White text
      GuiSetStyle(BUTTON, BASE_COLOR_PRESSED, 0x6ABB66FF); // Lighter green
      GuiSetStyle(BUTTON, TEXT_COLOR_PRESSED, 0xFFFFFFFF);
      GuiSetStyle(BUTTON, BASE_COLOR_FOCUSED, 0x84C781FF); // Hover color
      GuiSetStyle(BUTTON, TEXT_COLOR_FOCUSED, 0xFFFFFFFF);
      GuiSetStyle(BUTTON, BORDER_WIDTH, 2);
      GuiSetStyle(BUTTON, BORDER_COLOR_NORMAL, 0x327D2EFF); // Dark green border

      // Adjust font size for raygui controls (optional, since font is already 32pt)
      GuiSetStyle(DEFAULT, TEXT_SIZE, 20); // Slightly smaller for button and text box to fit better
      GuiSetStyle(DEFAULT, TEXT_SPACING, 1);

      char inputText[MAX_INPUT_CHARS + 1] = "\0"; // Buffer for text input
      bool textBoxEditMode = false; // Tracks if the text box is being edited
      bool messageSubmitted = false; // Tracks if a message has been submitted
      float effectTimer = 0.0f; // Timer for the flash effect
      const float effectDuration = 0.5f; // Flash duration in seconds

      while (!WindowShouldClose()) {
          // Update effect timer
          if (effectTimer > 0) {
              effectTimer -= GetFrameTime();
          }

          BeginDrawing();
          // Set background color based on effect
          if (effectTimer > 0) {
              ClearBackground((Color){ 255, 255, 150, 255 }); // Yellow flash (RGB order)
          } else {
              ClearBackground(RAYWHITE);
          }

          // Center the text box and button
          int textBoxWidth = 200;
          int textBoxHeight = 40;
          int buttonWidth = 120;
          int buttonHeight = 40;
          int textBoxX = (WINDOW_WIDTH - textBoxWidth) / 2;
          int textBoxY = (WINDOW_HEIGHT - textBoxHeight) / 2 - 40;
          int buttonX = (WINDOW_WIDTH - buttonWidth) / 2;
          int buttonY = textBoxY + textBoxHeight + 10;

          // Draw the text box
          if (GuiTextBox((Rectangle){ (float)textBoxX, (float)textBoxY, textBoxWidth, textBoxHeight }, inputText, MAX_INPUT_CHARS, textBoxEditMode)) {
              textBoxEditMode = !textBoxEditMode; // Toggle edit mode on click
          }

          // Draw the button
          if (GuiButton((Rectangle){ (float)buttonX, (float)buttonY, buttonWidth, buttonHeight }, "Submit")) {
              messageSubmitted = true;
              effectTimer = effectDuration; // Start the flash effect
              TraceLog(LOG_INFO, "Message submitted: %s", inputText);
          }

          // Display the submitted message
          if (messageSubmitted && inputText[0] != '\0') {
              const char *label = "Message: ";
              char displayText[256];
              snprintf(displayText, sizeof(displayText), "%s%s", label, inputText);
              int textWidth = MeasureTextEx(font, displayText, 32, 1).x;
              int textX = (WINDOW_WIDTH - textWidth) / 2;
              int textY = buttonY + buttonHeight + 20;
              DrawTextEx(font, displayText, (Vector2){ (float)textX, (float)textY }, 32, 1, (Color){ 33, 150, 243, 255 }); // Bright blue (RGB order)
          }

          EndDrawing();
      }

      UnloadFont(font);
      CloseWindow();
      return 0;
  }
I love it! I feel unleashed again to program in graphics and games and real GUI! The first real paid programming job I had was using a lot of ps5 in Java and JavaScript (Open Processing) and I dug it! :)

And the file sizes are sweet (to me):

- macOS: text_input - 123736

- Windows: text_input.exe - 538909

Two dependencies to distribute with on Windows: glfw3.dll and libraylib.dll (322K and 2.1MB respectively)

Raylib was built to make game programming fun. And maybe I will use it for that! :) But right now I want to use it for GUI. The issue with Qt and others, is while I like the idea of standard-Andy controls, I don't want to pay a commercial license - when I figure "it can't be that hard to get what I want" - as I plan to use this stuff for commercial/proprietary control-panes and layers on my existing products: BrowserBox, DiskerNet, and more.

At the same time I really respect what Qt have done growing their business and might be inspired or even emulate some of their model myself in my business.

fizlebit

9 months ago

Me no like inconsistent use of spaces.

            x += labelW+20;
            hDescEdit = createModernEdit(hwnd, x, y, editW, btnH, ID_DESC_EDIT);
            x += editW + gap;
What no clang-format or equiv in 1990?