Skip to content

Commit

Permalink
WinMsg 1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
CrazyCoder committed Jun 20, 2024
0 parents commit 2abaeff
Show file tree
Hide file tree
Showing 17 changed files with 321 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/cmake-build-debug/
/cmake-build-release/
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions .idea/winmsg.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions CMakeLists.txt
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")
125 changes: 125 additions & 0 deletions README.md
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.

![Example](doc/screenshot.png)

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>

![Sunshine Sleep App](doc/sunshine-app.png)
</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>

![Sunshine Sleep App](doc/moonlight-app.png)
</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.
Binary file added doc/moonlight-app.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/sunshine-app.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added res/winmsg.ico
Binary file not shown.
27 changes: 27 additions & 0 deletions res/winmsg.rc
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
}
}
117 changes: 117 additions & 0 deletions src/main.cpp
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;
}
1 change: 1 addition & 0 deletions src/resource.h
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#define IDI_MAIN 101

0 comments on commit 2abaeff

Please sign in to comment.