Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(wc:*)",
"Bash(cmake:*)",
"Bash(ctest:*)",
"Bash(\"build/tests/Release/display-lock-unittests.exe\")",
"Bash(\"display-lock-unittests.exe\")",
"Bash(\"./display-lock-unittests.exe\")",
"Bash(xxd:*)"
]
}
}
64 changes: 64 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Display-Lock is a lightweight Windows application that locks the cursor to a selected window. Written primarily in C using the Win32 API, it's designed for gamers and multi-monitor users who need to keep their cursor confined to a specific window.

## Build Commands

```bash
# Configure and build (Release)
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release

# Configure and build (Debug)
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --config Debug

# Run tests (requires Conan for GTest dependency)
pip install conan==1.59.0
ctest --test-dir build -C Release
```

The build requires Visual Studio 2022 (MSVC) on Windows. Conan package manager is used to fetch Google Test for unit testing.

## Architecture

**Component Library (`src/components/`)**: Core functionality as a static library (`display_lock_components`)
- `win.c` - Cursor locking logic and window manipulation
- `settings.c` - Binary config file persistence (`.DLOCK` format in `%APPDATA%/DisplayLock/`)
- `applications.c` - Application whitelist management and monitoring thread
- `notify.c` - System tray icon and notifications
- `update.c` - Version checking via HTTP
- `menu.c` - Menu UI handling

**Main Application (`src/`)**: Win32 dialog-based UI
- `main.c` - Entry point, window class registration, message loop
- `ui.c` - Tab view rendering (Window Select, Settings, Applications)
- `procedures/` - Dialog procedure handlers

**Key Data Structures (`include/common.h`)**:
- `SETTINGS` - User preferences (minimize on start, fullscreen, borderless, etc.)
- `WINDOWLIST` - Array of up to 35 windows for selection
- `APPLICATION_LIST` - Whitelisted applications for automatic cursor locking

**Threading Model**: Cursor locking and application monitoring run in dedicated threads, synchronized via mutex (`DLockApplicationMutex`).

## Testing

Tests use Google Test (fetched via Conan). Test files are in `tests/` with test resources copied to the build directory.

```bash
# Run a single test
ctest --test-dir build -C Release -R SettingsTest
```

## Contributing Guidelines

- Work on `develop` branch for features, `master` for hotfixes
- Avoid external dependencies (keep it lightweight)
- Do not manually change version numbers or build scripts
- Update CHANGELOG.md with changes
- All builds and tests must pass before merge
2 changes: 1 addition & 1 deletion include/applications.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ BOOL createApplicationDirectory(wchar_t *outPath);
BOOL createApplicationSettings(const wchar_t *appPath, APPLICATION_SETTINGS *application);

BOOL startApplicationThread(HANDLE *thread, int (*callback)(void *parameters), void *args);
void closeApplicationThread(HANDLE thread, BOOL *status);
void closeApplicationThread(HANDLE *thread, volatile BOOL *status);
DWORD getPidFromName(const wchar_t *name);

BOOL CALLBACK EnumWindowsProcPID(HWND hwnd, LPARAM lParam);
Expand Down
17 changes: 12 additions & 5 deletions include/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
#define APPLICATION_VIEW 2

#define APPLICATION_MUTEX_NAME L"DLockApplicationMutex"

// Magic number constants
#define MAX_WINDOW_COUNT 35
#define MAX_TITLE_LENGTH 500
#define MAX_CLASS_NAME 500
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant MAX_CLASS_NAME is defined but never used in the codebase. Consider removing it to keep the code clean, or document where it's intended to be used if it's for future functionality.

Suggested change
#define MAX_CLASS_NAME 500

Copilot uses AI. Check for mistakes.

// custom messages

#define NOTIFY_MSG (WM_USER + 0x1)
Expand Down Expand Up @@ -86,7 +92,7 @@ struct MAIN_WINDOW_CONTROLS

struct WINDOW
{
char title[500];
char title[MAX_TITLE_LENGTH];
int x;
int y;
RECT size;
Expand All @@ -97,7 +103,7 @@ struct WINDOW
struct WINDOWLIST
{
int count;
WINDOW windows[35];
WINDOW windows[MAX_WINDOW_COUNT];
};

struct WINDOW_VIEW_CONTROLS
Expand Down Expand Up @@ -125,7 +131,7 @@ struct SETTINGS_VIEW_CONTROLS

struct MENU
{
void (*closeThread)(HANDLE thread, BOOL *status);
void (*closeThread)(HANDLE *thread, volatile BOOL *status);
void (*updateComboBox)(HWND control, WINDOWLIST *windows, void (*callback)(WINDOWLIST *));
BOOL (*startThread)
(HANDLE *thread, int (*callback)(void *parameters), void *args);
Expand All @@ -145,7 +151,7 @@ struct SETTINGS
struct ARGS
{
SETTINGS *settings;
BOOL *clipRunning;
volatile BOOL *clipRunning;
WINDOW selectedWindow;
HWND hWnd;
MAIN_WINDOW_CONTROLS controls;
Expand All @@ -154,7 +160,8 @@ struct ARGS
struct APPLICATION_ARGS
{
APPLICATION_LIST *applicationList;
BOOL *clipRunning;
volatile BOOL *clipRunning;
HANDLE mutex;
};

union VERSION
Expand Down
2 changes: 1 addition & 1 deletion include/menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@
void initMenuObj(MENU *menu);
void updateComboBox(HWND control, WINDOWLIST *windows, void (*callback)(WINDOWLIST *));
BOOL startThread(HANDLE *thread, int (*callback)(void *parameters), void *args);
void closeThread(HANDLE thread, BOOL *status);
void closeThread(HANDLE *thread, volatile BOOL *status);
118 changes: 75 additions & 43 deletions src/components/applications.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,42 @@ BOOL readApplicationList(APPLICATION_LIST *applicationList, const wchar_t *path)
return FALSE;
}

fread(&applicationList->count, sizeof(int), 1, file);
applicationList->applications = (APPLICATION_SETTINGS *)malloc(sizeof(APPLICATION_SETTINGS) * applicationList->count);
for (int i = 0; i < applicationList->count; i++)
if (fread(&applicationList->count, sizeof(int), 1, file) != 1)
{
fread(&applicationList->applications[i], sizeof(APPLICATION_SETTINGS), 1, file);
applicationList->count = 0;
applicationList->applications = NULL;
fclose(file);
return FALSE;
}

if (applicationList->count > 0)
{
applicationList->applications = (APPLICATION_SETTINGS *)malloc(sizeof(APPLICATION_SETTINGS) * applicationList->count);
if (applicationList->applications == NULL)
{
applicationList->count = 0;
fclose(file);
return FALSE;
}

for (int i = 0; i < applicationList->count; i++)
{
if (fread(&applicationList->applications[i], sizeof(APPLICATION_SETTINGS), 1, file) != 1)
{
free(applicationList->applications);
applicationList->applications = NULL;
applicationList->count = 0;
fclose(file);
return FALSE;
}
}
}
else
{
applicationList->applications = NULL;
}

fclose(file);
_fcloseall();

return TRUE;
}
Expand All @@ -64,7 +91,6 @@ BOOL writeApplicationList(APPLICATION_LIST *applicationList, const wchar_t *path
}

fclose(file);
_fcloseall();

return TRUE;
}
Expand All @@ -76,6 +102,8 @@ BOOL addApplication(APPLICATION_LIST *applicationList, APPLICATION_SETTINGS appl
{
applicationList->count = 1;
applicationList->applications = (APPLICATION_SETTINGS *)malloc(sizeof(APPLICATION_SETTINGS));
if (applicationList->applications == NULL)
return FALSE;
applicationList->applications[0] = application;
}
else
Expand All @@ -87,8 +115,11 @@ BOOL addApplication(APPLICATION_LIST *applicationList, APPLICATION_SETTINGS appl
return FALSE;
}

APPLICATION_SETTINGS *temp = (APPLICATION_SETTINGS *)realloc(applicationList->applications, sizeof(APPLICATION_SETTINGS) * (applicationList->count + 1));
if (temp == NULL)
return FALSE;
applicationList->applications = temp;
applicationList->count++;
applicationList->applications = (APPLICATION_SETTINGS *)realloc(applicationList->applications, sizeof(APPLICATION_SETTINGS) * applicationList->count);
applicationList->applications[applicationList->count - 1] = application;
}

Expand Down Expand Up @@ -119,7 +150,10 @@ BOOL removeApplication(APPLICATION_LIST *applicationList, int index)
}

applicationList->count--;
applicationList->applications = (APPLICATION_SETTINGS *)realloc(applicationList->applications, sizeof(APPLICATION_SETTINGS) * applicationList->count);
APPLICATION_SETTINGS *temp = (APPLICATION_SETTINGS *)realloc(applicationList->applications, sizeof(APPLICATION_SETTINGS) * applicationList->count);
if (temp != NULL)
applicationList->applications = temp;
// If realloc fails, keep the old pointer (memory is still valid, just larger than needed)
}

return TRUE;
Expand Down Expand Up @@ -153,7 +187,7 @@ BOOL createApplicationDirectory(wchar_t *outPath)
if (!SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_RoamingAppData, 0, NULL, &path)))
return FALSE;

wcscpy(outPath, path);
wcscpy_s(outPath, MAX_PATH, path);

// create directory
PathAppend(outPath, TEXT("DisplayLock"));
Expand All @@ -173,8 +207,8 @@ BOOL createApplicationSettings(const wchar_t *appPath, APPLICATION_SETTINGS *app
if (basename == appPath)
return FALSE;

wcscpy(application->application_path, appPath);
wcscpy(application->application_name, basename);
wcscpy_s(application->application_path, MAX_PATH, appPath);
wcscpy_s(application->application_name, MAX_PATH, basename);
application->borderless = FALSE;
application->fullscreen = FALSE;
application->enabled = TRUE;
Expand All @@ -185,7 +219,7 @@ BOOL createApplicationSettings(const wchar_t *appPath, APPLICATION_SETTINGS *app
BOOL startApplicationThread(HANDLE *thread, int (*callback)(void *parameters), void *args)
{
// TODO: check better error checking
*thread = (HANDLE)_beginthreadex(NULL, 0, callback, args, 0, NULL);
*thread = (HANDLE)(uintptr_t)_beginthreadex(NULL, 0, callback, args, 0, NULL);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cast through uintptr_t is good for suppressing warnings when converting between HANDLE and unsigned int, but the underlying issue is that _beginthreadex returns unsigned int, not HANDLE. Consider using a temporary variable to store the thread ID separately if needed, or document why this cast pattern is necessary.

Copilot uses AI. Check for mistakes.

if (*thread == NULL)
return FALSE;
Expand All @@ -194,15 +228,15 @@ BOOL startApplicationThread(HANDLE *thread, int (*callback)(void *parameters), v
}

// safely closes the thread
void closeApplicationThread(HANDLE thread, BOOL *status)
void closeApplicationThread(HANDLE *thread, volatile BOOL *status)
{
// check to see if thread is running
if (thread != NULL)
if (*thread != NULL)
{
*status = FALSE;
WaitForSingleObject(thread, INFINITE);
CloseHandle(thread);
thread = NULL;
WaitForSingleObject(*thread, INFINITE);
CloseHandle(*thread);
*thread = NULL;
}
}

Expand Down Expand Up @@ -251,62 +285,61 @@ int CALLBACK cursorLockApplications(void *parameters)

while (*(args->clipRunning))
{
HANDLE mutex = CreateMutex(NULL, FALSE, APPLICATION_MUTEX_NAME);
WaitForSingleObject(mutex, INFINITE);
WaitForSingleObject(args->mutex, INFINITE);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for WaitForSingleObject. While INFINITE timeout is used, the function can still fail and return WAIT_FAILED. Consider checking the return value and handling errors appropriately to avoid undefined behavior if the wait operation fails.

Copilot uses AI. Check for mistakes.

for (int i = 0; i < args->applicationList->count; i++)
{
APPLICATION_SETTINGS application = args->applicationList->applications[i];

if (application.enabled)
{
EnumWindowsProcPIDArgs args;
args.pid = getPidFromName(application.application_name);
args.hwnd = NULL;
EnumWindowsProcPIDArgs enumArgs;
enumArgs.pid = getPidFromName(application.application_name);
enumArgs.hwnd = NULL;

if (args.pid != 0)
EnumWindows(EnumWindowsProcPID, (LPARAM)&args);
if (enumArgs.pid != 0)
EnumWindows(EnumWindowsProcPID, (LPARAM)&enumArgs);

if (args.hwnd != NULL)
if (enumArgs.hwnd != NULL)
{
RECT rect;
GetWindowRect(args.hwnd, &rect);
GetWindowRect(enumArgs.hwnd, &rect);

if (application.borderless)
{
const long long borderlessStyle = GetWindowLongPtr(args.hwnd, GWL_STYLE);
const long long borderlessStyleEx = GetWindowLongPtr(args.hwnd, GWL_EXSTYLE);
const long long borderlessStyle = GetWindowLongPtr(enumArgs.hwnd, GWL_STYLE);
const long long borderlessStyleEx = GetWindowLongPtr(enumArgs.hwnd, GWL_EXSTYLE);

const long long mask = WS_OVERLAPPED | WS_THICKFRAME | WS_SYSMENU | WS_CAPTION;
const long long exMask = WS_EX_WINDOWEDGE;

if ((borderlessStyle & mask) != 0)
SetWindowLongPtr(args.hwnd, GWL_STYLE, borderlessStyle & ~mask);
SetWindowLongPtr(enumArgs.hwnd, GWL_STYLE, borderlessStyle & ~mask);

if ((borderlessStyleEx & exMask) != 0)
SetWindowLongPtr(args.hwnd, GWL_EXSTYLE, borderlessStyleEx & ~exMask);
SetWindowLongPtr(enumArgs.hwnd, GWL_EXSTYLE, borderlessStyleEx & ~exMask);
}
else if (application.fullscreen)
{
RECT rect = {0};
GetClientRect(args.hwnd, &rect);
ClientToScreen(args.hwnd, (LPPOINT)&rect.left);
ClientToScreen(args.hwnd, (LPPOINT)&rect.right);
RECT fsRect = {0};
GetClientRect(enumArgs.hwnd, &fsRect);
ClientToScreen(enumArgs.hwnd, (LPPOINT)&fsRect.left);
ClientToScreen(enumArgs.hwnd, (LPPOINT)&fsRect.right);

if (rect.left != 0 || rect.top != 0 || rect.right != GetSystemMetrics(SM_CXSCREEN) || rect.bottom != GetSystemMetrics(SM_CYSCREEN))
SetWindowPos(args.hwnd, NULL, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), 0);
if (fsRect.left != 0 || fsRect.top != 0 || fsRect.right != GetSystemMetrics(SM_CXSCREEN) || fsRect.bottom != GetSystemMetrics(SM_CYSCREEN))
SetWindowPos(enumArgs.hwnd, NULL, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), 0);
}

// TODO: lock cursor
HWND active = GetForegroundWindow();

if (args.hwnd == active)
if (enumArgs.hwnd == active)
{
GetCursorPos(&cursorPosition);
RECT windowRect = {0};
GetClientRect(args.hwnd, &windowRect);
ClientToScreen(args.hwnd, (LPPOINT)&windowRect.left);
ClientToScreen(args.hwnd, (LPPOINT)&windowRect.right);
GetClientRect(enumArgs.hwnd, &windowRect);
ClientToScreen(enumArgs.hwnd, (LPPOINT)&windowRect.left);
ClientToScreen(enumArgs.hwnd, (LPPOINT)&windowRect.right);

if ((cursorPosition.y <= windowRect.bottom && cursorPosition.y >= windowRect.top) && (cursorPosition.x >= windowRect.left && cursorPosition.x <= windowRect.right))
ClipCursor(&windowRect);
Expand All @@ -315,11 +348,10 @@ int CALLBACK cursorLockApplications(void *parameters)
}
}
}
Sleep(1);
}

ReleaseMutex(mutex);
CloseHandle(mutex);
ReleaseMutex(args->mutex);
Sleep(1);
}

ClipCursor(NULL);
Expand Down
Loading
Loading