commit 94615948f8ce85e272327860da3bc331cffbec19 Author: acidburn Date: Wed Dec 3 13:05:57 2025 -0500 working 1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..564b110 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +CC = gcc +TARGET = notepad_clone.exe +SRC = notepad_clone.c + +CFLAGS = -Wall -Wextra -std=c11 -mwindows +LDLIBS = -luser32 -lgdi32 -lcomdlg32 + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) -o $@ $< $(LDLIBS) + +.PHONY: clean +clean: + del /f /q $(TARGET) 2>NUL || true diff --git a/notepad_clone.c b/notepad_clone.c new file mode 100644 index 0000000..8544479 --- /dev/null +++ b/notepad_clone.c @@ -0,0 +1,580 @@ +#define UNICODE +#define _UNICODE +#define __STDC_WANT_LIB_EXT1__ 1 +#include +#include +#include +#include +#include + +#ifndef swprintf_s +int swprintf_s(wchar_t *buffer, size_t sizeOfBuffer, const wchar_t *format, ...); +#endif + +#define IDM_FILE_NEW 1001 +#define IDM_FILE_OPEN 1002 +#define IDM_FILE_SAVE 1003 +#define IDM_FILE_SAVE_AS 1004 +#define IDM_FILE_EXIT 1005 + +#define IDM_EDIT_UNDO 2001 +#define IDM_EDIT_CUT 2002 +#define IDM_EDIT_COPY 2003 +#define IDM_EDIT_PASTE 2004 +#define IDM_EDIT_DELETE 2005 +#define IDM_EDIT_SELECT 2006 +#define IDM_EDIT_TIME 2007 + +#define IDM_FORMAT_WRAP 3001 +#define IDM_FORMAT_FONT 3002 + +#define IDM_HELP_ABOUT 4001 + +static HWND g_hEdit = NULL; +static HFONT g_hFont = NULL; +static HMENU g_hMenu = NULL; +static HACCEL g_hAccel = NULL; +static bool g_wordWrap = false; +static WCHAR g_filePath[MAX_PATH] = L""; +static LOGFONTW g_logFont = {0}; + +#ifndef ARRAYSIZE +#define ARRAYSIZE(arr) (sizeof(arr) / sizeof((arr)[0])) +#endif + +int swprintf_s(wchar_t *buffer, size_t sizeOfBuffer, const wchar_t *format, ...); + +static void CopyStringSafe(WCHAR *dest, size_t destCount, const WCHAR *src) +{ + if (!dest || destCount == 0) + { + return; + } + if (!src) + { + dest[0] = L'\0'; + return; + } + + wcsncpy(dest, src, destCount - 1); + dest[destCount - 1] = L'\0'; +} + +static void UpdateTitle(HWND hwnd) +{ + const WCHAR *name = g_filePath; + const WCHAR *lastSlash = wcsrchr(g_filePath, L'\\'); + if (lastSlash && *(lastSlash + 1) != L'\0') + { + name = lastSlash + 1; + } + if (name[0] == L'\0') + { + name = L"Untitled"; + } + + WCHAR title[512]; + swprintf_s(title, ARRAYSIZE(title), L"%s - Notepad Clone", name); + SetWindowTextW(hwnd, title); +} + +static void SetDefaultFont(HWND hwnd) +{ + if (!g_hFont) + { + if (!SystemParametersInfoW(SPI_GETICONTITLELOGFONT, sizeof(g_logFont), &g_logFont, 0)) + { + ZeroMemory(&g_logFont, sizeof(g_logFont)); + g_logFont.lfHeight = -11; + wcscpy(g_logFont.lfFaceName, L"Segoe UI"); + } + g_hFont = CreateFontIndirectW(&g_logFont); + } + SendMessage(hwnd, WM_SETFONT, (WPARAM)g_hFont, TRUE); +} + +static void ResizeEdit(HWND hwndParent) +{ + RECT rc; + GetClientRect(hwndParent, &rc); + MoveWindow(g_hEdit, 0, 0, rc.right - rc.left, rc.bottom - rc.top, TRUE); +} + +static void RecreateEdit(HWND hwndParent) +{ + int len = GetWindowTextLengthW(g_hEdit); + WCHAR *buffer = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (len + 1) * sizeof(WCHAR)); + if (buffer) + { + GetWindowTextW(g_hEdit, buffer, len + 1); + } + + DestroyWindow(g_hEdit); + + DWORD styles = WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN; + if (!g_wordWrap) + { + styles |= WS_HSCROLL | ES_AUTOHSCROLL; + } + + g_hEdit = CreateWindowExW(0, L"EDIT", NULL, styles, 0, 0, 0, 0, hwndParent, (HMENU)1, GetModuleHandle(NULL), NULL); + SetDefaultFont(g_hEdit); + if (buffer) + { + SetWindowTextW(g_hEdit, buffer); + SendMessageW(g_hEdit, EM_SETSEL, (WPARAM)len, (LPARAM)len); + HeapFree(GetProcessHeap(), 0, buffer); + } + ResizeEdit(hwndParent); +} + +static HMENU BuildMenu(void) +{ + HMENU hMenu = CreateMenu(); + + HMENU hFile = CreatePopupMenu(); + AppendMenuW(hFile, MF_STRING, IDM_FILE_NEW, L"&New\tCtrl+N"); + AppendMenuW(hFile, MF_STRING, IDM_FILE_OPEN, L"&Open...\tCtrl+O"); + AppendMenuW(hFile, MF_STRING, IDM_FILE_SAVE, L"&Save\tCtrl+S"); + AppendMenuW(hFile, MF_STRING, IDM_FILE_SAVE_AS, L"Save &As..."); + AppendMenuW(hFile, MF_SEPARATOR, 0, NULL); + AppendMenuW(hFile, MF_STRING, IDM_FILE_EXIT, L"E&xit"); + + HMENU hEdit = CreatePopupMenu(); + AppendMenuW(hEdit, MF_STRING, IDM_EDIT_UNDO, L"&Undo\tCtrl+Z"); + AppendMenuW(hEdit, MF_SEPARATOR, 0, NULL); + AppendMenuW(hEdit, MF_STRING, IDM_EDIT_CUT, L"Cu&t\tCtrl+X"); + AppendMenuW(hEdit, MF_STRING, IDM_EDIT_COPY, L"&Copy\tCtrl+C"); + AppendMenuW(hEdit, MF_STRING, IDM_EDIT_PASTE, L"&Paste\tCtrl+V"); + AppendMenuW(hEdit, MF_STRING, IDM_EDIT_DELETE, L"&Delete\tDel"); + AppendMenuW(hEdit, MF_SEPARATOR, 0, NULL); + AppendMenuW(hEdit, MF_STRING, IDM_EDIT_SELECT, L"Select &All\tCtrl+A"); + AppendMenuW(hEdit, MF_STRING, IDM_EDIT_TIME, L"Time/&Date\tF5"); + + HMENU hFormat = CreatePopupMenu(); + AppendMenuW(hFormat, MF_STRING | (g_wordWrap ? MF_CHECKED : 0), IDM_FORMAT_WRAP, L"&Word Wrap"); + AppendMenuW(hFormat, MF_STRING, IDM_FORMAT_FONT, L"&Font..."); + + HMENU hHelp = CreatePopupMenu(); + AppendMenuW(hHelp, MF_STRING, IDM_HELP_ABOUT, L"&About Notepad Clone"); + + AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hFile, L"&File"); + AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hEdit, L"&Edit"); + AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hFormat, L"F&ormat"); + AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hHelp, L"&Help"); + + return hMenu; +} + +static void UpdateWrapMenu(void) +{ + HMENU hFormat = GetSubMenu(g_hMenu, 2); + CheckMenuItem(hFormat, IDM_FORMAT_WRAP, MF_BYCOMMAND | (g_wordWrap ? MF_CHECKED : MF_UNCHECKED)); +} + +static bool ShowOpenDialog(HWND hwnd, WCHAR *pathBuffer, DWORD cchBuffer) +{ + pathBuffer[0] = L'\0'; + OPENFILENAMEW ofn = {0}; + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = hwnd; + ofn.lpstrFilter = L"Text Files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0"; + ofn.lpstrFile = pathBuffer; + ofn.nMaxFile = cchBuffer; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_EXPLORER; + ofn.lpstrDefExt = L"txt"; + return GetOpenFileNameW(&ofn); +} + +static bool ShowSaveDialog(HWND hwnd) +{ + OPENFILENAMEW ofn = {0}; + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = hwnd; + ofn.lpstrFilter = L"Text Files (*.txt)\0*.txt\0All Files (*.*)\0*.*\0"; + ofn.lpstrFile = g_filePath; + ofn.nMaxFile = MAX_PATH; + ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_EXPLORER; + ofn.lpstrDefExt = L"txt"; + return GetSaveFileNameW(&ofn); +} + +static bool LoadFileToEdit(HWND hwnd, const WCHAR *path) +{ + HANDLE hFile = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) + { + MessageBoxW(hwnd, L"Unable to open the file.", L"Error", MB_ICONERROR | MB_OK); + return false; + } + + LARGE_INTEGER size; + if (!GetFileSizeEx(hFile, &size) || size.QuadPart > 20 * 1024 * 1024) + { + CloseHandle(hFile); + MessageBoxW(hwnd, L"File is too large to open.", L"Error", MB_ICONERROR | MB_OK); + return false; + } + + DWORD bytesToRead = (DWORD)size.QuadPart; + BYTE *buffer = (BYTE *)HeapAlloc(GetProcessHeap(), 0, bytesToRead + 3); + DWORD bytesRead = 0; + bool success = false; + + if (buffer && ReadFile(hFile, buffer, bytesToRead, &bytesRead, NULL)) + { + int skip = 0; + UINT codePage = CP_UTF8; + if (bytesRead >= 3 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF) + { + skip = 3; + } + else if (bytesRead >= 2 && buffer[0] == 0xFF && buffer[1] == 0xFE) + { + codePage = 1200; // UTF-16 LE + } + else if (bytesRead >= 2 && buffer[0] == 0xFE && buffer[1] == 0xFF) + { + codePage = 1201; // UTF-16 BE + } + else + { + codePage = CP_ACP; + } + + int wideLen = MultiByteToWideChar(codePage, 0, (LPCCH)(buffer + skip), bytesRead - skip, NULL, 0); + WCHAR *wbuf = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (wideLen + 1) * sizeof(WCHAR)); + if (wbuf) + { + MultiByteToWideChar(codePage, 0, (LPCCH)(buffer + skip), bytesRead - skip, wbuf, wideLen); + wbuf[wideLen] = L'\0'; + SetWindowTextW(g_hEdit, wbuf); + HeapFree(GetProcessHeap(), 0, wbuf); + success = true; + } + } + + if (buffer) + { + HeapFree(GetProcessHeap(), 0, buffer); + } + CloseHandle(hFile); + + if (!success) + { + MessageBoxW(hwnd, L"Unable to read the file.", L"Error", MB_ICONERROR | MB_OK); + } + return success; +} + +static bool SaveFileFromEdit(HWND hwnd, const WCHAR *path) +{ + int len = GetWindowTextLengthW(g_hEdit); + WCHAR *wbuf = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (len + 1) * sizeof(WCHAR)); + if (!wbuf) + { + MessageBoxW(hwnd, L"Not enough memory to save.", L"Error", MB_ICONERROR | MB_OK); + return false; + } + + GetWindowTextW(g_hEdit, wbuf, len + 1); + + HANDLE hFile = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) + { + HeapFree(GetProcessHeap(), 0, wbuf); + MessageBoxW(hwnd, L"Unable to create the file.", L"Error", MB_ICONERROR | MB_OK); + return false; + } + + // Save as UTF-8 with BOM for reliable re-open. + BYTE bom[] = {0xEF, 0xBB, 0xBF}; + DWORD written = 0; + bool success = false; + if (!WriteFile(hFile, bom, sizeof(bom), &written, NULL)) + { + MessageBoxW(hwnd, L"Failed to save the file.", L"Error", MB_ICONERROR | MB_OK); + HeapFree(GetProcessHeap(), 0, wbuf); + CloseHandle(hFile); + return false; + } + + int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wbuf, len, NULL, 0, NULL, NULL); + if (utf8Len > 0) + { + BYTE *mbuf = (BYTE *)HeapAlloc(GetProcessHeap(), 0, utf8Len); + if (mbuf) + { + WideCharToMultiByte(CP_UTF8, 0, wbuf, len, (LPSTR)mbuf, utf8Len, NULL, NULL); + if (WriteFile(hFile, mbuf, utf8Len, &written, NULL)) + { + success = true; + } + HeapFree(GetProcessHeap(), 0, mbuf); + } + } + else + { + success = true; // Empty file is fine. + } + + HeapFree(GetProcessHeap(), 0, wbuf); + CloseHandle(hFile); + + if (!success) + { + MessageBoxW(hwnd, L"Failed to save the file.", L"Error", MB_ICONERROR | MB_OK); + } + return success; +} + +static void DoFileNew(HWND hwnd) +{ + g_filePath[0] = L'\0'; + SetWindowTextW(g_hEdit, L""); + UpdateTitle(hwnd); +} + +static void DoFileOpen(HWND hwnd) +{ + WCHAR original[MAX_PATH]; + CopyStringSafe(original, ARRAYSIZE(original), g_filePath); + if (!ShowOpenDialog(hwnd, g_filePath, MAX_PATH)) + { + CopyStringSafe(g_filePath, ARRAYSIZE(g_filePath), original); + return; + } + + if (LoadFileToEdit(hwnd, g_filePath)) + { + UpdateTitle(hwnd); + } + else + { + CopyStringSafe(g_filePath, ARRAYSIZE(g_filePath), original); + } +} + +static void DoFileSave(HWND hwnd) +{ + if (g_filePath[0] == L'\0') + { + if (!ShowSaveDialog(hwnd)) + { + return; + } + } + + if (SaveFileFromEdit(hwnd, g_filePath)) + { + UpdateTitle(hwnd); + } +} + +static void DoFileSaveAs(HWND hwnd) +{ + WCHAR backup[MAX_PATH]; + CopyStringSafe(backup, ARRAYSIZE(backup), g_filePath); + if (ShowSaveDialog(hwnd)) + { + if (SaveFileFromEdit(hwnd, g_filePath)) + { + UpdateTitle(hwnd); + } + else + { + CopyStringSafe(g_filePath, ARRAYSIZE(g_filePath), backup); + } + } +} + +static void InsertTimeDate(void) +{ + SYSTEMTIME st; + GetLocalTime(&st); + WCHAR text[64]; + swprintf_s(text, ARRAYSIZE(text), L"%02d/%02d/%04d %02d:%02d", st.wMonth, st.wDay, st.wYear, st.wHour, st.wMinute); + SendMessageW(g_hEdit, EM_REPLACESEL, TRUE, (LPARAM)text); +} + +static void ToggleWordWrap(HWND hwnd) +{ + g_wordWrap = !g_wordWrap; + RecreateEdit(hwnd); + UpdateWrapMenu(); +} + +static void ChooseFontAndApply(HWND hwnd) +{ + CHOOSEFONTW cf = {0}; + cf.lStructSize = sizeof(cf); + cf.hwndOwner = hwnd; + cf.lpLogFont = &g_logFont; + cf.Flags = CF_SCREENFONTS | CF_INITTOLOGFONTSTRUCT | CF_EFFECTS; + + if (ChooseFontW(&cf)) + { + if (g_hFont) + { + DeleteObject(g_hFont); + g_hFont = NULL; + } + g_hFont = CreateFontIndirectW(&g_logFont); + SetDefaultFont(g_hEdit); + } +} + +static void ShowAbout(HWND hwnd) +{ + MessageBoxW(hwnd, L"Notepad Clone\nBuilt with Win32 API in C.", L"About", MB_OK | MB_ICONINFORMATION); +} + +static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + switch (msg) + { + case WM_CREATE: + g_hMenu = BuildMenu(); + SetMenu(hwnd, g_hMenu); + RecreateEdit(hwnd); + UpdateTitle(hwnd); + return 0; + case WM_SIZE: + ResizeEdit(hwnd); + return 0; + case WM_COMMAND: + switch (LOWORD(wParam)) + { + case IDM_FILE_NEW: + DoFileNew(hwnd); + break; + case IDM_FILE_OPEN: + DoFileOpen(hwnd); + break; + case IDM_FILE_SAVE: + DoFileSave(hwnd); + break; + case IDM_FILE_SAVE_AS: + DoFileSaveAs(hwnd); + break; + case IDM_FILE_EXIT: + PostMessageW(hwnd, WM_CLOSE, 0, 0); + break; + case IDM_EDIT_UNDO: + SendMessageW(g_hEdit, WM_UNDO, 0, 0); + break; + case IDM_EDIT_CUT: + SendMessageW(g_hEdit, WM_CUT, 0, 0); + break; + case IDM_EDIT_COPY: + SendMessageW(g_hEdit, WM_COPY, 0, 0); + break; + case IDM_EDIT_PASTE: + SendMessageW(g_hEdit, WM_PASTE, 0, 0); + break; + case IDM_EDIT_DELETE: + SendMessageW(g_hEdit, WM_CLEAR, 0, 0); + break; + case IDM_EDIT_SELECT: + SendMessageW(g_hEdit, EM_SETSEL, 0, -1); + break; + case IDM_EDIT_TIME: + InsertTimeDate(); + break; + case IDM_FORMAT_WRAP: + ToggleWordWrap(hwnd); + break; + case IDM_FORMAT_FONT: + ChooseFontAndApply(hwnd); + break; + case IDM_HELP_ABOUT: + ShowAbout(hwnd); + break; + } + return 0; + case WM_SETFOCUS: + SetFocus(g_hEdit); + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + } + return DefWindowProcW(hwnd, msg, wParam, lParam); +} + +int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) +{ + (void)hPrevInstance; + (void)lpCmdLine; + + { + HMODULE hUser = GetModuleHandleW(L"user32.dll"); + if (hUser) + { + BOOL(WINAPI *pSetProcessDPIAware)(void) = (BOOL(WINAPI *)(void))GetProcAddress(hUser, "SetProcessDPIAware"); + if (pSetProcessDPIAware) + { + pSetProcessDPIAware(); + } + } + } + + ACCEL accels[] = { + {FVIRTKEY | FCONTROL, 'N', IDM_FILE_NEW}, + {FVIRTKEY | FCONTROL, 'O', IDM_FILE_OPEN}, + {FVIRTKEY | FCONTROL, 'S', IDM_FILE_SAVE}, + {FVIRTKEY | FCONTROL, 'A', IDM_EDIT_SELECT}, + {FVIRTKEY | FCONTROL, 'Z', IDM_EDIT_UNDO}, + {FVIRTKEY | FCONTROL, 'X', IDM_EDIT_CUT}, + {FVIRTKEY | FCONTROL, 'C', IDM_EDIT_COPY}, + {FVIRTKEY | FCONTROL, 'V', IDM_EDIT_PASTE}, + {FVIRTKEY, VK_F5, IDM_EDIT_TIME}, + }; + g_hAccel = CreateAcceleratorTableW(accels, sizeof(accels) / sizeof(accels[0])); + + WNDCLASSEXW wc = {0}; + wc.cbSize = sizeof(wc); + wc.lpfnWndProc = WndProc; + wc.hInstance = hInstance; + wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); + wc.lpszClassName = L"NotepadCloneClass"; + wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); + + if (!RegisterClassExW(&wc)) + { + MessageBoxW(NULL, L"Failed to register window class.", L"Error", MB_ICONERROR | MB_OK); + return -1; + } + + HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"Notepad Clone", WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL); + + if (!hwnd) + { + MessageBoxW(NULL, L"Failed to create window.", L"Error", MB_ICONERROR | MB_OK); + return -1; + } + + ShowWindow(hwnd, nCmdShow); + UpdateWindow(hwnd); + + MSG msg; + while (GetMessageW(&msg, NULL, 0, 0) > 0) + { + if (!TranslateAcceleratorW(hwnd, g_hAccel, &msg)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + + return (int)msg.wParam; +} + +int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) +{ + (void)lpCmdLine; + return wWinMain(hInstance, hPrevInstance, GetCommandLineW(), nCmdShow); +}