/** 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 #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, char* arg) { // check to make sure that if 'arg' is NULL, make it an empty string if (arg == NULL) arg = (char*)""; // 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 { printf("Organising first window.\n"); // initialise handler for window wh = hc.handles[0]; // the sizing for fullscreen windows cx = workArea.right - workArea.left - 32; cy = workArea.bottom - workArea.top - 32; if ( strcmp(arg, "--fullscreen") == 0 ) { printf("Organising to fullscreen.\n"); x = (workArea.right - workArea.left)/2 - cx/2; y = (workArea.bottom - workArea.top)/2 - cy/2; defer = DeferWindowPos(defer, wh, (HWND)0, x, y, cx, cy, SWP_SHOWWINDOW | SWP_NOZORDER ); break; } // 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; } printf("Organised windows successfully.\n"); 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) ) { // 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. printf("Collecting windows...\n"); 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 }; if ( argc > 1 ) { if (strcmp(argv[1], "--fullscreen") == 0) { handles.handles[0] = GetForegroundWindow(); handles.count += 1; RECT wa; // work area of desktop RECT wr; // window rect if ( !SystemParametersInfo(SPI_GETWORKAREA, 0, &wa, 0) ) return 1; GetWindowRect(handles.handles[0], &wr); int IsFullscreen = (wr.right - wr.left == wa.right - wa.left - 32 && wr.bottom - wr.top == wa.bottom - wa.top - 32) ? true : false; // if the window is already fullscreen, then coming out of fullscreen // means the window should be placed back to where it was - collect // all windows again and organise. if ( !IsFullscreen ) { if ( !organiseWindows(handles, argv[1]) ) return 1; return 0; } // remove the added foreground window ready for collector handles.count = 0; // this will allow for the array to be overwritten } } // no windows were collected, nothing to organise if ( HwndCollector(handles) == 0 ) return 0; // attempt to organise all windows collected if ( !organiseWindows(handles, (char*)0) ) { printf("There was an issue organising the current window(s).\n"); return 1; } return 0; }