-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/cmake-build-debug/ | ||
/cmake-build-release/ |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
cmake_minimum_required(VERSION 3.28) | ||
project(winmsg) | ||
|
||
set(CMAKE_CXX_STANDARD 17) | ||
|
||
set(CMAKE_CXX_FLAGS "-static-libgcc -static-libstdc++ -static") | ||
|
||
add_executable(winmsg WIN32 src/main.cpp src/resource.h) | ||
target_sources(winmsg PRIVATE "res/winmsg.rc") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
### WinMsg | ||
|
||
This is a very basic Windows GUI application that takes a **message** to display and a **timeout** as the parameters | ||
and does exactly that. The message is displayed in the center of the black **full-screen** window in large white text, | ||
then the app exits automatically. | ||
|
||
 | ||
|
||
I wrote it for [Sunshine](https://github.com/LizardByte/Sunshine) / [Moonlight](https://github.com/moonlight-stream) | ||
streaming as a companion application when running command-line tools on a host via Moonlight. | ||
|
||
The common use case is to control the host PC state (**Shut down** / **Sleep** / **Hibernate**) with the tools | ||
like [NirCmd](https://www.nirsoft.net/utils/nircmd.html). | ||
|
||
Instead of the black screen and an error dialog, if the connection is aborted unexpectedly, the user will see the | ||
message from this app, then the stream will close gracefully after the app exists. | ||
|
||
This project page and documentation below also serve as a guide how to set up sleep/shutdown via Sunshine app | ||
shortcuts for Moonlight so that you can manually control your host state directly from a client. | ||
|
||
Of course, there are other solutions for that: | ||
|
||
- add an **Undo** command in Sunshine to run a tool that will suspend the PC when the app you stream exists | ||
- configure Windows Power profile to sleep/hibernate/shutdown after N minutes of inactivity | ||
- if streaming from Steam Deck, | ||
use [MoonDeck](https://github.com/FrogTheFrog/moondeck)/[MoonDeck Buddy](https://github.com/FrogTheFrog/moondeck-buddy) | ||
to control the PC state | ||
- for true nerds, use HomeAssistant with [HASS Agent](https://github.com/hass-agent/HASS.Agent) and | ||
a [WOL switch](https://www.home-assistant.io/integrations/wake_on_lan/) so that you can control the host PC via voice | ||
commands, HA dashboard, CLI and automations | ||
|
||
While all the above will work just fine, sometimes you may want to manually force the streaming host to sleep directly | ||
from the client. That is where the blow guide should help. | ||
|
||
### Usage | ||
|
||
``` | ||
winmsg.exe [<message>] [<timeout>] | ||
``` | ||
|
||
The message is the actual message to display, make sure to surround it with double quotes if it contains spaces. | ||
Timeout is the time in milliseconds to display the message. The app exits after timeout. | ||
|
||
Example: | ||
|
||
```commandline | ||
winmsg.exe "Going to sleep..." 5000 | ||
``` | ||
|
||
This will show the "Going to sleep..." message in the center of the black screen and exit after 5 seconds (5000ms). | ||
|
||
If no parameters are provided, the app will show the empty black screen with no text and exit after 5 seconds. | ||
|
||
### Sample Sunshine Application configuration to suspend the PC via NirCmd | ||
|
||
<details> | ||
<summary>Click to show the screenshot</summary> | ||
|
||
 | ||
</details> | ||
|
||
Open Applications settings in Sunshine Web UI (`https://<sunshine-pc:47990/apps`), click **Add New**. | ||
|
||
Make sure to download and install [NirCmd](https://www.nirsoft.net/utils/nircmd.html) so that it's available in your | ||
PATH (or adjust the **Detached Command** to use the full path to `nircmd.exe`). This app can do many things, including | ||
putting the PC in sleep mode (`standby` command), shutting down the PC (`exitwin shutdown`), | ||
rebooting (`exitwin reboot`), and [much more](https://www.nirsoft.net/utils/nircmd2.html#using). | ||
|
||
In this example, we use the following command in the **Detached Commands** section (to be run in the background): | ||
|
||
```commandline | ||
nircmd.exe cmdwait 20000 standby | ||
``` | ||
|
||
`cmdwait 20000` waits 20 seconds before forcing the PC to sleep, this allows Moonlight to disconnect properly and | ||
Sunshine to run "Undo" commands, if needed. Feel free to use a shorter delay. | ||
|
||
As the main **Command** we use this 'winmsg.exe' tool to display a message and exit after timeout: | ||
|
||
```commandline | ||
winmsg.exe "Sleeping..." 5000 | ||
``` | ||
|
||
Recommended options: | ||
|
||
- **Global Prep Commands**: _Disabled_ — this allows the command to run faster as the global Do/Undo commands will not | ||
run | ||
- **Continue streaming if the application exits quickly**: _Uncheck_ — the app will quick quickly anyway, we don't want | ||
any special handling by Sunshine | ||
- **Continue streaming until all app processes exit**: _Uncheck_ — also not needed for our case | ||
- **Exit Timeout**: 0 — for faster exit if forced by the client | ||
- `cmdwait` value should be greater than `winmsg` timeout (20000 > 5000 in this example) | ||
|
||
If you run the configured **Sleep** app via Moonlight, it will start the streaming session, run the `nircmd.exe` in | ||
background with the timeout, run `winmsg.exe` as your main streaming app, you will see the specified message, after | ||
5 seconds the app will exit, Moonlight will close the streaming session and then the PC with Sunshine will go to sleep | ||
after ~15 more seconds. | ||
|
||
<details> | ||
<summary>Click for Moonlight Sleep shortcut sample</summary> | ||
|
||
 | ||
</details> | ||
|
||
### Compiling | ||
|
||
This project can be built on Windows with [MinGW](https://www.mingw-w64.org/downloads/#mingw-builds)/CMake. | ||
|
||
Inside `mingw64` shell, `cd` into the project root directory, then: | ||
|
||
```commandline | ||
pacman -S mingw-w64-x86_64-ninja | ||
pacman -S mingw-w64-x86_64-cmake | ||
mkdir build | ||
cd build | ||
cmake .. | ||
cmake --build . --config Release | ||
``` | ||
|
||
`winmsg.exe` file will be in the `build` directory. | ||
|
||
You can also use [JetBrains CLion IDE](https://www.jetbrains.com/clion/) to open this project and build it right away | ||
as it already comes with MinGW toolchain and CMake build tool. | ||
|
||
Or just download the pre-compiled binary from the **Releases** page. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
#include <winver.h> | ||
#include "../src/resource.h" | ||
|
||
IDI_MAIN ICON "winmsg.ico" | ||
|
||
VS_VERSION_INFO VERSIONINFO | ||
FILEVERSION 1,0,0,0 | ||
PRODUCTVERSION 1,0,0,0 | ||
{ | ||
BLOCK "StringFileInfo" | ||
{ | ||
BLOCK "040904b0" | ||
{ | ||
VALUE "CompanyName", "CrazyCoder" | ||
VALUE "FileDescription", "winmsg.exe -- displays a message and exits" | ||
VALUE "FileVersion", "V1.0.0.0" | ||
VALUE "LegalCopyright", "(C)2024 Serge Baranov" | ||
VALUE "OriginalFilename", "winmsg.exe" | ||
VALUE "ProductName", "WinMsg" | ||
VALUE "ProductVersion", "V1.0.0.0" | ||
} | ||
} | ||
BLOCK "VarFileInfo" | ||
{ | ||
VALUE "Translation", 0x0409,1200 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
#include <windows.h> | ||
#include <algorithm> | ||
#include <cstdlib> | ||
#include <string> | ||
#include <chrono> | ||
|
||
#include "resource.h" | ||
|
||
static constexpr const char *const CLASS_NAME = "WinMsgWindowClass"; | ||
static constexpr const char *const WINDOW_NAME = "WinMsg"; | ||
|
||
LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { | ||
switch (msg) { | ||
case WM_DESTROY: | ||
PostQuitMessage(0); | ||
break; | ||
case WM_PAINT: { | ||
// hide mouse cursor | ||
SetCursor(NULL); | ||
|
||
PAINTSTRUCT ps; | ||
HDC hdc = BeginPaint(hwnd, &ps); | ||
|
||
RECT rect; | ||
GetClientRect(hwnd, &rect); | ||
int fontSize = std::min(rect.right, rect.bottom) / 8; // adjust this for relative font size | ||
|
||
HFONT hfont, hOldFont; | ||
hfont = CreateFont(fontSize, 0, 0, 0, FW_BOLD, 0, 0, 0, 0, 0, 0, 0, 0, "Arial"); | ||
hOldFont = (HFONT) SelectObject(hdc, hfont); | ||
|
||
// Set the text color to white | ||
SetTextColor(hdc, RGB(255, 255, 255)); | ||
// Set the background color to black | ||
SetBkColor(hdc, RGB(0, 0, 0)); | ||
SetBkMode(hdc, OPAQUE); | ||
|
||
std::string text = (__argc > 1) ? __argv[1] : ""; | ||
DrawText(hdc, text.c_str(), -1, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); | ||
|
||
SelectObject(hdc, hOldFont); | ||
DeleteObject(hfont); | ||
|
||
EndPaint(hwnd, &ps); | ||
} | ||
break; | ||
default: | ||
return DefWindowProc(hwnd, msg, wparam, lparam); | ||
} | ||
return 0; | ||
} | ||
|
||
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { | ||
WNDCLASSEX wc; | ||
HWND hwnd; | ||
|
||
wc.cbSize = sizeof(WNDCLASSEX); | ||
wc.style = 0; | ||
wc.lpfnWndProc = WindowProcedure; | ||
wc.cbClsExtra = 0; | ||
wc.cbWndExtra = 0; | ||
wc.hInstance = hInstance; | ||
wc.hIcon = static_cast<HICON>(::LoadImage(hInstance, | ||
MAKEINTRESOURCE(IDI_MAIN), | ||
IMAGE_ICON, | ||
256, 256, | ||
LR_DEFAULTCOLOR)); | ||
wc.hCursor = NULL; // hide mouse cursor | ||
wc.hbrBackground = (HBRUSH) (HBRUSH) CreateSolidBrush(RGB(0, 0, 0)); | ||
wc.lpszMenuName = NULL; | ||
wc.lpszClassName = CLASS_NAME; | ||
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); | ||
|
||
if (!RegisterClassEx(&wc)) { | ||
return 0; | ||
} | ||
|
||
int nScreenWidth = GetSystemMetrics(SM_CXSCREEN); | ||
int nScreenHeight = GetSystemMetrics(SM_CYSCREEN); | ||
|
||
hwnd = CreateWindowEx(0, CLASS_NAME, WINDOW_NAME, WS_POPUP, 0, 0, nScreenWidth, nScreenHeight, NULL, NULL, | ||
hInstance, NULL); | ||
|
||
if (hwnd == NULL) { | ||
return 0; | ||
} | ||
|
||
ShowWindow(hwnd, nCmdShow); | ||
UpdateWindow(hwnd); | ||
|
||
MSG Msg; | ||
int ReturnCode; | ||
int timeout = (__argc > 2) ? std::stoi(__argv[2]) : 5000; | ||
auto start = std::chrono::high_resolution_clock::now(); | ||
|
||
while (true) { | ||
while (PeekMessage(&Msg, NULL, 0, 0, PM_REMOVE)) { | ||
if (Msg.message == WM_QUIT) { | ||
ReturnCode = 0; | ||
goto End; | ||
} | ||
TranslateMessage(&Msg); | ||
DispatchMessage(&Msg); | ||
} | ||
|
||
auto end = std::chrono::high_resolution_clock::now(); | ||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count(); | ||
|
||
if (elapsed > timeout) { | ||
ReturnCode = 0; | ||
break; | ||
} | ||
} | ||
|
||
End: | ||
return ReturnCode; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
#define IDI_MAIN 101 |