1679 lines · 60.5 KB · TypeScript
RawDownload
// @effect-diagnostics globalTimers:off -- Capture timeouts and Electron overlay animation timers run at native callback boundaries outside Effect fibers. import {  DEFAULT_CLIENT_SETTINGS,  DesktopPendingSnapShot,  isModifierPairShortcut,  snapShotModifierPairLabel,  snapShotShortcutModifierPair,  type DesktopSnapShot as DesktopSnapShotValue,  type DesktopSnapShotShortcutAvailability,  type DesktopSnapShotState,  DesktopSnapShotSetupAction,  type DesktopCaptureConfigRequest,  type DesktopCaptureConfigPreview,  type DesktopCaptureConfigApplied,  type ClientSettings,  type SnapShotModifier,  type SnapShotModifierPairShortcut,  type SnapShotShortcut,  type DesktopSnapShotEvent,  type DesktopSnapShotId,} from "@t3tools/contracts";import * as Clock from "effect/Clock";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Semaphore from "effect/Semaphore"; import * as Electron from "electron"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts";import * as DesktopWindow from "../window/DesktopWindow.ts";import { startGlobalShiftShortcutProcess } from "./GlobalShiftShortcutProcess.ts";import { startMacModifierPairShortcutProcess } from "./MacModifierPairShortcutProcess.ts";import { activeWindow, type ActiveWindow } from "./ActiveWindow.ts";import { captureMacWindowSnapshot, type MacSnapShotSource } from "./MacSnapShot.ts";import type { LinuxCaptureFeedback, LinuxWindowMetadata } from "./LinuxSnapShot.ts";import { niriSocketPath } from "./NiriSnapShot.ts";import { CaptureShortcutConfig, niriCaptureConfigPath } from "./CaptureShortcutConfig.ts";import type { PortalCaptureShortcut } from "./PortalCaptureShortcut.ts";import {  isGnomeCaptureSession,  niriCaptureBinding,  portalShortcutTrigger,} from "./linuxCaptureSession.ts";import {  HyprlandCaptureSetup,  HYPRLAND_CAPTURE_EXECUTABLE,  isHyprlandCaptureSession,  hyprlandCaptureShortcut,  type HyprlandCapturePaths,} from "./HyprlandSnapShot.ts";import {  KdeCaptureSetup,  KDE_CAPTURE_EXECUTABLE,  isKdeCaptureSession,  type KdeCapturePaths,} from "./KdeSnapShot.ts";import {  captureRegionWindowSnapshot,  makeRegionSnapShotPool,  type RegionSnapShotPool,  type RegionSnapShotSource,} from "./RegionSnapShot.ts";import { type SnapShotAnimationDestination, SnapShotTransition } from "./SnapShotTransition.ts";import {  type AccessibilityProcessPool,  makeSnapShotAccessibilityProcessPool,} from "./SnapShotAccessibilityProcess.ts";import { showWindowsCaptureOverlay } from "./WindowsCaptureFeedback.ts"; import {  boundedSnapShotString,  isWaylandSession,  sameSnapShotShortcut,  toElectronAccelerator,  snapShotShortcutRegistrationFailureMessage,  snapShotShortcutSystemConflict,} from "./snapShot.ts"; const MAX_CAPTURE_WIDTH = 2_560;const MAX_CAPTURE_HEIGHT = 1_600;const SHORTCUT_COOLDOWN_NS = 200_000_000n;const WAYLAND_MODIFIER_PAIR_UNAVAILABLE_MESSAGE =  "Modifier-pair shortcuts aren't available in this Wayland session. Choose another shortcut or use Take snapshot from the command palette.";const FLASH_ANIMATION_DURATION_MS = 180;const FLASH_STATIC_DURATION_MS = 60;const FLASH_FRAME_INTERVAL_MS = 16;const FLASH_PEAK_OPACITY = 0.08;const MAC_SCREEN_CAPTURE_SETTINGS_URL =  "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture";const MAC_SCREEN_CAPTURE_PERMISSION_MESSAGE =  "Allow Screen Recording in System Settings, then restart T3 Code.";const MAC_ACCESSIBILITY_PERMISSION_MESSAGE =  "Allow Accessibility in System Settings, then restart T3 Code.";const MAC_BOTH_PERMISSIONS_MESSAGE =  "Allow Accessibility and Screen Recording in System Settings, then restart T3 Code.";const MAC_PERMISSION_MESSAGES = new Set([  MAC_SCREEN_CAPTURE_PERMISSION_MESSAGE,  MAC_ACCESSIBILITY_PERMISSION_MESSAGE,  MAC_BOTH_PERMISSIONS_MESSAGE,]); const decodePendingCapture = Schema.decodeUnknownEffect(DesktopPendingSnapShot); const PendingCaptureJson = Schema.fromJsonString(DesktopPendingSnapShot);const decodePendingCaptureJson = Schema.decodeEffect(PendingCaptureJson);const encodePendingCaptureJson = Schema.encodeEffect(PendingCaptureJson);const DesktopSnapShotOperation = Schema.Literals([  "list-pending",  "read",  "acknowledge",  "unsupported",  "disabled",  "no-window-selected",  "window-unavailable",  "capture",]); export class DesktopSnapShotError extends Schema.TaggedError<DesktopSnapShotError>()(  "DesktopSnapShotError",  {    operation: DesktopSnapShotOperation,    captureId: Schema.optional(Schema.String),    cause: Schema.optional(Schema.Defect()),  },) {  override get message(): string {    switch (this.operation) {      case "list-pending":        return "Could not list pending snapshots.";      case "read":        return "Could not read the snapshot.";      case "acknowledge":        return "Could not remove the snapshot.";      case "unsupported":        return "SnapShots are not supported here.";      case "disabled":        return "Enable SnapShots in Settings first.";      case "no-window-selected":        return "No window was selected.";      case "window-unavailable":        return "The active window is not available for capture.";      case "capture":        return "Could not capture the active window.";    }  }} const isDesktopSnapShotError = Schema.is(DesktopSnapShotError); function captureFailure(cause: unknown, captureId?: string): DesktopSnapShotError {  return isDesktopSnapShotError(cause)    ? cause    : new DesktopSnapShotError({ operation: "capture", captureId, cause });} export class DesktopSnapShot extends Context.Service<  DesktopSnapShot,  {    readonly initialize: Effect.Effect<void>;    readonly configure: (settings: ClientSettings) => Effect.Effect<void>;    readonly requestPermissions: (includeAccessibility: boolean) => Effect.Effect<void>;    readonly state: Effect.Effect<DesktopSnapShotState>;    readonly setup: (      action: DesktopSnapShotSetupAction,    ) => Effect.Effect<void, DesktopSnapShotSetupError>;    readonly previewConfig: (      request: DesktopCaptureConfigRequest,      selectedPath?: string,    ) => Effect.Effect<DesktopCaptureConfigPreview, DesktopSnapShotSetupError>;    readonly applyConfig: (      previewId: string,    ) => Effect.Effect<DesktopCaptureConfigApplied, DesktopSnapShotSetupError>;    readonly checkShortcut: (      shortcut: SnapShotShortcut,    ) => Effect.Effect<DesktopSnapShotShortcutAvailability>;    readonly setShortcutSuppressed: (suppressed: boolean) => Effect.Effect<void>;    /** Capture the foreground window in place, including T3 Code itself. */    readonly capture: Effect.Effect<void, DesktopSnapShotError>;    readonly listPending: Effect.Effect<      ReadonlyArray<DesktopPendingSnapShot>,      DesktopSnapShotError    >;    readonly read: (id: string) => Effect.Effect<DesktopSnapShotValue, DesktopSnapShotError>;    readonly setAnimationDestination: (      id: string,      destination: SnapShotAnimationDestination,    ) => Effect.Effect<void>;    readonly dismissAnimation: (id: string) => Effect.Effect<void>;    readonly acknowledge: (id: string) => Effect.Effect<void, DesktopSnapShotError>;  }>()("@t3tools/desktop/snapShot/DesktopSnapShot") {} export class DesktopSnapShotSetupError extends Schema.TaggedError<DesktopSnapShotSetupError>()(  "DesktopSnapShotSetupError",  {    action: Schema.Union([      DesktopSnapShotSetupAction,      Schema.Literals(["preview-config", "apply-config"]),    ]),    reason: Schema.Literals(["unsupported-session", "setup-failed", "shortcut-permissions"]),    cause: Schema.optional(Schema.Defect()),  },) {  override get message(): string {    if (this.action === "preview-config" || this.action === "apply-config") {      if (this.reason === "unsupported-session")        return "Config setup requires a Niri or Hyprland session.";      return this.action === "preview-config"        ? "Couldn't prepare your capture shortcut changes."        : "Couldn't save your capture shortcut.";    }    const kde = this.action === "install-kde-helper" || this.action === "remove-kde-helper";    const hyprland =      this.action === "install-hyprland-helper" || this.action === "remove-hyprland-helper";    if (this.reason === "unsupported-session")      return hyprland        ? "Helper setup requires a Hyprland Wayland session outside a sandbox."        : kde          ? "Helper setup requires a KDE Plasma Wayland session outside a sandbox."          : "Extension setup requires a GNOME Wayland session outside a sandbox.";    if (this.reason === "shortcut-permissions") return "Could not open shortcut permissions.";    return hyprland      ? "Could not set up Hyprland capture."      : kde        ? "Could not set up KDE capture."        : "Could not set up the GNOME extension.";  }} type SnapShotSystemAnimationSettings = Pick<  ReturnType<typeof Electron.systemPreferences.getAnimationSettings>,  "prefersReducedMotion" | "shouldRenderRichAnimation">; function captureMode(platform: NodeJS.Platform): DesktopSnapShotState["mode"] {  if (platform === "linux")    return isWaylandSession(platform, process.env) ? "portal" : "unavailable";  return platform === "darwin" || platform === "win32" ? "direct" : "unavailable";} export function shouldAnimateSnapShot(settings: SnapShotSystemAnimationSettings): boolean {  return settings.shouldRenderRichAnimation && !settings.prefersReducedMotion;} export function snapShotThumbnailSize(active: ActiveWindow | undefined): Electron.Size {  if (!active) return { width: 2_560, height: 1_600 };  return {    width: Math.min(Math.max(active.bounds.width, 1), MAX_CAPTURE_WIDTH),    height: Math.min(Math.max(active.bounds.height, 1), MAX_CAPTURE_HEIGHT),  };} export function snapShotIconDataUrl(  ...icons: ReadonlyArray<Electron.NativeImage | null | undefined>): string | undefined {  const icon = icons.find((candidate): candidate is Electron.NativeImage =>    Boolean(candidate && !candidate.isEmpty()),  );  if (!icon) return undefined;  return icon.resize({ width: 64, height: 64, quality: "best" }).toDataURL({ scaleFactor: 2 });} async function appFileIcon(  path: string,  platform: NodeJS.Platform,): Promise<Electron.NativeImage | undefined> {  if (platform === "darwin") {    const thumbnail = await Electron.nativeImage      .createThumbnailFromPath(path, { width: 64, height: 64 })      .catch(() => undefined);    if (thumbnail && !thumbnail.isEmpty()) return thumbnail;  }  return Electron.app.getFileIcon(path, { size: "normal" }).catch(() => undefined);} export async function iconDataUrl(  source: { readonly appIcon?: Electron.NativeImage | null },  active: ActiveWindow | undefined,  platform: NodeJS.Platform,): Promise<string | undefined> {  try {    const fileIcon = active?.owner.path      ? await appFileIcon(active.owner.path, platform)      : undefined;    return snapShotIconDataUrl(fileIcon, source.appIcon);  } catch {    return undefined;  }} function snapShotAppName(  active: ActiveWindow | undefined,  linuxWindow: LinuxWindowMetadata | undefined,  sourceName: string,): string {  const appName =    active?.owner.name.trim() || linuxWindow?.appName.trim() || sourceName.trim() || "Window";  if (active?.platform !== "windows") return appName;  return appName.replace(/\.exe$/i, "").trim() || appName;} async function requestMacScreenCapturePermission(): Promise<string | null> {  let status: ReturnType<typeof Electron.systemPreferences.getMediaAccessStatus>;  try {    status = Electron.systemPreferences.getMediaAccessStatus("screen");    if (status === "granted") return null;    if (status === "not-determined") {      try {        await Electron.desktopCapturer.getSources({          types: ["screen"],          thumbnailSize: { width: 1, height: 1 },        });      } catch {}      status = Electron.systemPreferences.getMediaAccessStatus("screen");      if (status === "granted") return null;    }  } catch {}  await Electron.shell.openExternal(MAC_SCREEN_CAPTURE_SETTINGS_URL).catch(() => undefined);  return MAC_SCREEN_CAPTURE_PERMISSION_MESSAGE;} function currentMacPermissions(): NonNullable<DesktopSnapShotState["macPermissions"]> {  return {    screenRecording: Electron.systemPreferences.getMediaAccessStatus("screen") === "granted",    accessibility: Electron.systemPreferences.isTrustedAccessibilityClient(false),  };} function macPermissionMessage(  permissions: NonNullable<DesktopSnapShotState["macPermissions"]>,  includeAccessibility: boolean,): string | null {  const accessibilityGranted = !includeAccessibility || permissions.accessibility;  if (!accessibilityGranted && !permissions.screenRecording) {    return MAC_BOTH_PERMISSIONS_MESSAGE;  }  if (!accessibilityGranted) {    return MAC_ACCESSIBILITY_PERMISSION_MESSAGE;  }  return permissions.screenRecording ? null : MAC_SCREEN_CAPTURE_PERMISSION_MESSAGE;} function currentMacSnapShotPermissionMessage(includeAccessibility: boolean): string | null {  return macPermissionMessage(    {      screenRecording: Electron.systemPreferences.getMediaAccessStatus("screen") === "granted",      accessibility:        !includeAccessibility || Electron.systemPreferences.isTrustedAccessibilityClient(false),    },    includeAccessibility,  );} async function requestMacSnapShotPermissions(  includeAccessibility: boolean,): Promise<string | null> {  const accessibilityGranted =    !includeAccessibility || Electron.systemPreferences.isTrustedAccessibilityClient(true);  const screenMessage = await requestMacScreenCapturePermission();  if (!accessibilityGranted && screenMessage) {    return MAC_BOTH_PERMISSIONS_MESSAGE;  }  if (!accessibilityGranted) {    return MAC_ACCESSIBILITY_PERMISSION_MESSAGE;  }  return screenMessage;} function snapShotImageSize(png: Buffer, fallback: Electron.Rectangle): Electron.Size {  try {    const size = Electron.nativeImage.createFromBuffer(png).getSize();    if (size.width > 0 && size.height > 0) return size;  } catch {}  return {    width: Math.max(1, Math.round(fallback.width)),    height: Math.max(1, Math.round(fallback.height)),  };} async function captureSource({  mode,  captureId,  platform,  settings,  flash,  transition,  imageTempPath,  linuxAppId,  kdeCapturePaths,  hyprlandCapturePaths,  accessibilityProcessPool,  regionSnapShotPool,  prepareReveal,  onLinuxFeedback,}: {  mode: DesktopSnapShotState["mode"];  captureId: string;  platform: NodeJS.Platform;  settings: ClientSettings;  flash: SnapShotFlash;  transition: SnapShotTransition;  imageTempPath: string;  linuxAppId: string;  kdeCapturePaths: KdeCapturePaths;  hyprlandCapturePaths: HyprlandCapturePaths;  accessibilityProcessPool: AccessibilityProcessPool;  regionSnapShotPool: RegionSnapShotPool;  prepareReveal: () => Promise<void>;  onLinuxFeedback: (feedback: LinuxCaptureFeedback) => void;}) {  let active: ActiveWindow | undefined;  let linuxWindow: LinuxWindowMetadata | undefined;  let linuxFeedback: LinuxCaptureFeedback | undefined;  let linuxActivationFailure: { readonly cause: unknown } | undefined;  const destinationWindow =    Electron.BrowserWindow.getFocusedWindow() ??    Electron.BrowserWindow.getAllWindows().find((window) => !window.isDestroyed());  const destinationWindowBounds = destinationWindow?.getBounds();  {    const revealPreparation =      platform === "win32" ? prepareReveal().catch(() => undefined) : Promise.resolve();    if (mode === "direct") {      active = await activeWindow(platform);    }     let source: MacSnapShotSource | RegionSnapShotSource | Electron.DesktopCapturerSource;    let png: Buffer;    let imageTempReady = false;    if (platform === "darwin") {      if (!active) {        throw new DesktopSnapShotError({ operation: "window-unavailable", captureId });      }      ({ source, png } = await captureMacWindowSnapshot(        active,        imageTempPath,        snapShotThumbnailSize(active),      ));      imageTempReady = true;    } else if (mode === "direct") {      if (!active) {        throw new DesktopSnapShotError({ operation: "window-unavailable", captureId });      }      ({ source, png } = await captureRegionWindowSnapshot(        regionSnapShotPool,        active,        snapShotFlashBounds(active, platform),        snapShotThumbnailSize(active),      ));    } else {      const { captureLinuxWindow } = await import("./LinuxSnapShot.ts");      const snapshot = await captureLinuxWindow(        linuxAppId,        {          flash: settings.snapShotFlash,          animate:            settings.snapShotAnimations &&            shouldAnimateSnapShot(Electron.systemPreferences.getAnimationSettings()),        },        kdeCapturePaths,        hyprlandCapturePaths,      );      if (snapshot) {        linuxFeedback = snapshot.feedback;        if (linuxFeedback) onLinuxFeedback(linuxFeedback);        linuxWindow = snapshot.window;        source = { name: linuxWindow?.title || "Active window" };        png = snapshot.png;      } else {        const [selected] = await Electron.desktopCapturer.getSources({          types: ["window", "screen"],          thumbnailSize: snapShotThumbnailSize(active),          fetchWindowIcons: true,        });        if (!selected || selected.thumbnail.isEmpty()) {          throw new DesktopSnapShotError({            operation: "no-window-selected",            captureId,          });        }        source = selected;        png = selected.thumbnail.toPNG();      }    }    const accessibleIdentity = active      ? { ...active, bounds: snapShotFlashBounds(active, platform) }      : linuxWindow?.processId        ? {            title: linuxWindow.title,            bounds: linuxWindow.bounds,

Lines 1–500 of 1679.