├── .gitignore ├── .screenshot.png ├── Makefile ├── README.md └── nightcap.c /.gitignore: -------------------------------------------------------------------------------- 1 | nightcap.exe 2 | nightcap.exe.so 3 | -------------------------------------------------------------------------------- /.screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abrasive/nightcap/0e57c1b57d2d2a64cd62f772c45f914a5dae8e98/.screenshot.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC = winegcc 2 | CFLAGS ?= -O2 3 | LDFLAGS ?= -lX11 4 | 5 | default: nightcap.exe 6 | 7 | %.exe: %.c 8 | $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Nightcap lets you run old Windows screensavers under XScreenSaver, using wine. 2 | 3 | ![xscreensaver-demo showing Windows' 3D Maze screensaver running](.screenshot.png) 4 | 5 | This uses the preview mode of the screensaver - where it draws into that little monitor in Windows' display settings. 6 | Screensavers that behave differently in preview mode probably won't work very well. 7 | 8 | Tested with various MS/Plus screensavers, including the all-important 3D Maze. 9 | 10 | # Usage 11 | 12 | 0. Make sure `wine` is installed and you can run `winegcc`. 13 | 14 | 1. Compile `nightcap.exe`: 15 | 16 | ``` 17 | make 18 | ``` 19 | 20 | 2. Make sure `nightcap.exe` is on your `PATH`. Note that the `xscreensaver` daemon needs to see this as well. 21 | 22 | 3. Place your screensavers in the directory with `nightcap.exe`. 23 | 24 | 4. Edit `~/.xscreensaver` to add your new screensavers! You need to add them to the end of the `programs:` entry, which is a multiline adventure. Make sure you don't leave a blank line between the existing entries and the new ones! The end of my programs list looks like this: 25 | 26 | ``` 27 | - GL: squirtorus --root \n\ 28 | GL: hextrail --root \n\ 29 | "3D Flower Box" nightcap.exe "3D Flower Box.scr" \n\ 30 | "3D Flying Objects" nightcap.exe "3D Flying Objects.scr" \n\ 31 | "3D Maze" nightcap.exe "3D Maze.scr" \n\ 32 | 33 | ``` 34 | 35 | The first quoted string is the title displayed in the XScreenSaver config tool; the second is the filename of the screensaver itself. 36 | 37 | Unfortunately you can't access the screensaver configurations through the XScreenSaver UI. 38 | Instead, run `wine screensavername.scr` to access each saver's config. 39 | 40 | 41 | # How 42 | 43 | Windows' screensaver preview mode works by supplying a HWND on the commandline; the screensaver is responsible for creating a child window and drawing its output there. 44 | Meanwhile, XScreenSaver provides its own X11 window, giving us the window ID to draw into. 45 | 46 | So all we have to do is we create a Win32 window, find the X11 window that Wine created for it, reparent the X11 window into XScreenSaver's X11 window, and then ask the screensaver to create itself under the Win32 window. 47 | 48 | Easy when you live in the cursēd half-light of `winelib`, one foot firmly on the UNIX plane, and the other dipped in the Gatesian river... 49 | -------------------------------------------------------------------------------- /nightcap.c: -------------------------------------------------------------------------------- 1 | // James Wah (abrasive) 2023 2 | // This file is in the public domain. 3 | 4 | // handle a delightful Windows/X11 type name conflict 5 | #define Status XStatus 6 | #include 7 | #undef Status 8 | #undef ControlMask 9 | 10 | #ifndef UNICODE 11 | #define UNICODE 12 | #endif 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 22 | 23 | static const wchar_t whole_window_prop[] = L"__wine_x11_whole_window"; 24 | 25 | HANDLE hProc = NULL; 26 | 27 | void cleanup(int signal) { 28 | if (hProc) { 29 | TerminateProcess(hProc, 0); 30 | hProc = NULL; 31 | } 32 | 33 | exit(0); 34 | } 35 | 36 | BOOL found_child = FALSE; 37 | BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam) { 38 | found_child = TRUE; 39 | } 40 | 41 | Display *dpy = NULL; 42 | int parent_xwindow; 43 | 44 | void die(const char *format, ...) { 45 | va_list ap; 46 | va_start(ap, format); 47 | char msg[512]; 48 | vsprintf(msg, format, ap); 49 | if (!msg) 50 | exit(1); 51 | 52 | printf("ERROR: %s\n", msg); 53 | 54 | XGCValues gc_values; 55 | gc_values.foreground = XWhitePixel(dpy, 0); 56 | GC gc = XCreateGC(dpy, parent_xwindow, GCForeground, &gc_values); 57 | XTextItem xti = { 58 | .chars = msg, 59 | .nchars = strlen(msg), 60 | }; 61 | XDrawText(dpy, parent_xwindow, gc, 100, 100, &xti, 1); 62 | XFlush(dpy); 63 | 64 | Sleep(3000); 65 | 66 | exit(1); 67 | } 68 | 69 | 70 | int main(int argc, char **argv) { 71 | signal(SIGTERM, cleanup); 72 | signal(SIGINT, cleanup); 73 | 74 | dpy = XOpenDisplay(NULL); 75 | HINSTANCE hInstance = NULL; 76 | 77 | const char *parent_xwindow_str = getenv("XSCREENSAVER_WINDOW"); 78 | if (!parent_xwindow_str) { 79 | printf("error: XSCREENSAVER_WINDOW is not set\n"); 80 | return 1; 81 | } 82 | parent_xwindow = strtol(parent_xwindow_str, NULL, 0); 83 | 84 | Window root; 85 | int x, y; 86 | unsigned int width, height, border_width, depth; 87 | 88 | XGetGeometry(dpy, parent_xwindow, &root, &x, &y, &width, &height, &border_width, &depth); 89 | 90 | if (argc < 2) { 91 | printf("usage: %s screensaver.scr\n", argv[0]); 92 | return 1; 93 | } 94 | 95 | const wchar_t CLASS_NAME[] = L"nightcap"; 96 | 97 | WNDCLASS wc = { }; 98 | 99 | wc.lpfnWndProc = WindowProc; 100 | wc.hInstance = hInstance; 101 | wc.lpszClassName = CLASS_NAME; 102 | RegisterClass(&wc); 103 | 104 | HWND hwnd = CreateWindowEx( 105 | 0, 106 | CLASS_NAME, 107 | L"nightcap.exe", 108 | WS_POPUP, 109 | 110 | // Size and position 111 | CW_USEDEFAULT, CW_USEDEFAULT, width, height, 112 | 113 | NULL, // Parent window 114 | NULL, // Menu 115 | hInstance, // Instance handle 116 | NULL // Additional application data 117 | ); 118 | 119 | if (hwnd == NULL) 120 | { 121 | return 0; 122 | } 123 | 124 | STARTUPINFO si; 125 | PROCESS_INFORMATION pi; 126 | 127 | ZeroMemory( &si, sizeof(si) ); 128 | si.cb = sizeof(si); 129 | ZeroMemory( &pi, sizeof(pi) ); 130 | 131 | char cmdline[MAX_PATH]; 132 | wchar_t wcmdline[MAX_PATH]; 133 | sprintf(cmdline, "\"%s\" /p %lu", argv[1], (uintptr_t)hwnd); 134 | MultiByteToWideChar(CP_UTF8, 0, cmdline, -1, wcmdline, MAX_PATH); 135 | 136 | if (!CreateProcess(NULL, 137 | wcmdline, 138 | NULL, 139 | NULL, 140 | FALSE, 141 | 0, 142 | NULL, 143 | NULL, 144 | &si, 145 | &pi)) { 146 | die("CreateProcess failed (%d). Probably the .scr was not found?", GetLastError()); 147 | } 148 | 149 | while (!found_child) { 150 | EnumChildWindows(hwnd, EnumChildProc, 0); 151 | Sleep(5); 152 | } 153 | 154 | RECT winsize; 155 | GetWindowRect(hwnd, &winsize); 156 | 157 | hProc = pi.hProcess; 158 | 159 | int our_xwindow = (uintptr_t)GetPropW(hwnd, whole_window_prop); 160 | 161 | int result = XReparentWindow(dpy, our_xwindow, parent_xwindow, 0, 0); 162 | XFlush(dpy); 163 | ShowWindow(hwnd, SW_SHOW); 164 | 165 | MSG msg = { }; 166 | while (GetMessage(&msg, NULL, 0, 0) > 0) 167 | { 168 | TranslateMessage(&msg); 169 | DispatchMessage(&msg); 170 | } 171 | 172 | TerminateProcess(hProc, 0); 173 | 174 | return 0; 175 | } 176 | 177 | LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 178 | { 179 | switch (uMsg) 180 | { 181 | case WM_DESTROY: 182 | PostQuitMessage(0); 183 | if (hProc) { 184 | TerminateProcess(hProc, 0); 185 | } 186 | return 0; 187 | } 188 | 189 | return DefWindowProc(hwnd, uMsg, wParam, lParam); 190 | } 191 | --------------------------------------------------------------------------------