Skip to content

Commit 8b07e76

Browse files
committed
fix(Core): do not call IsWindowMinimized preventing GetWindowsList from succeeding on Linux
1 parent 0d5bd68 commit 8b07e76

2 files changed

Lines changed: 238 additions & 70 deletions

File tree

SnapX.Core/SharpCapture/Linux/LinuxCapture.cs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,40 @@ public class LinuxCapture : BaseCapture
1414
{
1515
var isWayland = LinuxAPI.IsWayland();
1616

17-
if (!isWayland || !IsCompositorKwin)
18-
return LinuxAPI.TakeScreenshotWithX11(Methods.GetScreen(Methods.GetCursorPosition()) ?? new Screen());
17+
if (IsCompositorKwin)
18+
{
19+
try
20+
{
21+
return await TakeScreenshotWithKwin();
22+
}
23+
catch { }
24+
}
25+
26+
if (isWayland)
27+
{
28+
try
29+
{
30+
return await TakeScreenshotWithPortal();
31+
}
32+
catch (Exception ex)
33+
{
34+
DebugHelper.WriteException(ex);
35+
}
36+
}
37+
1938
try
2039
{
21-
return await TakeScreenshotWithKwin();
40+
return LinuxAPI.TakeScreenshotWithX11(Methods.GetScreen(Methods.GetCursorPosition()) ?? new Screen());
2241
}
23-
catch
42+
catch (Exception ex)
2443
{
25-
// Fallback to portal if KWin fails
26-
return await TakeScreenshotWithPortal();
44+
DebugHelper.WriteException(ex);
2745
}
46+
47+
return null;
2848
}
2949

50+
3051
private static async Task<Image> TakeScreenshotWithPortal()
3152
{
3253
var connection = new Connection(Address.Session!);
@@ -124,9 +145,6 @@ private static Image CropFullscreenScreenshotToBounds(Rectangle bounds, Image im
124145
var fullscreenImage = await CaptureFullscreen().ConfigureAwait(false);
125146
var croppedImage = CropFullscreenScreenshotToBounds(bounds, fullscreenImage);
126147
return croppedImage;
127-
// }
128-
129-
// return LinuxAPI.TakeScreenshotWithX11(screen);
130148
}
131149
public override async Task<Image?> CaptureScreen(Point? pos)
132150
{
@@ -141,6 +159,16 @@ private static Image CropFullscreenScreenshotToBounds(Rectangle bounds, Image im
141159
{
142160
return CropFullscreenScreenshotToBounds(rect, await CaptureFullscreen().ConfigureAwait(false));
143161
}
162+
public Task<Image?> CaptureWindow(WindowInfo window)
163+
{
164+
return Task.Run(() => ((LinuxAPI)Methods.NativeAPI).TakeScreenshotOfX11Window(window));
165+
}
166+
public override Task<Image?> CaptureWindow(Point pos)
167+
{
168+
var windows = Methods.NativeAPI.GetWindowList();
169+
170+
return (from window in windows.AsEnumerable().Reverse() let rect = window.Rectangle where rect.Contains(pos) select CaptureWindow(window)).FirstOrDefault() ?? Task.FromResult<Image?>(null);
171+
}
144172

145173
private static bool IsCompositorKwin => Environment.GetEnvironmentVariable("XDG_SESSION_TYPE") == "wayland" && Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP") == "KDE";
146174
}

SnapX.Core/Utils/Native/LinuxAPI.cs

Lines changed: 201 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -124,81 +124,94 @@ public override Rectangle GetWindowRectangle(IntPtr windowHandle)
124124
return null;
125125
}
126126

127-
127+
private static readonly Lock X11Sync = new();
128128

129129

130130
public override List<WindowInfo> GetWindowList()
131131
{
132+
DebugHelper.WriteLine($"GetWindowList called");
132133
var windows = new List<WindowInfo>();
133-
if (IsWayland())
134+
lock (X11Sync)
134135
{
135-
if (IsPlasma())
136+
// if (IsWayland())
137+
// {
138+
// if (IsPlasma())
139+
// {
140+
// // Interact with Plasmashell over DBus
141+
// return windows;
142+
// }
143+
//
144+
// if (IsGNOME())
145+
// {
146+
// // Interact with GNOME Shell
147+
// return windows;
148+
// }
149+
//
150+
// return windows;
151+
// }
152+
153+
var display = XOpenDisplay(null);
154+
if (display == IntPtr.Zero)
136155
{
137-
// Interact with Plasmashell over DBus
156+
DebugHelper.Logger?.Debug("Unable to open X display");
138157
return windows;
139158
}
140159

141-
if (IsGNOME())
160+
var root = XDefaultRootWindow(display); // Get the root window of the X display
161+
162+
// Get all the child windows of the root window
163+
var status = XQueryTree(
164+
display,
165+
root,
166+
out root,
167+
out _,
168+
out var windowsPtr,
169+
out var nchildren
170+
);
171+
if (status == 0 || windowsPtr == IntPtr.Zero)
142172
{
143-
// Interact with GNOME Shell
173+
DebugHelper.Logger?.Debug("XQueryTree failed");
174+
XCloseDisplay(display);
144175
return windows;
145176
}
177+
DebugHelper.Logger?.Debug("XQueryTree returned: " + status);
178+
DebugHelper.Logger?.Debug("nchildren: " + nchildren);
146179

147-
return windows;
148-
}
180+
for (uint i = 0; i < nchildren; i++)
181+
{
182+
var window = Marshal.ReadIntPtr(windowsPtr, (int)(i * IntPtr.Size));
183+
DebugHelper.Logger?.Debug("Window ptr: {Window:X0}", window);
149184

150-
var display = XOpenDisplay(null);
151-
if (display == IntPtr.Zero)
152-
{
153-
DebugHelper.Logger?.Debug("Unable to open X display");
154-
return windows;
155-
}
185+
var title = GetWindowTitle(display, window);
186+
DebugHelper.Logger?.Debug("Window title: {Title}", title);
187+
if (title == string.Empty || title == "Untitled")
188+
{
189+
DebugHelper.Logger?.Debug("Window is untitled, skipping");
190+
continue;
191+
}
156192

157-
var root = XDefaultRootWindow(display); // Get the root window of the X display
193+
XGetWindowAttributes(display, window, out var attributes);
194+
var isVisible = attributes.is_colormap_installed;
195+
// Active window
196+
XGetInputFocus(display, out var focusWindow, out _);
197+
var isActive = focusWindow == window;
198+
var rect = GetWindowRectangle(window);
199+
windows.Add(
200+
new WindowInfo
201+
{
202+
Handle = window,
203+
Title = title,
204+
IsVisible = isVisible,
205+
Rectangle = rect,
206+
// IsMinimized = IsWindowMinimized(display, window),
207+
IsActive = isActive,
208+
}
209+
);
210+
}
158211

159-
// Get all the child windows of the root window
160-
var status = XQueryTree(
161-
display,
162-
root,
163-
out root,
164-
out _,
165-
out var windowsPtr,
166-
out var nchildren
167-
);
168-
if (status == 0)
169-
{
170-
DebugHelper.Logger?.Debug("XQueryTree failed");
171212
XCloseDisplay(display);
172-
return windows;
173213
}
174214

175-
for (uint i = 0; i < nchildren; i++)
176-
{
177-
var window = Marshal.ReadIntPtr(windowsPtr, (int)(i * IntPtr.Size));
178-
var title = GetWindowTitle(display, window);
179-
XGetGeometry(display, window, out root, out _, out _, out _, out _, out _, out _);
180-
181-
XGetWindowAttributes(display, window, out var attributes);
182-
var isVisible = attributes.is_colormap_installed;
183-
184-
// Active window
185-
XGetInputFocus(display, out var focusWindow, out _);
186-
var isActive = focusWindow == window;
187-
var rect = GetWindowRectangle(window);
188-
windows.Add(
189-
new WindowInfo
190-
{
191-
Handle = window,
192-
Title = title,
193-
IsVisible = isVisible,
194-
Rectangle = rect,
195-
IsMinimized = IsWindowMinimized(display, window),
196-
IsActive = isActive,
197-
}
198-
);
199-
}
200-
201-
XCloseDisplay(display);
202215
return windows;
203216
}
204217

@@ -449,6 +462,103 @@ internal static Image TakeScreenshotWithX11(Screen screen)
449462

450463
return image;
451464
}
465+
internal Image? TakeScreenshotOfX11Window(WindowInfo window)
466+
{
467+
DebugHelper.WriteLine($"Screenshotting window '{window.Title}' with bounds {window.Rectangle}");
468+
469+
var display = XOpenDisplay(null);
470+
if (display == IntPtr.Zero)
471+
throw new Exception("Unable to open X display.");
472+
473+
// Get window attributes
474+
XGetWindowAttributes(display, window.Handle, out var attributes);
475+
476+
int width = attributes.width;
477+
int height = attributes.height;
478+
479+
DebugHelper.Logger?.Debug("Window Attributes: width={Width}, height={Height}, depth={Depth}", width, height, attributes.depth);
480+
DebugHelper.Logger?.Debug("visual: {Visual}, colormap: {Colormap}", attributes.visual, attributes.colormap);
481+
482+
// Capture the image from the window using XGetImage
483+
var imagePtr = XGetImage(
484+
display,
485+
window.Handle,
486+
0, 0,
487+
(uint)width,
488+
(uint)height,
489+
ALL_PLANES,
490+
ZPIXMAP
491+
);
492+
493+
if (imagePtr == IntPtr.Zero)
494+
throw new Exception("Unable to capture window image using XGetImage.");
495+
496+
var xImage = Marshal.PtrToStructure<XImage>(imagePtr);
497+
var bpp = xImage.bits_per_pixel;
498+
var bytesPerPixel = bpp / 8;
499+
500+
DebugHelper.Logger?.Debug($"XImage: width={xImage.width}, height={xImage.height}, bits_per_pixel={bpp}");
501+
DebugHelper.Logger?.Debug($"XImage masks: red=0x{xImage.red_mask:X}, green=0x{xImage.green_mask:X}, blue=0x{xImage.blue_mask:X}");
502+
503+
var rawPixels = new byte[width * height * bytesPerPixel];
504+
Marshal.Copy(xImage.data, rawPixels, 0, rawPixels.Length);
505+
506+
var pixelData = new byte[width * height * 4];
507+
508+
for (var y = 0; y < height; y++)
509+
{
510+
if (y % 100 == 0)
511+
DebugHelper.Logger?.Debug($"Processing row {y}/{height}");
512+
513+
for (var x = 0; x < width; x++)
514+
{
515+
var pixelOffset = (y * width + x) * bytesPerPixel;
516+
517+
ulong pixel = 0;
518+
var rawPixelBytes = new byte[bytesPerPixel];
519+
520+
for (var byteIndex = 0; byteIndex < bytesPerPixel; byteIndex++)
521+
{
522+
var currentByte = rawPixels[pixelOffset + byteIndex];
523+
rawPixelBytes[byteIndex] = currentByte;
524+
pixel |= (ulong)currentByte << (8 * byteIndex); // assuming little-endian
525+
}
526+
527+
if ((x == 0 && y == 0) || (x == width / 2 && y == height / 2))
528+
{
529+
DebugHelper.Logger?.Debug(
530+
"Pixel at ({X},{Y}): Raw Bytes: {RawBytes}, Combined pixel ulong: 0x{Pixel:X}, BytesPerPixel: {BPP}",
531+
x, y,
532+
BitConverter.ToString(rawPixelBytes),
533+
pixel,
534+
bytesPerPixel
535+
);
536+
DebugHelper.Logger?.Debug(
537+
"Masks - Red: 0x{RedMask:X}, Green: 0x{GreenMask:X}, Blue: 0x{BlueMask:X}",
538+
xImage.red_mask, xImage.green_mask, xImage.blue_mask
539+
);
540+
}
541+
542+
var r = ExtractColorComponent(pixel, xImage.red_mask);
543+
var g = ExtractColorComponent(pixel, xImage.green_mask);
544+
var b = ExtractColorComponent(pixel, xImage.blue_mask);
545+
546+
var idx = (y * width + x) * 4;
547+
pixelData[idx + 0] = r;
548+
pixelData[idx + 1] = g;
549+
pixelData[idx + 2] = b;
550+
pixelData[idx + 3] = 255;
551+
}
552+
}
553+
554+
var image = Image.LoadPixelData<Rgba32>(pixelData, width, height);
555+
556+
XDestroyImage(imagePtr);
557+
XCloseDisplay(display);
558+
559+
return image;
560+
}
561+
452562
static byte ExtractColorComponent(ulong pixel, ulong mask)
453563
{
454564
if (mask == 0)
@@ -481,14 +591,44 @@ static int CountBits(ulong mask)
481591
}
482592
return count;
483593
}
594+
private static readonly IntPtr XA_WM_NAME = new IntPtr(39); // Usually 39, but can be obtained via XInternAtom
595+
[StructLayout(LayoutKind.Sequential)]
596+
public struct XTextProperty
597+
{
598+
public IntPtr value;
599+
public IntPtr encoding;
600+
public int format;
601+
public ulong nitems;
602+
}
603+
604+
[LibraryImport(LibX11)]
605+
internal static partial int XGetTextProperty(IntPtr display, IntPtr window, out XTextProperty textProp, IntPtr property);
606+
484607
private static string GetWindowTitle(IntPtr display, IntPtr window)
485608
{
486-
var windowTitlePtr = XFetchName(display, window);
487-
return windowTitlePtr != IntPtr.Zero
488-
? Marshal.PtrToStringAnsi(windowTitlePtr) ?? "Untitled"
489-
: "Untitled";
609+
var netWmNameAtom = XInternAtom(display, "_NET_WM_NAME", false);
610+
XTextProperty textProp;
611+
612+
if (XGetTextProperty(display, window, out textProp, netWmNameAtom) != 0 && textProp.value != IntPtr.Zero)
613+
{
614+
var title = Marshal.PtrToStringAnsi(textProp.value);
615+
XFree(textProp.value);
616+
return title ?? "Untitled";
617+
}
618+
619+
// Fallback to XA_WM_NAME
620+
if (XGetTextProperty(display, window, out textProp, XA_WM_NAME) != 0 && textProp.value != IntPtr.Zero)
621+
{
622+
var title = Marshal.PtrToStringAnsi(textProp.value);
623+
XFree(textProp.value);
624+
return title ?? "Untitled";
625+
}
626+
627+
return "Untitled";
628+
490629
}
491630

631+
492632
[LibraryImport(LibX11)]
493633
internal static partial IntPtr XGetSelectionOwner(IntPtr display, IntPtr selection);
494634

@@ -517,8 +657,8 @@ internal static partial int XQueryTree(
517657
out uint nchildren
518658
);
519659

520-
[LibraryImport(LibX11)]
521-
internal static partial IntPtr XFetchName(IntPtr display, IntPtr window);
660+
[LibraryImport(LibX11, EntryPoint = "XFetchName", StringMarshalling = StringMarshalling.Utf8)]
661+
internal static partial int XFetchName(IntPtr display, IntPtr window, out IntPtr windowName);
522662

523663
[LibraryImport(LibX11)]
524664
internal static partial int XDestroyImage(IntPtr ximage);

0 commit comments

Comments
 (0)