diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..6d3258a --- /dev/null +++ b/main.cpp @@ -0,0 +1,423 @@ +/** awm - Alacritty Window Manager + +Description: + A simple WM for organising Alacritty windows up to 4 defined-set of configurations. + + Program is responsible for identifying all currently running instances of Alacritty + and organising them depending on the number of open windows. If no windows are open + when the program is called, the program simply finishes; otherwise, Alacritty windows + can take one-of-four different configuration layouts shown below. + + +Usage: + This program can be called using keybindings defined in an alacritty.yml file located + in "%APPDATA%\alacritty\alacritty.yml". An example keybinding might be as followed: + + key_binds: + - { key: O, mods: Control|Shift, action: command: { program: "awm", args: [] } } + + +* [[Future]]: The user can fullscreen a Alacritty window by passing the `--fullscreen` +* argument to the program. An example keybinding might be as followed: +* +* key_binds: +* - { key: F, mods: Control|Shift, action: command: { program: "awm", args: ["--fullscreen"] } } + + +Disclaimer: + This program cannot organise once a new instance/window has been created for Alacritty. + Users must manually call the program to start the organisation of windows. This is simply + because there is no mechanism within this program to listen for when newly created Alacritty + instances/windows are made from within an existing Alacritty instance. This is why keybindings + in Alacritty are recommended to call this program to organise windows - and the keybinding + can be used in the new window (no window is biased). + + +Examples: + 1 Window + ---------- + + Desktop Work Area + ==================================================================== + # # + # # + # ++++++++++++++++++++++++++++++++++++++++++++++ # + # + + # + # + + # + # + + # + # + Alacritty.exe + # + # + + # + # + + # + # + + # + # ++++++++++++++++++++++++++++++++++++++++++++++ # + # # + # # + ==================================================================== + + + 2 Windows + --------- + + Desktop Work Area + ===================================================================== + # # + # +++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++ # + # + + + + # + # + + + + # + # + + + + # + # + + + + # + # + Alacritty.exe + + Alacritty.exe + # + # + + + + # + # + + + + # + # + + + + # + # + + + + # + # +++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++ # + # # + ===================================================================== + + + 3 Windows + --------- + + Desktop Work Area + ===================================================================== + # # + # +++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++ # + # + + + + # + # + Alacritty.exe + + + # + # + + + + # + # +++++++++++++++++++++++++++++ + + # + # + Alacritty.exe + # + # +++++++++++++++++++++++++++++ + + # + # + + + + # + # + Alacritty.exe + + + # + # + + + + # + # +++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++ # + # # + ===================================================================== + + + 4 Windows + --------- + + Desktop Work Area + ===================================================================== + # # + # +++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++ # + # + + + + # + # + Alacritty.exe + + Alacritty.exe + # + # + + + + # + # +++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++ # + # # + # +++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++ # + # + + + + # + # + Alacritty.exe + + Alacritty.exe + # + # + + + + # + # +++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++ # + # # + ===================================================================== + + + All windows tiled are given an amount of padding against the desktop work area so + they don't take the full desktop work area space. Note, the "desktop work area" is + different from the resolution of the monitor. This area is the area which + applications will consume when maximised, thus it's the space which excludes the + taskbar. This means that regardless of the position of the taskbar, the window + manager will tile to utilise as much of the work area as possible, and centre to it + accordingly as well. + + In all examples (exc. 1 window), windows are sized accordingly with equal amounts + of padding against the work area, and centred accordingly to their new sizes in the + work area. In the case of 1 window, the WM will resize it back to a default defined + macro which specifies the width and height of the window - can be changed when + compiling with `-DWIN_DEFAULT_SIZE_X n` and `-DWIN_DEFAULT_SIZE_Y m`, where n,m are + non-negative and non-zero integers. + + +TODO: + - Ensure that no window can be resized less than 0 (more important for macros if changed). + - Consider bug that Task Manager is a collected window when open, but only occurring when + optimisation is not -O3 at least. + - Implement a fullscreen feature for a single window - possible toggle to/from fullscreen. +*/ + + +#include +#include + +#include +#include +#include +#include +#pragma comment(lib, "user32.lib") + +// the module name of the executable +#define PROC_IMAGE_FILENAME "alacritty.exe" +// the default pixel size of Windows +#define WIN_DEFAULT_SIZE_X 1064 +#define WIN_DEFAULT_SIZE_Y 560 + +/** + A structure which stores a collection of window handles and process IDs. + + PIDs are stored to keep track if the enumeration over windows returns a + PID multiple times (NOTE: the same window might appear multiple times in + this process. Might be a bug, consider future research for potential opts.). + + Struct also keeps count of how many window handles have been collected, so + the length of handle array doesn't require sizeof calculations. + */ +struct HandleCollector { + // create array to store collected handles (4 max instances allowed) + HWND* handles; + unsigned long* pids; + int count; +}; + + +/** Organise all the current window instances of Alacritty. + + Function takes a `HandleCollector`, which contains a pointer array of all handles + collected during enumeration of windows. Different configurations of organisation + of windows depends on the number of window handles collected, stored in `count` of + the `HandleCollector` struct. + + Works by starting a defer window position process which allows for multiple windows + to be resized and repositioned simultaneously, creating a seamless transition of + tiling. Once all the windows have been deferred accordingly, the deferral is closed, + finally performing all the transitions. + + [[Technical]]: The actual process is known as creating a multiple-window-position + structure, which is achieved with `BeginDeferWindowPos` to allocates memory for + this structure and returns a handle to said structure in memory. Any time `DeferWindowPos` + is called, the information about the changes of a window are then stored on the structure. + Once all deferrals have taken place, the `EndDeferWindowPos` is called with the handle + to complete the process and move all windows "simultaneously" by the information stored + in the structure. + + + @Return: a BOOL value if the defferal process was successful on all windows collected. + */ +static BOOL organiseWindows(HandleCollector* hc) { + // start a new defferal for simultaneous organisation of windows + HDWP defer = BeginDeferWindowPos(hc->count); + // get the current workspace area of the desktop + RECT workArea; + if ( !SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0) ) return FALSE; + // declare all needed parameters + int x, y, cx, cy; + HWND wh; + // create match for 4 different ways of organising windows + switch (hc->count) { + case 1: // recentre the single window + { + // initialise handler for window + wh = hc->handles[0]; + // set the window's X,Y coordinates + x = (workArea.right - workArea.left)/2 - WIN_DEFAULT_SIZE_X/2; + y = (workArea.bottom - workArea.top)/2 - WIN_DEFAULT_SIZE_Y/2; + + defer = DeferWindowPos(defer, wh, (HWND)0, x, y, + WIN_DEFAULT_SIZE_X, WIN_DEFAULT_SIZE_Y, + SWP_SHOWWINDOW | SWP_NOZORDER ); + break; + } + case 2: // arrange tiles as left|right + { + //printf("Organising first window.\n"); + wh = hc->handles[0]; + // set the window's width + cx = (workArea.right - workArea.left - 32) * 0.50f; + cy = (workArea.bottom - workArea.top) - 32; + + x = (workArea.right - workArea.left) * 0.50f - cx; + y = (workArea.bottom - workArea.top) * 0.50f - cy * 0.50f; + + defer = DeferWindowPos(defer, wh, (HWND)0, x-4, y, cx, cy, + SWP_SHOWWINDOW | SWP_NOZORDER ); + + //printf("Organising second window.\n"); + wh = hc->handles[1]; + defer = DeferWindowPos(defer, wh, (HWND)0, x+cx+4, y, cx, cy, + SWP_SHOWWINDOW | SWP_NOZORDER ); + + break; + } + case 3: // arrange tiles as 1---2|3 + { + //printf("Organising first window.\n"); + wh = hc->handles[0]; + // set the window's width + cx = (workArea.right - workArea.left - 32) * 0.50f; + cy = (workArea.bottom - workArea.top - 32) * 0.50f; + + x = (workArea.right - workArea.left) * 0.50f - cx; + y = (workArea.bottom - workArea.top) * 0.50f - cy; + + defer = DeferWindowPos(defer, wh, (HWND)0, x-4, y-4, cx, cy, + SWP_SHOWWINDOW | SWP_NOZORDER ); + + //printf("Organising second window.\n"); + wh = hc->handles[1]; + defer = DeferWindowPos(defer, wh, (HWND)0, x-4, y+cy+4, cx, cy, + SWP_SHOWWINDOW | SWP_NOZORDER ); + + cy = (workArea.bottom - workArea.top) - 24; + y = (workArea.bottom - workArea.top) * 0.50f - cy * 0.50f; + + //printf("Organising third window.\n"); + wh = hc->handles[2]; + defer = DeferWindowPos(defer, wh, (HWND)0, x+cx+4, y, cx, cy, + SWP_SHOWWINDOW | SWP_NOZORDER ); + + break; + } + case 4: // arrange tiles in quarts 1|2---3|4 + { + //printf("Organising first window.\n"); + wh = hc->handles[0]; + // set the window's width + cx = (workArea.right - workArea.left - 32) * 0.50f; + cy = (workArea.bottom - workArea.top - 32) * 0.50f; + + x = (workArea.right - workArea.left) * 0.50f - cx; + y = (workArea.bottom - workArea.top) * 0.50f - cy; + + defer = DeferWindowPos(defer, wh, (HWND)0, x-4, y-4, cx, cy, + SWP_SHOWWINDOW | SWP_NOZORDER ); + + //printf("Organising second window.\n"); + wh = hc->handles[1]; + defer = DeferWindowPos(defer, wh, (HWND)0, x+cx+4, y-4, cx, cy, + SWP_SHOWWINDOW | SWP_NOZORDER ); + + //printf("Organising third window.\n"); + wh = hc->handles[2]; + defer = DeferWindowPos(defer, wh, (HWND)0, x-4, y+cy+4, cx, cy, + SWP_SHOWWINDOW | SWP_NOZORDER ); + + //printf("Organising second window.\n"); + wh = hc->handles[3]; + defer = DeferWindowPos(defer, wh, (HWND)0, x+cx+4, y+cy+4, cx, cy, + SWP_SHOWWINDOW | SWP_NOZORDER ); + + } + default: break; // there's nothing to organise if no windows + } + + if ( !EndDeferWindowPos(defer) ) { + //printf("Failed to complete window deferral.\n"); + return FALSE; + } + + return TRUE; +} + +/** A callback function for `HwndCollector` during enumeration over windows. + + This function performs the intensive work of correctly identifying if a + window running should be gathered by the collector. If the callback deems + to have identified a window to collect, it'll add the window's handle and + PID to their respectful arrays, and increment the counter of collected windows. + + [[Technical]]: A callback is given an argument of type `LPARAM`, which can be + any type of object, cast in the function call, and cast back inside the callback. + A `HandleCollector` is given to the callback function to store all collected + window handles - this must be casted appropriately to the callback. + + To identify if a window belongs to an Alacritty executable, simple regex is used + to identify if a module filename belongs to an Alacritty executable. `GetModuleFileNameEx` + returns a full path to the window's image; that is, given the window's PID, this can be + used to attain the full pathspec to the executable a window belongs to. By using + regex to search if the Alacritty binary name is part of the pathspec, windows that are + Alacritty can be identified and thus collected. + + Finally, the length of the handle array can be checked before adding a newly + identified handle. If there's enough space, the handle is added; otherwise, if there + is no more space, the enumeration should halt (even if more instances exist). Returning + `FALSE` tells the enumerator to stop, and `TRUE` tells it to continue, but this never + needs to be checked in the location the callback function is called by the enumerator. + + + @Return: a BOOL identifying if the enumerator should continue iterating or finish. + */ +static BOOL CALLBACK HwndCollectorCallback( HWND hWnd, LPARAM lParam ) { + HandleCollector* hwnds = (HandleCollector*)lParam; // cast `lParam` back appropriately + TCHAR procMFN[32767]; // buffer to store the module filename + // setup regex pattern to see if a image filename is Alacritty + std::regex re(PROC_IMAGE_FILENAME); + std::cmatch m; + + unsigned long pid; // store the PID of the window + GetWindowThreadProcessId(hWnd, &pid); // get the PID of the window by handle + // check that the handle isn't already in the buffer (Windows stuff, idk) + for (int i = 0; i < hwnds->count; i++) { + if (hwnds->pids[i] == pid) { + return TRUE; // move onto the next window in the enumerator + } + } + + // create a handle for the PID we wish to query + HANDLE procH = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); + // query the process for its image filename (e.g., "abc.exe") + GetModuleFileNameEx(procH, NULL, procMFN, sizeof(procMFN)); + CloseHandle(procH); // make sure to close the handle once we've gotten the image filename + // check if the title of the image filename is one we are looking for + if ( std::regex_search(procMFN, m, re) ) { +// std::cout << "Found " << hWnd << "." << procMFN << ".\n"; + // ensure that we have space to store a handle + if ( hwnds->count < 4 ) { + hwnds->handles[hwnds->count] = hWnd; // add it to the array of handles + hwnds->pids[hwnds->count] = pid; // add the PID to the array of PIDs + hwnds->count += 1; // incrememnt the counter + } else { return FALSE; } // indicate we have maxed out windows to tile + } + + return TRUE; // enumerator should continue until no more windows are left, or array is full. +} + + +/** Collect all window handles of currently running instances of Alacritty. + + Function takes a pointer to a `HandleCollector` which will store all the + window handles identified as Alacritty instances running. This is done by + enumerating over all windows currently running on the system, which is + given a callback function to perform additional operations on a window + handle picked up by the enumerator. + + + @Return: the number of windows gathered by the collector. + */ +static int HwndCollector( HandleCollector* hwnds ) { + // iterate over all the windows currently running on the system + // NOTE: the callback actually identifies the correct windows to collect. + EnumWindows(HwndCollectorCallback, (LPARAM)hwnds); + return hwnds->count; // return the number of handles added. +} + + +int main(int argc, char* argv[]) { + // construct a new collector for windows to be gathered and organised + HandleCollector handles = { + .handles = new HWND[4], + .pids = new unsigned long[4], + .count = 0 }; + + // no windows were collected, nothing to organise + if ( HwndCollector(&handles) == 0 ) return 0; + // attempt to organise all windows collected + if ( !organiseWindows(&handles) ) { + //printf("There was an issue organising the current window(s).\n"); + return 1; + } + + // debug stuff +// for (int i = 0; i