@@ -24,7 +24,6 @@ import * as Crypto from "effect/Crypto";
2424import * as DateTime from "effect/DateTime" ;
2525import * as Effect from "effect/Effect" ;
2626import * as Encoding from "effect/Encoding" ;
27- import * as Exit from "effect/Exit" ;
2827import * as FileSystem from "effect/FileSystem" ;
2928import * as Layer from "effect/Layer" ;
3029import * as Option from "effect/Option" ;
@@ -34,7 +33,7 @@ import * as Schema from "effect/Schema";
3433import * as Semaphore from "effect/Semaphore" ;
3534
3635import * as Electron from "electron" ;
37- import { activeWindow , type Result as ActiveWindow } from "get-windows" ;
36+ import type { Result as ActiveWindow } from "get-windows" ;
3837
3938import * as DesktopEnvironment from "../app/DesktopEnvironment.ts" ;
4039import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts" ;
@@ -44,10 +43,13 @@ import { startMacModifierPairShortcutProcess } from "./MacModifierPairShortcutPr
4443import { captureMacWindowSnapshot , type MacSnapShotSource } from "./MacSnapShot.ts" ;
4544import type { LinuxCaptureFeedback , LinuxWindowMetadata } from "./LinuxSnapShot.ts" ;
4645import { niriSocketPath } from "./NiriSnapShot.ts" ;
47- import { niriCaptureBinding , startNiriCaptureShortcut } from "./NiriCaptureShortcut.ts" ;
4846import { CaptureShortcutConfig , niriCaptureConfigPath } from "./CaptureShortcutConfig.ts" ;
49- import { GnomeCaptureSetup , isGnomeCaptureSession } from "./GnomeCaptureSetup.ts" ;
50- import { PortalCaptureShortcut , portalShortcutTrigger } from "./PortalCaptureShortcut.ts" ;
47+ import type { PortalCaptureShortcut } from "./PortalCaptureShortcut.ts" ;
48+ import {
49+ isGnomeCaptureSession ,
50+ niriCaptureBinding ,
51+ portalShortcutTrigger ,
52+ } from "./linuxCaptureSession.ts" ;
5153import {
5254 HyprlandCaptureSetup ,
5355 HYPRLAND_CAPTURE_EXECUTABLE ,
@@ -72,8 +74,8 @@ import { windowsAppIcon } from "./WindowsWindowIcon.ts";
7274
7375import {
7476 boundedSnapShotString ,
75- hideAndWaitForBlur ,
7677 isWaylandSession ,
78+ sameSnapShotShortcut ,
7779 toElectronAccelerator ,
7880 snapShotShortcutRegistrationFailureMessage ,
7981 snapShotShortcutSystemConflict ,
@@ -180,8 +182,6 @@ export class DesktopSnapShot extends Context.Service<
180182 readonly setShortcutSuppressed : ( suppressed : boolean ) => Effect . Effect < void > ;
181183 /** Capture the foreground window in place, including T3 Code itself. */
182184 readonly capture : Effect . Effect < void , DesktopSnapShotError > ;
183- /** Capture from the command palette, revealing the previous app first. */
184- readonly captureNow : Effect . Effect < void , DesktopSnapShotError > ;
185185 readonly listPending : Effect . Effect <
186186 ReadonlyArray < DesktopPendingSnapShot > ,
187187 DesktopSnapShotError
@@ -388,10 +388,7 @@ export function snapShotImageSize(png: Buffer, fallback: Electron.Rectangle): El
388388 } ;
389389}
390390
391- type SnapShotTarget = "foreground" | "previous-app" ;
392-
393391async function captureSource ( {
394- target,
395392 mode,
396393 captureId,
397394 platform,
@@ -406,7 +403,6 @@ async function captureSource({
406403 prepareReveal,
407404 onLinuxFeedback,
408405} : {
409- target : SnapShotTarget ;
410406 mode : DesktopSnapShotState [ "mode" ] ;
411407 captureId : string ;
412408 platform : NodeJS . Platform ;
@@ -425,17 +421,15 @@ async function captureSource({
425421 let linuxWindow : LinuxWindowMetadata | undefined ;
426422 let linuxFeedback : LinuxCaptureFeedback | undefined ;
427423 let linuxActivationFailure : { readonly cause : unknown } | undefined ;
428- const focusedWindow = Electron . BrowserWindow . getFocusedWindow ( ) ;
429- const hiddenWindow = target === "previous-app" ? focusedWindow : undefined ;
430424 const destinationWindow =
431- focusedWindow ?? Electron . BrowserWindow . getAllWindows ( ) . find ( ( window ) => ! window . isDestroyed ( ) ) ;
425+ Electron . BrowserWindow . getFocusedWindow ( ) ??
426+ Electron . BrowserWindow . getAllWindows ( ) . find ( ( window ) => ! window . isDestroyed ( ) ) ;
432427 const destinationWindowBounds = destinationWindow ?. getBounds ( ) ;
433- let hiddenWindowRestored = false ;
434- try {
428+ {
435429 const revealPreparation =
436430 platform === "win32" ? prepareReveal ( ) . catch ( ( ) => undefined ) : Promise . resolve ( ) ;
437- if ( hiddenWindow ) await hideAndWaitForBlur ( hiddenWindow ) ;
438431 if ( mode === "direct" ) {
432+ const { activeWindow } = await import ( "get-windows" ) ;
439433 active = await activeWindow ( {
440434 accessibilityPermission : false ,
441435 screenRecordingPermission : platform === "darwin" ,
@@ -526,10 +520,6 @@ async function captureSource({
526520 }
527521 const contextPromise = accessibilityRead ?. result ?? Promise . resolve ( undefined ) ;
528522 await revealPreparation ;
529- if ( platform !== "win32" && hiddenWindow && ! hiddenWindow . isDestroyed ( ) ) {
530- hiddenWindow . show ( ) ;
531- hiddenWindowRestored = true ;
532- }
533523 if ( linuxFeedback && destinationWindow && ! destinationWindow . isDestroyed ( ) ) {
534524 if ( destinationWindow . isMinimized ( ) ) destinationWindow . restore ( ) ;
535525 if ( ! destinationWindow . isVisible ( ) ) destinationWindow . show ( ) ;
@@ -549,10 +539,6 @@ async function captureSource({
549539 platform ,
550540 destinationWindowBounds ,
551541 ) ) ;
552- if ( platform === "win32" && hiddenWindow && ! hiddenWindow . isDestroyed ( ) ) {
553- hiddenWindow . show ( ) ;
554- hiddenWindowRestored = true ;
555- }
556542 return {
557543 source,
558544 active,
@@ -563,8 +549,6 @@ async function captureSource({
563549 png,
564550 imageTempReady,
565551 } ;
566- } finally {
567- if ( ! hiddenWindowRestored && hiddenWindow && ! hiddenWindow . isDestroyed ( ) ) hiddenWindow . show ( ) ;
568552 }
569553}
570554
@@ -780,6 +764,8 @@ export const make = Effect.gen(function* () {
780764 ) ;
781765 const accessibilityProcessPool = makeSnapShotAccessibilityProcessPool ( accessibilityWorkerPath ) ;
782766 let registeredAccelerator : string | undefined ;
767+ // False until the first applySettings; the first pass must always register.
768+ let initialized = false ;
783769 let portalShortcut : PortalCaptureShortcut | undefined ;
784770 let shortcutGeneration = 0 ;
785771 let shortcutSuppressed = false ;
@@ -869,7 +855,6 @@ export const make = Effect.gen(function* () {
869855
870856 const prepareCapture = Effect . fn ( "desktop.snapShot.prepareCapture" ) ( function * (
871857 settings : ClientSettings ,
872- target : SnapShotTarget ,
873858 ) {
874859 const id = yield * crypto . randomUUIDv4 . pipe ( Effect . mapError ( ( cause ) => captureFailure ( cause ) ) ) ;
875860 const mode = captureMode ( environment . platform ) ;
@@ -891,7 +876,6 @@ export const make = Effect.gen(function* () {
891876 const snapshot = yield * Effect . tryPromise ( {
892877 try : ( ) =>
893878 captureSource ( {
894- target,
895879 mode,
896880 captureId : id ,
897881 platform : environment . platform ,
@@ -918,13 +902,9 @@ export const make = Effect.gen(function* () {
918902 ) ;
919903 }
920904 if ( snapshot . animationStarted ) {
921- const action = `snap-shot-started:${ id } ` ;
922- const revealExit = yield * Effect . exit ( desktopWindow . dispatchMenuAction ( action ) ) ;
923- if ( Exit . isFailure ( revealExit ) ) {
924- yield * desktopWindow
925- . dispatchMenuAction ( action , { reveal : false } )
926- . pipe ( Effect . catchCause ( ( ) => Effect . void ) ) ;
927- }
905+ yield * desktopWindow
906+ . dispatchMenuAction ( `snap-shot-started:${ id } ` )
907+ . pipe ( Effect . catchCause ( ( ) => Effect . void ) ) ;
928908 } else {
929909 yield * desktopWindow . activate . pipe ( Effect . catchCause ( ( ) => Effect . void ) ) ;
930910 }
@@ -987,13 +967,14 @@ export const make = Effect.gen(function* () {
987967 } ) . pipe ( Effect . mapError ( ( cause ) => captureFailure ( cause , id ) ) ) ;
988968 } ) ;
989969
990- const captureTarget = Effect . fn ( "desktop.snapShot.captureTarget" ) ( function * (
991- target : SnapShotTarget ,
992- ) {
970+ const capture = Effect . gen ( function * ( ) {
993971 const settings = yield * Ref . get ( settingsRef ) ;
972+ if ( ! settings . snapShotEnabled ) {
973+ return yield * new DesktopSnapShotError ( { operation : "disabled" } ) ;
974+ }
994975 // Only source acquisition and the initial handoff require exclusive access.
995976 // Each captured image can finish its own accessibility read and persistence.
996- const prepared = yield * prepareCapture ( settings , target ) . pipe (
977+ const prepared = yield * prepareCapture ( settings ) . pipe (
997978 Effect . tapError ( ( error ) =>
998979 ( error . captureId ? discardCapture ( error . captureId ) : Effect . void ) . pipe (
999980 Effect . andThen ( setFailure ( error . message , error . captureId ) ) ,
@@ -1022,17 +1003,7 @@ export const make = Effect.gen(function* () {
10221003 ) ,
10231004 ) ,
10241005 ) ;
1025- } ) ;
1026-
1027- const captureNow = captureTarget ( "previous-app" ) ;
1028-
1029- const capture = Effect . gen ( function * ( ) {
1030- const settings = yield * Ref . get ( settingsRef ) ;
1031- if ( ! settings . snapShotEnabled ) {
1032- return yield * new DesktopSnapShotError ( { operation : "disabled" } ) ;
1033- }
1034- yield * captureTarget ( "foreground" ) ;
1035- } ) ;
1006+ } ) . pipe ( Effect . withSpan ( "desktop.snapShot.capture" ) ) ;
10361007
10371008 const captureFromShortcut = Effect . gen ( function * ( ) {
10381009 if ( shortcutSuppressed ) return ;
@@ -1135,21 +1106,27 @@ export const make = Effect.gen(function* () {
11351106 transition . dispose ( ) ;
11361107 closeLinuxFeedback ( ) ;
11371108 }
1138- // Cosmetic capture preferences must not tear down an approved portal session.
1139- if (
1140- ! forceShortcut &&
1141- portalShortcut &&
1109+ // Every client-settings save lands here. Only the fields that decide which
1110+ // shortcut listener runs may tear it down; a font-size change must not
1111+ // uninstall a global keyboard hook or drop an approved portal session.
1112+ const shortcutInputsChanged =
1113+ settings . snapShotEnabled !== previousSettings . snapShotEnabled ||
1114+ settings . snapShotIncludeAccessibility !== previousSettings . snapShotIncludeAccessibility ||
1115+ ! sameSnapShotShortcut ( shortcut , previousSettings . snapShotShortcut ) ;
1116+ const portalShortcutUnchanged =
1117+ portalShortcut !== undefined &&
11421118 settings . snapShotEnabled &&
11431119 previousSettings . snapShotEnabled &&
11441120 ( isHyprlandCaptureSession ( ) ||
11451121 ( ! isModifierPairShortcut ( shortcut ) &&
11461122 ! isModifierPairShortcut ( previousSettings . snapShotShortcut ) &&
11471123 toElectronAccelerator ( shortcut ) ===
1148- toElectronAccelerator ( previousSettings . snapShotShortcut ) ) )
1149- ) {
1124+ toElectronAccelerator ( previousSettings . snapShotShortcut ) ) ) ;
1125+ if ( ! forceShortcut && ( portalShortcutUnchanged || ( initialized && ! shortcutInputsChanged ) ) ) {
11501126 yield * Ref . update ( stateRef , ( state ) => ( { ...state , shortcut } ) ) ;
11511127 return ;
11521128 }
1129+ initialized = true ;
11531130 releaseShortcut ( ) ;
11541131 shortcutVerified = false ;
11551132 const generation = shortcutGeneration ;
@@ -1189,13 +1166,14 @@ export const make = Effect.gen(function* () {
11891166 return ;
11901167 }
11911168 if ( mode === "portal" && niriSocketPath ( ) ) {
1192- const registered = yield * Effect . tryPromise ( ( ) =>
1193- startNiriCaptureShortcut ( linuxAppId , onCurrentShortcut , ( ) => {
1169+ const registered = yield * Effect . tryPromise ( async ( ) => {
1170+ const { startNiriCaptureShortcut } = await import ( "./NiriCaptureShortcut.ts" ) ;
1171+ return startNiriCaptureShortcut ( linuxAppId , onCurrentShortcut , ( ) => {
11941172 void runPromise (
11951173 setShortcutFailure ( "The Niri capture endpoint disconnected. Restart T3 Code." ) ,
11961174 ) . catch ( ( ) => undefined ) ;
1197- } ) ,
1198- ) . pipe (
1175+ } ) ;
1176+ } ) . pipe (
11991177 Effect . tap ( ( stop ) =>
12001178 Effect . sync ( ( ) => {
12011179 stopShiftShortcut = stop ;
@@ -1238,32 +1216,32 @@ export const make = Effect.gen(function* () {
12381216 shortcutMessage : null ,
12391217 message : null ,
12401218 } ) ;
1241- yield * Effect . try (
1242- ( ) =>
1243- new PortalCaptureShortcut (
1244- linuxAppId ,
1245- isModifierPairShortcut ( shortcut )
1246- ? {
1247- key : "2" ,
1248- ctrlKey : true ,
1249- modKey : false ,
1250- altKey : false ,
1251- shiftKey : true ,
1252- metaKey : false ,
1253- }
1254- : shortcut ,
1255- onCurrentShortcut ,
1256- ( ) => {
1257- if ( generation !== shortcutGeneration ) return ;
1258- shortcutVerified = false ;
1259- void runPromise ( desktopWindow . dispatchMenuAction ( "snap-shot-shortcut-changed" ) ) . catch (
1260- ( ) => undefined ,
1261- ) ;
1262- } ,
1263- undefined ,
1264- hyprland ,
1265- ) ,
1266- ) . pipe (
1219+ yield * Effect . tryPromise ( async ( ) => {
1220+ const { PortalCaptureShortcut } = await import ( "./PortalCaptureShortcut.ts" ) ;
1221+ return new PortalCaptureShortcut (
1222+ linuxAppId ,
1223+ isModifierPairShortcut ( shortcut )
1224+ ? {
1225+ key : "2" ,
1226+ ctrlKey : true ,
1227+ modKey : false ,
1228+ altKey : false ,
1229+ shiftKey : true ,
1230+ metaKey : false ,
1231+ }
1232+ : shortcut ,
1233+ onCurrentShortcut ,
1234+ ( ) => {
1235+ if ( generation !== shortcutGeneration ) return ;
1236+ shortcutVerified = false ;
1237+ void runPromise ( desktopWindow . dispatchMenuAction ( "snap-shot-shortcut-changed" ) ) . catch (
1238+ ( ) => undefined ,
1239+ ) ;
1240+ } ,
1241+ undefined ,
1242+ hyprland ,
1243+ ) ;
1244+ } ) . pipe (
12671245 Effect . tap ( ( registration ) =>
12681246 Effect . sync ( ( ) => {
12691247 portalShortcut = registration ;
@@ -1386,6 +1364,7 @@ export const make = Effect.gen(function* () {
13861364 } ) ;
13871365 yield * Effect . tryPromise ( {
13881366 try : async ( ) => {
1367+ const { GnomeCaptureSetup } = await import ( "./GnomeCaptureSetup.ts" ) ;
13891368 const setup = new GnomeCaptureSetup ( gnomeSetupPaths ) ;
13901369 try {
13911370 await setup . perform ( action ) ;
@@ -1527,7 +1506,7 @@ export const make = Effect.gen(function* () {
15271506 state . message !== null &&
15281507 MAC_PERMISSION_MESSAGES . has ( state . message )
15291508 ? yield * configurationMutex
1530- . withPermits ( 1 ) ( applySettings ( settings , null ) )
1509+ . withPermits ( 1 ) ( applySettings ( settings , null , true ) )
15311510 . pipe ( Effect . andThen ( Ref . get ( stateRef ) ) )
15321511 : state ;
15331512 return { ...recovered , macPermissions, ...( message ? { message } : { } ) } ;
@@ -1550,6 +1529,7 @@ export const make = Effect.gen(function* () {
15501529 : undefined ;
15511530 const gnomeExtension = hasGnomeSetup ( )
15521531 ? yield * Effect . promise ( async ( ) => {
1532+ const { GnomeCaptureSetup } = await import ( "./GnomeCaptureSetup.ts" ) ;
15531533 const setup = new GnomeCaptureSetup ( gnomeSetupPaths ) ;
15541534 try {
15551535 return await setup . state ( ) ;
@@ -1601,7 +1581,6 @@ export const make = Effect.gen(function* () {
16011581 checkShortcut,
16021582 setShortcutSuppressed,
16031583 capture,
1604- captureNow,
16051584 listPending : fileSystem . readDirectory ( captureDirectory ) . pipe (
16061585 Effect . catchTags ( {
16071586 PlatformError : ( cause ) =>
0 commit comments