@bitefoundry/retrieviewer 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ASSET-PROVENANCE.md +43 -0
- package/ASSETS-LICENSE.md +58 -0
- package/ATTRIBUTION.md +54 -0
- package/EMBEDDING.md +96 -0
- package/INSTALLING.md +126 -0
- package/LICENSE +16 -0
- package/LICENSE-CODE +21 -0
- package/PLUGIN.md +110 -0
- package/PUPPY-MEMORY.md +57 -0
- package/PUPPY-Q5-COMPARISON.md +73 -0
- package/README.md +86 -2
- package/RELEASING.md +70 -0
- package/THIRD-PARTY-NOTICES.md +17 -0
- package/TRADEMARKS.md +21 -0
- package/WINDOWS-COMPANION.md +113 -0
- package/braille.mjs +45 -0
- package/examples/bridge.py +62 -0
- package/examples/embed.mjs +15 -0
- package/examples/live.mjs +40 -0
- package/examples/plugin.mjs +21 -0
- package/integrations/opencode/runtime.mjs +195 -0
- package/integrations/opencode/tui.tsx +59 -0
- package/integrations/windows-overlay/Overlay.cs +259 -0
- package/integrations/windows-overlay/desktop.mjs +87 -0
- package/integrations/windows-overlay/protocol.mjs +39 -0
- package/integrations/windows-overlay/worker.mjs +60 -0
- package/locomotion-core.mjs +21 -0
- package/locomotion-puppy.animal +0 -0
- package/package.json +131 -4
- package/pet-q5-manifest.json +118 -0
- package/puppy-clean.mjs +51 -0
- package/puppy-companion-interactions-v3-q5.bin.gz +0 -0
- package/puppy-companion-interactions-v3.json +1 -0
- package/puppy-companion-life.mjs +182 -0
- package/puppy-companion-objects.mjs +30 -0
- package/puppy-companion-sprites.mjs +14 -0
- package/puppy-continuous.mjs +57 -0
- package/puppy-face.mjs +46 -0
- package/puppy-frame-predictive.mjs +147 -0
- package/puppy-frame-store.mjs +108 -0
- package/puppy-mouth-anchor.json +1 -0
- package/puppy-mouth-anchor.mjs +33 -0
- package/puppy-paper-actions-v9-q5.bin.gz +0 -0
- package/puppy-paper-actions-v9.json +1 -0
- package/puppy-paper-actions.mjs +48 -0
- package/puppy-paper-approach-q5.bin.gz +0 -0
- package/puppy-paper-approach.json +1 -0
- package/puppy-paper-area.mjs +15 -0
- package/puppy-paper-arrival-seat-q5.bin.gz +0 -0
- package/puppy-paper-arrival-seat.json +1 -0
- package/puppy-paper-behaviors-q5.bin.gz +0 -0
- package/puppy-paper-behaviors.json +1 -0
- package/puppy-paper-behaviors.mjs +8 -0
- package/puppy-paper-contact.mjs +103 -0
- package/puppy-paper-director.mjs +21 -0
- package/puppy-paper-paw-transport.mjs +76 -0
- package/puppy-paper-rear-gallop-q5.bin.gz +0 -0
- package/puppy-paper-rear-gallop.json +1 -0
- package/puppy-paper-seat-q5.bin.gz +0 -0
- package/puppy-paper-seat.json +14 -0
- package/puppy-paper-seat.mjs +13 -0
- package/puppy-paper-trick-timing.mjs +58 -0
- package/puppy-paper-world.mjs +108 -0
- package/puppy-paper.mjs +271 -0
- package/puppy-stable-meta.json +1606 -0
- package/puppy-stable.mjs +72 -0
- package/puppy-stride-meta.json +343 -0
- package/puppy-stride-tweens-q5.bin.gz +0 -0
- package/puppy-zoomies.mjs +27 -0
- package/puppy-zoomies30.mjs +12 -0
- package/retrieviewer-cli.mjs +94 -0
- package/retrieviewer-config.mjs +49 -0
- package/retrieviewer-install.mjs +157 -0
- package/retrieviewer-plugin.d.mts +44 -0
- package/retrieviewer-plugin.mjs +97 -0
- package/retrieviewer-stdio.mjs +38 -0
- package/retrieviewer.d.mts +85 -0
- package/retrieviewer.mjs +142 -0
- package/vendor/procedural-animals/LICENSE +21 -0
- package/vendor/procedural-animals/package.json +40 -0
- package/vendor/procedural-animals/src/animal.js +146 -0
- package/vendor/procedural-animals/src/bake.js +51 -0
- package/vendor/procedural-animals/src/build.js +81 -0
- package/vendor/procedural-animals/src/core/build/coatKit.js +121 -0
- package/vendor/procedural-animals/src/core/build/featherCards.js +189 -0
- package/vendor/procedural-animals/src/core/build/finEdges.js +85 -0
- package/vendor/procedural-animals/src/core/build/furSlope.js +115 -0
- package/vendor/procedural-animals/src/core/build/mesh.js +67 -0
- package/vendor/procedural-animals/src/core/build/pipeline.js +272 -0
- package/vendor/procedural-animals/src/core/build/seams.js +183 -0
- package/vendor/procedural-animals/src/core/build/warp.js +112 -0
- package/vendor/procedural-animals/src/core/build/weights.js +393 -0
- package/vendor/procedural-animals/src/core/math/vec.js +81 -0
- package/vendor/procedural-animals/src/core/motion/actions.js +800 -0
- package/vendor/procedural-animals/src/core/motion/bird.js +3063 -0
- package/vendor/procedural-animals/src/core/motion/birdActions.js +1059 -0
- package/vendor/procedural-animals/src/core/motion/common.js +158 -0
- package/vendor/procedural-animals/src/core/motion/index.js +22 -0
- package/vendor/procedural-animals/src/core/motion/quadruped.js +2875 -0
- package/vendor/procedural-animals/src/core/motion/snake.js +1594 -0
- package/vendor/procedural-animals/src/core/motion/spider.js +1810 -0
- package/vendor/procedural-animals/src/core/motion/swimmer.js +1047 -0
- package/vendor/procedural-animals/src/core/motion/swimmerActions.js +613 -0
- package/vendor/procedural-animals/src/core/render/animalObject.js +262 -0
- package/vendor/procedural-animals/src/core/render/coatMaterial.js +1301 -0
- package/vendor/procedural-animals/src/core/render/dqs.js +159 -0
- package/vendor/procedural-animals/src/core/render/eyes.js +676 -0
- package/vendor/procedural-animals/src/core/render/membranes.js +77 -0
- package/vendor/procedural-animals/src/core/render/skeleton.js +41 -0
- package/vendor/procedural-animals/src/core/rig/bird.js +327 -0
- package/vendor/procedural-animals/src/core/rig/quadruped.js +76 -0
- package/vendor/procedural-animals/src/core/rig/rig.js +79 -0
- package/vendor/procedural-animals/src/core/rig/snake.js +76 -0
- package/vendor/procedural-animals/src/core/rig/spider.js +110 -0
- package/vendor/procedural-animals/src/core/rig/swimmer.js +181 -0
- package/vendor/procedural-animals/src/core/sdf/eyeSocket.js +53 -0
- package/vendor/procedural-animals/src/core/sdf/mesher.js +238 -0
- package/vendor/procedural-animals/src/core/sdf/sdf.js +263 -0
- package/vendor/procedural-animals/src/index.d.ts +98 -0
- package/vendor/procedural-animals/src/index.js +29 -0
- package/vendor/procedural-animals/src/registry.js +48 -0
- package/vendor/procedural-animals/src/species/bear/behaviour.js +463 -0
- package/vendor/procedural-animals/src/species/bear/claws.js +147 -0
- package/vendor/procedural-animals/src/species/bear/coat.js +726 -0
- package/vendor/procedural-animals/src/species/bear/index.js +139 -0
- package/vendor/procedural-animals/src/species/bear/motion.js +112 -0
- package/vendor/procedural-animals/src/species/bear/regions.js +78 -0
- package/vendor/procedural-animals/src/species/bear/rig.js +112 -0
- package/vendor/procedural-animals/src/species/bear/sculpt.js +272 -0
- package/vendor/procedural-animals/src/species/boar/behaviour.js +335 -0
- package/vendor/procedural-animals/src/species/boar/coat.js +372 -0
- package/vendor/procedural-animals/src/species/boar/index.js +110 -0
- package/vendor/procedural-animals/src/species/boar/motion.js +85 -0
- package/vendor/procedural-animals/src/species/boar/regions.js +88 -0
- package/vendor/procedural-animals/src/species/boar/rig.js +111 -0
- package/vendor/procedural-animals/src/species/boar/sculpt.js +285 -0
- package/vendor/procedural-animals/src/species/cat/behaviour.js +559 -0
- package/vendor/procedural-animals/src/species/cat/coat.js +841 -0
- package/vendor/procedural-animals/src/species/cat/index.js +211 -0
- package/vendor/procedural-animals/src/species/cat/motion.js +101 -0
- package/vendor/procedural-animals/src/species/cat/regions.js +82 -0
- package/vendor/procedural-animals/src/species/cat/rig.js +100 -0
- package/vendor/procedural-animals/src/species/cat/sculpt.js +266 -0
- package/vendor/procedural-animals/src/species/cat/whiskers.js +110 -0
- package/vendor/procedural-animals/src/species/cheetah/coat.js +427 -0
- package/vendor/procedural-animals/src/species/cheetah/index.js +51 -0
- package/vendor/procedural-animals/src/species/cheetah/joints.js +3 -0
- package/vendor/procedural-animals/src/species/cheetah/motion.js +77 -0
- package/vendor/procedural-animals/src/species/cheetah/regions.js +95 -0
- package/vendor/procedural-animals/src/species/cheetah/rig.js +66 -0
- package/vendor/procedural-animals/src/species/cheetah/sculpt.js +158 -0
- package/vendor/procedural-animals/src/species/chicken/coat.js +313 -0
- package/vendor/procedural-animals/src/species/chicken/coverts.js +320 -0
- package/vendor/procedural-animals/src/species/chicken/index.js +110 -0
- package/vendor/procedural-animals/src/species/chicken/motion.js +177 -0
- package/vendor/procedural-animals/src/species/chicken/regions.js +66 -0
- package/vendor/procedural-animals/src/species/chicken/rig.js +144 -0
- package/vendor/procedural-animals/src/species/chicken/sculpt.js +389 -0
- package/vendor/procedural-animals/src/species/chicken/wingLook.js +66 -0
- package/vendor/procedural-animals/src/species/chicken/wingStack.js +124 -0
- package/vendor/procedural-animals/src/species/cow/behaviour.js +521 -0
- package/vendor/procedural-animals/src/species/cow/coat.js +465 -0
- package/vendor/procedural-animals/src/species/cow/index.js +118 -0
- package/vendor/procedural-animals/src/species/cow/motion.js +81 -0
- package/vendor/procedural-animals/src/species/cow/regions.js +98 -0
- package/vendor/procedural-animals/src/species/cow/rig.js +116 -0
- package/vendor/procedural-animals/src/species/cow/sculpt.js +339 -0
- package/vendor/procedural-animals/src/species/crow/coat.js +145 -0
- package/vendor/procedural-animals/src/species/crow/index.js +70 -0
- package/vendor/procedural-animals/src/species/crow/joints.js +3 -0
- package/vendor/procedural-animals/src/species/crow/motion.js +101 -0
- package/vendor/procedural-animals/src/species/crow/regions.js +50 -0
- package/vendor/procedural-animals/src/species/crow/rig.js +83 -0
- package/vendor/procedural-animals/src/species/crow/sculpt.js +118 -0
- package/vendor/procedural-animals/src/species/deer/behaviour.js +513 -0
- package/vendor/procedural-animals/src/species/deer/coat.js +391 -0
- package/vendor/procedural-animals/src/species/deer/index.js +106 -0
- package/vendor/procedural-animals/src/species/deer/motion.js +97 -0
- package/vendor/procedural-animals/src/species/deer/regions.js +60 -0
- package/vendor/procedural-animals/src/species/deer/rig.js +124 -0
- package/vendor/procedural-animals/src/species/deer/sculpt.js +297 -0
- package/vendor/procedural-animals/src/species/dog/behaviour.js +521 -0
- package/vendor/procedural-animals/src/species/dog/coat.js +495 -0
- package/vendor/procedural-animals/src/species/dog/index.js +144 -0
- package/vendor/procedural-animals/src/species/dog/motion.js +118 -0
- package/vendor/procedural-animals/src/species/dog/regions.js +61 -0
- package/vendor/procedural-animals/src/species/dog/rig.js +129 -0
- package/vendor/procedural-animals/src/species/dog/sculpt.js +246 -0
- package/vendor/procedural-animals/src/species/eagle/coat.js +250 -0
- package/vendor/procedural-animals/src/species/eagle/index.js +76 -0
- package/vendor/procedural-animals/src/species/eagle/joints.js +3 -0
- package/vendor/procedural-animals/src/species/eagle/motion.js +137 -0
- package/vendor/procedural-animals/src/species/eagle/regions.js +57 -0
- package/vendor/procedural-animals/src/species/eagle/rig.js +107 -0
- package/vendor/procedural-animals/src/species/eagle/sculpt.js +137 -0
- package/vendor/procedural-animals/src/species/fish/coat.js +271 -0
- package/vendor/procedural-animals/src/species/fish/index.js +88 -0
- package/vendor/procedural-animals/src/species/fish/motion.js +92 -0
- package/vendor/procedural-animals/src/species/fish/regions.js +27 -0
- package/vendor/procedural-animals/src/species/fish/rig.js +143 -0
- package/vendor/procedural-animals/src/species/fish/sculpt.js +214 -0
- package/vendor/procedural-animals/src/species/fish/variants.js +126 -0
- package/vendor/procedural-animals/src/species/fox/behaviour.js +314 -0
- package/vendor/procedural-animals/src/species/fox/coat.js +444 -0
- package/vendor/procedural-animals/src/species/fox/index.js +120 -0
- package/vendor/procedural-animals/src/species/fox/motion.js +81 -0
- package/vendor/procedural-animals/src/species/fox/regions.js +59 -0
- package/vendor/procedural-animals/src/species/fox/rig.js +91 -0
- package/vendor/procedural-animals/src/species/fox/sculpt.js +202 -0
- package/vendor/procedural-animals/src/species/frog/behaviour.js +590 -0
- package/vendor/procedural-animals/src/species/frog/coat.js +305 -0
- package/vendor/procedural-animals/src/species/frog/index.js +97 -0
- package/vendor/procedural-animals/src/species/frog/motion.js +69 -0
- package/vendor/procedural-animals/src/species/frog/regions.js +46 -0
- package/vendor/procedural-animals/src/species/frog/rig.js +156 -0
- package/vendor/procedural-animals/src/species/frog/sculpt.js +240 -0
- package/vendor/procedural-animals/src/species/goat/behaviour.js +509 -0
- package/vendor/procedural-animals/src/species/goat/coat.js +590 -0
- package/vendor/procedural-animals/src/species/goat/index.js +149 -0
- package/vendor/procedural-animals/src/species/goat/motion.js +95 -0
- package/vendor/procedural-animals/src/species/goat/regions.js +98 -0
- package/vendor/procedural-animals/src/species/goat/rig.js +181 -0
- package/vendor/procedural-animals/src/species/goat/sculpt.js +547 -0
- package/vendor/procedural-animals/src/species/horse/coat.js +361 -0
- package/vendor/procedural-animals/src/species/horse/coatSeam.js +122 -0
- package/vendor/procedural-animals/src/species/horse/index.js +80 -0
- package/vendor/procedural-animals/src/species/horse/motion.js +245 -0
- package/vendor/procedural-animals/src/species/horse/regions.js +60 -0
- package/vendor/procedural-animals/src/species/horse/rig.js +117 -0
- package/vendor/procedural-animals/src/species/horse/sculpt.js +316 -0
- package/vendor/procedural-animals/src/species/horse/seamWeights.js +272 -0
- package/vendor/procedural-animals/src/species/lion/behaviour.js +509 -0
- package/vendor/procedural-animals/src/species/lion/coat.js +1016 -0
- package/vendor/procedural-animals/src/species/lion/index.js +111 -0
- package/vendor/procedural-animals/src/species/lion/motion.js +87 -0
- package/vendor/procedural-animals/src/species/lion/regions.js +106 -0
- package/vendor/procedural-animals/src/species/lion/rig.js +110 -0
- package/vendor/procedural-animals/src/species/lion/sculpt.js +384 -0
- package/vendor/procedural-animals/src/species/lion/whiskers.js +132 -0
- package/vendor/procedural-animals/src/species/pig/behaviour.js +575 -0
- package/vendor/procedural-animals/src/species/pig/coat.js +730 -0
- package/vendor/procedural-animals/src/species/pig/ears.js +242 -0
- package/vendor/procedural-animals/src/species/pig/index.js +161 -0
- package/vendor/procedural-animals/src/species/pig/motion.js +105 -0
- package/vendor/procedural-animals/src/species/pig/regions.js +100 -0
- package/vendor/procedural-animals/src/species/pig/rig.js +152 -0
- package/vendor/procedural-animals/src/species/pig/sculpt.js +382 -0
- package/vendor/procedural-animals/src/species/rabbit/behaviour.js +605 -0
- package/vendor/procedural-animals/src/species/rabbit/coat.js +285 -0
- package/vendor/procedural-animals/src/species/rabbit/index.js +85 -0
- package/vendor/procedural-animals/src/species/rabbit/motion.js +85 -0
- package/vendor/procedural-animals/src/species/rabbit/regions.js +48 -0
- package/vendor/procedural-animals/src/species/rabbit/rig.js +99 -0
- package/vendor/procedural-animals/src/species/rabbit/sculpt.js +148 -0
- package/vendor/procedural-animals/src/species/rat/behaviour.js +539 -0
- package/vendor/procedural-animals/src/species/rat/coat.js +341 -0
- package/vendor/procedural-animals/src/species/rat/index.js +111 -0
- package/vendor/procedural-animals/src/species/rat/motion.js +90 -0
- package/vendor/procedural-animals/src/species/rat/regions.js +92 -0
- package/vendor/procedural-animals/src/species/rat/rig.js +112 -0
- package/vendor/procedural-animals/src/species/rat/sculpt.js +163 -0
- package/vendor/procedural-animals/src/species/rat/whiskers.js +119 -0
- package/vendor/procedural-animals/src/species/shark/coat.js +383 -0
- package/vendor/procedural-animals/src/species/shark/index.js +104 -0
- package/vendor/procedural-animals/src/species/shark/motion.js +69 -0
- package/vendor/procedural-animals/src/species/shark/regions.js +34 -0
- package/vendor/procedural-animals/src/species/shark/rig.js +194 -0
- package/vendor/procedural-animals/src/species/shark/sculpt.js +343 -0
- package/vendor/procedural-animals/src/species/shark/variants.js +105 -0
- package/vendor/procedural-animals/src/species/sheep/behaviour.js +522 -0
- package/vendor/procedural-animals/src/species/sheep/coat.js +529 -0
- package/vendor/procedural-animals/src/species/sheep/index.js +152 -0
- package/vendor/procedural-animals/src/species/sheep/motion.js +86 -0
- package/vendor/procedural-animals/src/species/sheep/neck.js +41 -0
- package/vendor/procedural-animals/src/species/sheep/regions.js +62 -0
- package/vendor/procedural-animals/src/species/sheep/rig.js +163 -0
- package/vendor/procedural-animals/src/species/sheep/sculpt.js +542 -0
- package/vendor/procedural-animals/src/species/snake/coat.js +315 -0
- package/vendor/procedural-animals/src/species/snake/index.js +73 -0
- package/vendor/procedural-animals/src/species/snake/motion.js +51 -0
- package/vendor/procedural-animals/src/species/snake/regions.js +51 -0
- package/vendor/procedural-animals/src/species/snake/rig.js +108 -0
- package/vendor/procedural-animals/src/species/snake/sculpt.js +122 -0
- package/vendor/procedural-animals/src/species/spider/coat.js +279 -0
- package/vendor/procedural-animals/src/species/spider/index.js +78 -0
- package/vendor/procedural-animals/src/species/spider/motion.js +72 -0
- package/vendor/procedural-animals/src/species/spider/regions.js +87 -0
- package/vendor/procedural-animals/src/species/spider/rig.js +118 -0
- package/vendor/procedural-animals/src/species/spider/sculpt.js +182 -0
- package/vendor/procedural-animals/src/species/wolf/behaviour.js +160 -0
- package/vendor/procedural-animals/src/species/wolf/coat.js +749 -0
- package/vendor/procedural-animals/src/species/wolf/index.js +107 -0
- package/vendor/procedural-animals/src/species/wolf/motion.js +80 -0
- package/vendor/procedural-animals/src/species/wolf/regions.js +57 -0
- package/vendor/procedural-animals/src/species/wolf/rig.js +87 -0
- package/vendor/procedural-animals/src/species/wolf/sculpt.js +339 -0
- package/vendor/procedural-animals/src/worker.js +29 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Host-neutral controller behind the OpenCode TUI widget. One pet core is shared by
|
|
2
|
+
// every mounted slot view; the most recently attached view receives frames. Import
|
|
3
|
+
// and creation start no timer and load no art or settings: the first attach() starts
|
|
4
|
+
// a <=1 Hz settings poll, and only an enabled, visible view loads the pet. It never
|
|
5
|
+
// touches stdin/stdout, the alternate screen, processes, windows or the network.
|
|
6
|
+
|
|
7
|
+
export const AGENT='opencode';
|
|
8
|
+
export const DEFAULT_FPS=30;
|
|
9
|
+
export const POLL_MS=1000;
|
|
10
|
+
export const HEARTBEAT_MS=5000;
|
|
11
|
+
// Bounded so the host prompt stays usable: at most 64x12 cells, hidden when the
|
|
12
|
+
// terminal is too small for a legible pet.
|
|
13
|
+
export const LIMITS=Object.freeze({maxColumns:64,maxRows:12,minColumns:20,minRows:5,sideMargin:4,reservedRows:14});
|
|
14
|
+
export const COMMANDS=Object.freeze(['enable','disable','ball','treat','status']);
|
|
15
|
+
|
|
16
|
+
const fn=(value,name)=>{if(typeof value!=='function')throw TypeError(`${name} must be a function`);};
|
|
17
|
+
const defaultScheduler=()=>({now:()=>performance.now(),schedule:(callback,delayMs)=>setTimeout(callback,delayMs),cancel:handle=>clearTimeout(handle)});
|
|
18
|
+
const defaultLoadPlugin=async()=>(await import('../../retrieviewer-plugin.mjs')).createPetPlugin;
|
|
19
|
+
const defaultLoadSettings=()=>import('../../retrieviewer-config.mjs');
|
|
20
|
+
|
|
21
|
+
// Terminal viewport (cells) -> pet pane, or null when hidden/too small.
|
|
22
|
+
export function petViewport(viewport){
|
|
23
|
+
if(!viewport||typeof viewport!=='object')return null;
|
|
24
|
+
const {width,height}=viewport;
|
|
25
|
+
if(!Number.isFinite(width)||!Number.isFinite(height))return null;
|
|
26
|
+
const columns=Math.min(LIMITS.maxColumns,Math.floor(width)-LIMITS.sideMargin);
|
|
27
|
+
const rows=Math.min(LIMITS.maxRows,Math.floor((Math.floor(height)-LIMITS.reservedRows)/2));
|
|
28
|
+
return columns<LIMITS.minColumns||rows<LIMITS.minRows?null:{columns,rows};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createOpenCodePetController({hostVersion='unknown',scheduler,fps=DEFAULT_FPS,petOptions,loadPlugin=defaultLoadPlugin,loadSettings=defaultLoadSettings,onError}={}){
|
|
32
|
+
if(scheduler!==undefined){fn(scheduler?.now,'scheduler.now');fn(scheduler.schedule,'scheduler.schedule');fn(scheduler.cancel,'scheduler.cancel');}
|
|
33
|
+
fn(loadPlugin,'loadPlugin');fn(loadSettings,'loadSettings');
|
|
34
|
+
if(onError!==undefined)fn(onError,'onError');
|
|
35
|
+
const clock=scheduler??defaultScheduler();
|
|
36
|
+
const views=new Set();
|
|
37
|
+
let active=null,shown=null,enabled=false,disposed=false;
|
|
38
|
+
let plugin=null,loading=false,gen=0;
|
|
39
|
+
let settings=null,pollHandle=null,polling=false,epoch=0,hiddenCheck=null,pollError=null;
|
|
40
|
+
let receipt=null,writes=Promise.resolve();
|
|
41
|
+
const report=error=>{try{onError?.(error);}catch{}};
|
|
42
|
+
const settingsModule=()=>settings??=Promise.resolve().then(loadSettings);
|
|
43
|
+
|
|
44
|
+
// Receipts carry our own state only, serialized so phases never reorder.
|
|
45
|
+
function writeReceipt(phase){
|
|
46
|
+
if(!receipt)return;
|
|
47
|
+
const body={hostVersion:String(hostVersion),phase,firstFrameAt:receipt.firstFrameAt,columns:receipt.columns,rows:receipt.rows,frames:receipt.frames};
|
|
48
|
+
writes=writes.then(async()=>{const m=await settingsModule();await m.writeRuntimeReceipt(AGENT,body);}).catch(report);
|
|
49
|
+
}
|
|
50
|
+
function show(view,text){if(!view)return;view.render(text);shown=text?view:(shown===view?null:shown);}
|
|
51
|
+
function clearShown(){if(shown){const v=shown;shown=null;v.render('');}}
|
|
52
|
+
|
|
53
|
+
const host={
|
|
54
|
+
getSize(){
|
|
55
|
+
const box=active?petViewport(active.getViewport()):null;
|
|
56
|
+
if(!box&&!hiddenCheck)hiddenCheck=clock.schedule(()=>{hiddenCheck=null;reconcile();},0);
|
|
57
|
+
return box;
|
|
58
|
+
},
|
|
59
|
+
render(frame){
|
|
60
|
+
if(!active)return;
|
|
61
|
+
if(shown&&shown!==active)clearShown();
|
|
62
|
+
show(active,frame.text);
|
|
63
|
+
const now=clock.now();
|
|
64
|
+
if(!receipt){receipt={firstFrameAt:new Date().toISOString(),frames:1,beat:now,columns:frame.columns,rows:frame.rows};writeReceipt('rendering');return;}
|
|
65
|
+
receipt.frames++;receipt.columns=frame.columns;receipt.rows=frame.rows;
|
|
66
|
+
if(now-receipt.beat>=HEARTBEAT_MS){receipt.beat=now;writeReceipt('rendering');}
|
|
67
|
+
},
|
|
68
|
+
clear:clearShown,
|
|
69
|
+
onError(error){stopPet();report(error);},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function stopPet(){
|
|
73
|
+
gen++;loading=false;
|
|
74
|
+
if(hiddenCheck){clock.cancel(hiddenCheck);hiddenCheck=null;}
|
|
75
|
+
const p=plugin;plugin=null;
|
|
76
|
+
try{p?.dispose();}catch(error){report(error);}
|
|
77
|
+
clearShown();
|
|
78
|
+
if(receipt){writeReceipt('stopped');receipt=null;}
|
|
79
|
+
}
|
|
80
|
+
const wanted=()=>!disposed&&enabled&&views.size>0;
|
|
81
|
+
|
|
82
|
+
async function load(){
|
|
83
|
+
const mine=++gen;loading=true;
|
|
84
|
+
try{
|
|
85
|
+
const create=await loadPlugin();
|
|
86
|
+
if(mine!==gen||!wanted())return;
|
|
87
|
+
const p=await create({host,fps,petOptions,scheduler:clock});
|
|
88
|
+
// Disabled, unmounted or reloaded while the art loaded: release it unseen.
|
|
89
|
+
if(mine!==gen||!wanted()){p.dispose();return;}
|
|
90
|
+
plugin=p;
|
|
91
|
+
}catch(error){if(mine===gen)report(error);return;}
|
|
92
|
+
finally{if(mine===gen)loading=false;}
|
|
93
|
+
reconcile();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function reconcile(redraw=false){
|
|
97
|
+
if(!wanted()){if(plugin||loading||receipt)stopPet();return;}
|
|
98
|
+
if(!plugin){if(!loading)load();return;}
|
|
99
|
+
const box=active?petViewport(active.getViewport()):null;
|
|
100
|
+
// Hidden: no timer and no simulated time until a refresh reveals the pane.
|
|
101
|
+
if(!box){plugin.pause();clearShown();return;}
|
|
102
|
+
try{if(!plugin.running)plugin.start();else if(redraw)plugin.render();}
|
|
103
|
+
catch(error){stopPet();report(error);}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function schedulePoll(){
|
|
107
|
+
if(disposed||views.size===0||pollHandle!==null)return;
|
|
108
|
+
pollHandle=clock.schedule(()=>{pollHandle=null;poll();},POLL_MS);
|
|
109
|
+
}
|
|
110
|
+
async function poll(){
|
|
111
|
+
if(disposed||views.size===0||polling)return;
|
|
112
|
+
polling=true;const seen=epoch;
|
|
113
|
+
try{
|
|
114
|
+
const m=await settingsModule();
|
|
115
|
+
const s=await m.readAgentSettings(AGENT);
|
|
116
|
+
pollError=null;
|
|
117
|
+
if(seen===epoch&&!disposed){enabled=s?.enabled===true;reconcile();}
|
|
118
|
+
}catch(error){
|
|
119
|
+
// A persistent failure (e.g. missing settings module) is reported once, not per poll.
|
|
120
|
+
const key=String(error?.message??error);
|
|
121
|
+
if(key!==pollError){pollError=key;report(error);}
|
|
122
|
+
}
|
|
123
|
+
finally{polling=false;schedulePoll();}
|
|
124
|
+
}
|
|
125
|
+
function stopPolling(){if(pollHandle!==null){clock.cancel(pollHandle);pollHandle=null;}}
|
|
126
|
+
|
|
127
|
+
async function setEnabled(value){
|
|
128
|
+
if(typeof value!=='boolean')throw TypeError('enabled must be boolean');
|
|
129
|
+
const m=await settingsModule();
|
|
130
|
+
await m.setAgentEnabled(AGENT,value);
|
|
131
|
+
epoch++;
|
|
132
|
+
if(disposed)return;
|
|
133
|
+
enabled=value;reconcile();
|
|
134
|
+
}
|
|
135
|
+
function interact(kind){
|
|
136
|
+
if(kind!=='ball'&&kind!=='treat')throw TypeError("kind must be 'ball' or 'treat'");
|
|
137
|
+
return plugin?plugin.interact(kind):false;
|
|
138
|
+
}
|
|
139
|
+
function status(){
|
|
140
|
+
return {enabled,mounted:views.size,loading,running:!!plugin?.running,frames:receipt?.frames??0,columns:receipt?.columns??null,rows:receipt?.rows??null,firstFrameAt:receipt?.firstFrameAt??null};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
// view: {getViewport(): {width,height}|null, render(text: string): void}. Text is
|
|
145
|
+
// plain Braille (rows lines); '' means show nothing.
|
|
146
|
+
attach(view){
|
|
147
|
+
if(disposed)throw Error('OpenCode pet controller is disposed');
|
|
148
|
+
if(!view||typeof view!=='object')throw TypeError('view must be an object');
|
|
149
|
+
fn(view.getViewport,'view.getViewport');fn(view.render,'view.render');
|
|
150
|
+
const record={getViewport:view.getViewport,render:view.render};
|
|
151
|
+
views.add(record);active=record;
|
|
152
|
+
if(views.size===1)poll();
|
|
153
|
+
reconcile(true);
|
|
154
|
+
let attached=true;
|
|
155
|
+
return {
|
|
156
|
+
// Call on resize or visibility change; re-derives the pane from the viewport.
|
|
157
|
+
refresh(){if(attached&&!disposed)reconcile(true);},
|
|
158
|
+
detach(){
|
|
159
|
+
if(!attached)return;attached=false;
|
|
160
|
+
views.delete(record);
|
|
161
|
+
if(shown===record)clearShown();
|
|
162
|
+
if(active===record)active=[...views].at(-1)??null;
|
|
163
|
+
if(views.size===0)stopPolling();
|
|
164
|
+
if(!disposed)reconcile(true);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
setEnabled,
|
|
169
|
+
interact,
|
|
170
|
+
status,
|
|
171
|
+
// Slash handler: '/retrieviewer <enable|disable|ball|treat|status>'.
|
|
172
|
+
async command(argument){
|
|
173
|
+
const word=String(argument??'').trim().toLowerCase()||'status';
|
|
174
|
+
if(word==='enable'||word==='disable'){await setEnabled(word==='enable');return word==='enable'?'Retrieviewer enabled':'Retrieviewer disabled';}
|
|
175
|
+
if(word==='ball'||word==='treat'){
|
|
176
|
+
if(!plugin)return enabled?'Retrieviewer is still loading':'Retrieviewer is disabled; run /retrieviewer enable';
|
|
177
|
+
return interact(word)?`Retrieviewer: ${word} accepted`:`Retrieviewer is busy; ${word} not accepted`;
|
|
178
|
+
}
|
|
179
|
+
if(word==='status'){
|
|
180
|
+
const s=status();
|
|
181
|
+
return `Retrieviewer ${s.enabled?'enabled':'disabled'}, ${s.running?`rendering ${s.columns}x${s.rows}, ${s.frames} frames`:s.loading?'loading':'idle'}, ${s.mounted} view(s)`;
|
|
182
|
+
}
|
|
183
|
+
return `Usage: /retrieviewer ${COMMANDS.join('|')}`;
|
|
184
|
+
},
|
|
185
|
+
// Resolves once queued receipt writes have finished (for clean shutdown/tests).
|
|
186
|
+
settled(){return writes;},
|
|
187
|
+
get disposed(){return disposed;},
|
|
188
|
+
dispose(){
|
|
189
|
+
if(disposed)return writes;
|
|
190
|
+
disposed=true;stopPolling();stopPet();
|
|
191
|
+
views.clear();active=null;
|
|
192
|
+
return writes;
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
// OpenCode v2 TUI plugin: the Retrieviewer pet drawn inside OpenCode's own UI (home
|
|
3
|
+
// footer and session composer slots). The host resolves @opencode/plugin/tui,
|
|
4
|
+
// @opentui/solid and solid-js and compiles this file; no production dependency here.
|
|
5
|
+
// All lifecycle lives in ./runtime.mjs, which loads nothing until a view mounts.
|
|
6
|
+
import {Plugin} from '@opencode/plugin/tui';
|
|
7
|
+
import {useTerminalDimensions} from '@opentui/solid';
|
|
8
|
+
import {createEffect, createSignal, onCleanup, Show} from 'solid-js';
|
|
9
|
+
import {createOpenCodePetController} from './runtime.mjs';
|
|
10
|
+
|
|
11
|
+
export default Plugin.define({
|
|
12
|
+
id:'retrieviewer.pet',
|
|
13
|
+
setup(context){
|
|
14
|
+
const toast=(message,variant='info')=>context.ui.toast.show({title:'Retrieviewer',message,variant});
|
|
15
|
+
const pet=createOpenCodePetController({
|
|
16
|
+
hostVersion:context.app?.version??'unknown',
|
|
17
|
+
onError:error=>toast(String(error?.message??error),'error'),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// One view per mounted slot; all share the single pet core in the controller.
|
|
21
|
+
function PetView(){
|
|
22
|
+
const dimensions=useTerminalDimensions();
|
|
23
|
+
const [text,setText]=createSignal('');
|
|
24
|
+
const view=pet.attach({
|
|
25
|
+
getViewport:()=>{const {width,height}=dimensions();return {width,height};},
|
|
26
|
+
render:value=>setText(value),
|
|
27
|
+
});
|
|
28
|
+
createEffect(()=>{dimensions();view.refresh();});
|
|
29
|
+
onCleanup(()=>view.detach());
|
|
30
|
+
return (
|
|
31
|
+
<Show when={text()}>
|
|
32
|
+
<box flexShrink={0} paddingLeft={2}>
|
|
33
|
+
<text wrapMode="none" fg={context.theme.text.base}>{text()}</text>
|
|
34
|
+
</box>
|
|
35
|
+
</Show>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
context.ui.slot({append:'home.footer',render:()=><PetView/>});
|
|
40
|
+
context.ui.slot({append:'session.composer.top',render:()=><PetView/>});
|
|
41
|
+
context.keymap.layer(()=>({
|
|
42
|
+
mode:'global',
|
|
43
|
+
commands:[{
|
|
44
|
+
id:'retrieviewer.pet',
|
|
45
|
+
title:'Retrieviewer pet',
|
|
46
|
+
description:'enable | disable | ball | treat | status',
|
|
47
|
+
group:'Retrieviewer',
|
|
48
|
+
palette:true,
|
|
49
|
+
slash:{name:'retrieviewer',arguments:true},
|
|
50
|
+
async run(argument){
|
|
51
|
+
try{toast(await pet.command(argument));}
|
|
52
|
+
catch(error){toast(String(error?.message??error),'error');}
|
|
53
|
+
},
|
|
54
|
+
}],
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
return ()=>pet.dispose();
|
|
58
|
+
},
|
|
59
|
+
});
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Native Windows painter for the existing Braille engine. No injection, keyboard
|
|
2
|
+
// hooks, terminal output, screen capture, network, registry or agent config access.
|
|
3
|
+
// Built locally with the Windows .NET Framework compiler; no downloaded binaries.
|
|
4
|
+
using System;
|
|
5
|
+
using System.Collections.Generic;
|
|
6
|
+
using System.Diagnostics;
|
|
7
|
+
using System.Drawing;
|
|
8
|
+
using System.Drawing.Imaging;
|
|
9
|
+
using System.IO;
|
|
10
|
+
using System.Runtime.InteropServices;
|
|
11
|
+
using System.Text;
|
|
12
|
+
using System.Threading;
|
|
13
|
+
using System.Web.Script.Serialization;
|
|
14
|
+
using System.Windows.Forms;
|
|
15
|
+
|
|
16
|
+
static class Native {
|
|
17
|
+
[StructLayout(LayoutKind.Sequential)] internal struct POINT { internal int X,Y; internal POINT(int x,int y){X=x;Y=y;} }
|
|
18
|
+
[StructLayout(LayoutKind.Sequential)] internal struct SIZE { internal int Width,Height; internal SIZE(int w,int h){Width=w;Height=h;} }
|
|
19
|
+
[StructLayout(LayoutKind.Sequential)] internal struct RECT { internal int Left,Top,Right,Bottom; }
|
|
20
|
+
[StructLayout(LayoutKind.Sequential,Pack=1)] internal struct BLEND { internal byte Op,Flags,Alpha,Format; }
|
|
21
|
+
internal delegate bool EnumProc(IntPtr hwnd,IntPtr data);
|
|
22
|
+
[DllImport("user32.dll")] internal static extern bool EnumWindows(EnumProc proc,IntPtr data);
|
|
23
|
+
[DllImport("user32.dll")] internal static extern bool IsWindowVisible(IntPtr hwnd);
|
|
24
|
+
[DllImport("user32.dll")] internal static extern bool IsWindow(IntPtr hwnd);
|
|
25
|
+
[DllImport("user32.dll")] internal static extern bool IsIconic(IntPtr hwnd);
|
|
26
|
+
[DllImport("user32.dll")] internal static extern IntPtr GetForegroundWindow();
|
|
27
|
+
[DllImport("user32.dll",CharSet=CharSet.Unicode)] internal static extern int GetWindowText(IntPtr hwnd,StringBuilder text,int length);
|
|
28
|
+
[DllImport("user32.dll")] internal static extern uint GetWindowThreadProcessId(IntPtr hwnd,out uint pid);
|
|
29
|
+
[DllImport("user32.dll")] internal static extern bool GetClientRect(IntPtr hwnd,out RECT rect);
|
|
30
|
+
[DllImport("user32.dll")] internal static extern bool GetWindowRect(IntPtr hwnd,out RECT rect);
|
|
31
|
+
[DllImport("user32.dll")] internal static extern bool ClientToScreen(IntPtr hwnd,ref POINT point);
|
|
32
|
+
[DllImport("user32.dll")] internal static extern bool SetWindowPos(IntPtr hwnd,IntPtr after,int x,int y,int w,int h,uint flags);
|
|
33
|
+
[DllImport("user32.dll")] internal static extern int GetWindowLong(IntPtr hwnd,int index);
|
|
34
|
+
[DllImport("user32.dll")] internal static extern bool ShowWindow(IntPtr hwnd,int command);
|
|
35
|
+
[DllImport("user32.dll")] internal static extern bool SetProcessDPIAware();
|
|
36
|
+
[DllImport("user32.dll")] internal static extern bool SetProcessDpiAwarenessContext(IntPtr context);
|
|
37
|
+
[DllImport("user32.dll")] internal static extern IntPtr GetDC(IntPtr hwnd);
|
|
38
|
+
[DllImport("user32.dll")] internal static extern int ReleaseDC(IntPtr hwnd,IntPtr dc);
|
|
39
|
+
[DllImport("gdi32.dll")] internal static extern IntPtr CreateCompatibleDC(IntPtr dc);
|
|
40
|
+
[DllImport("gdi32.dll")] internal static extern IntPtr SelectObject(IntPtr dc,IntPtr bitmap);
|
|
41
|
+
[DllImport("gdi32.dll")] internal static extern bool DeleteDC(IntPtr dc);
|
|
42
|
+
[DllImport("gdi32.dll")] internal static extern bool DeleteObject(IntPtr obj);
|
|
43
|
+
[DllImport("user32.dll",SetLastError=true)] internal static extern bool UpdateLayeredWindow(IntPtr hwnd,IntPtr dc,ref POINT dst,ref SIZE size,IntPtr src,ref POINT point,int key,ref BLEND blend,int flags);
|
|
44
|
+
internal static Rectangle ClientBounds(IntPtr hwnd){
|
|
45
|
+
RECT r;POINT p=new POINT(0,0);
|
|
46
|
+
if(!GetClientRect(hwnd,out r)||!ClientToScreen(hwnd,ref p))return Rectangle.Empty;
|
|
47
|
+
return new Rectangle(p.X,p.Y,r.Right-r.Left,r.Bottom-r.Top);
|
|
48
|
+
}
|
|
49
|
+
internal static void Place(IntPtr hwnd,Rectangle rect){
|
|
50
|
+
SetWindowPos(hwnd,new IntPtr(-1),rect.X,rect.Y,rect.Width,rect.Height,0x10|0x40); // TOPMOST, NOACTIVATE, SHOWWINDOW
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class QuietWindow : Form {
|
|
55
|
+
internal QuietWindow(){FormBorderStyle=FormBorderStyle.None;ShowInTaskbar=false;AutoScaleMode=AutoScaleMode.None;StartPosition=FormStartPosition.Manual;}
|
|
56
|
+
protected override bool ShowWithoutActivation {get{return true;}}
|
|
57
|
+
protected override CreateParams CreateParams {get{CreateParams p=base.CreateParams;p.ExStyle|=0x08000000|0x80;return p;}} // NOACTIVATE, TOOLWINDOW
|
|
58
|
+
protected override void WndProc(ref Message m){if(m.Msg==0x21){m.Result=new IntPtr(3);return;}base.WndProc(ref m);} // MA_NOACTIVATE
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
sealed class PetWindow : QuietWindow {
|
|
62
|
+
protected override CreateParams CreateParams {get{CreateParams p=base.CreateParams;p.ExStyle|=0x80000|0x20;return p;}} // LAYERED + TRANSPARENT
|
|
63
|
+
internal int PaintedDots;
|
|
64
|
+
internal Bitmap PaintFrame(Dictionary<string,object> m,int cell,Rectangle canvas){
|
|
65
|
+
int cols=Program.Int(m,"columns"),rows=Program.Int(m,"rows"),cx=Program.Int(m,"x"),cy=Program.Int(m,"y");
|
|
66
|
+
string text=Program.Str(m,"text");
|
|
67
|
+
if(cols==0&&rows==0&&text.Length==0){Hide();return null;}
|
|
68
|
+
if(cols<1||rows<1||cols*rows>125000||cx<0||cy<0||(cx+cols)*cell>canvas.Width||(cy+rows)*cell*2>canvas.Height)throw new Exception("Out-of-bounds overlay frame");
|
|
69
|
+
string[] lines=text.Split('\n');if(lines.Length!=rows)throw new Exception("Invalid overlay row count");
|
|
70
|
+
Bitmap image=new Bitmap(cols*cell,rows*cell*2,PixelFormat.Format32bppPArgb);
|
|
71
|
+
try{
|
|
72
|
+
using(Graphics g=Graphics.FromImage(image))using(Brush ink=new SolidBrush(Color.FromArgb(245,248,242,232))){
|
|
73
|
+
g.Clear(Color.Transparent);g.SmoothingMode=System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
74
|
+
int[] dx={0,0,0,1,1,1,0,1},dy={0,1,2,0,1,2,3,3};float step=cell/2f,diameter=Math.Max(1.1f,step*.48f);PaintedDots=0;
|
|
75
|
+
for(int y=0;y<rows;y++){
|
|
76
|
+
if(lines[y].Length!=cols)throw new Exception("Invalid overlay column count");
|
|
77
|
+
for(int x=0;x<cols;x++){
|
|
78
|
+
int mask=lines[y][x]-0x2800;if(mask<0||mask>255)throw new Exception("Not a Braille frame");
|
|
79
|
+
for(int bit=0;bit<8;bit++)if((mask&(1<<bit))!=0){
|
|
80
|
+
float px=x*cell+(dx[bit]+.5f)*step,py=y*cell*2+(dy[bit]+.5f)*step;
|
|
81
|
+
g.FillEllipse(ink,px-diameter/2,py-diameter/2,diameter,diameter);PaintedDots++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
Rectangle destination=new Rectangle(canvas.X+cx*cell,canvas.Y+cy*cell*2,image.Width,image.Height);
|
|
87
|
+
if(!Visible){Bounds=destination;Show();}
|
|
88
|
+
IntPtr screen=Native.GetDC(IntPtr.Zero),memory=Native.CreateCompatibleDC(screen),bitmap=IntPtr.Zero,old=IntPtr.Zero;
|
|
89
|
+
try{
|
|
90
|
+
bitmap=image.GetHbitmap(Color.FromArgb(0));old=Native.SelectObject(memory,bitmap);
|
|
91
|
+
Native.POINT target=new Native.POINT(canvas.X+cx*cell,canvas.Y+cy*cell*2),source=new Native.POINT(0,0);
|
|
92
|
+
Native.SIZE size=new Native.SIZE(image.Width,image.Height);Native.BLEND blend=new Native.BLEND();blend.Alpha=255;blend.Format=1;
|
|
93
|
+
if(!Native.UpdateLayeredWindow(Handle,screen,ref target,ref size,memory,ref source,0,ref blend,2))throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
|
|
94
|
+
Native.Place(Handle,new Rectangle(target.X,target.Y,size.Width,size.Height));
|
|
95
|
+
}finally{if(old!=IntPtr.Zero)Native.SelectObject(memory,old);if(bitmap!=IntPtr.Zero)Native.DeleteObject(bitmap);Native.DeleteDC(memory);Native.ReleaseDC(IntPtr.Zero,screen);}
|
|
96
|
+
return image;
|
|
97
|
+
}catch{image.Dispose();throw;}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
sealed class TargetItem {
|
|
102
|
+
internal IntPtr Window;internal uint Process;internal string Title;internal bool Terminal;
|
|
103
|
+
public override string ToString(){return Title;}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
sealed class Companion : ApplicationContext {
|
|
107
|
+
readonly PetWindow pet=new PetWindow();readonly QuietWindow bar=new QuietWindow();
|
|
108
|
+
readonly NotifyIcon tray=new NotifyIcon();readonly System.Windows.Forms.Timer clock=new System.Windows.Forms.Timer();
|
|
109
|
+
readonly JavaScriptSerializer json=new JavaScriptSerializer();readonly object inputLock=new object();
|
|
110
|
+
readonly Queue<Dictionary<string,object>> messages=new Queue<Dictionary<string,object>>();
|
|
111
|
+
TargetItem target;Form pickerWindow;QuietWindow testTarget;readonly bool testMode;Rectangle canvas;int revision=0,cell=8;bool visible=false,paused=false,loaded=false,quitting=false,picking=false,announced=false;
|
|
112
|
+
Label status=new Label();Mutex mutex;
|
|
113
|
+
internal Companion(Mutex single,bool diagnostic){
|
|
114
|
+
mutex=single;testMode=diagnostic;bar.Text="Retrieviewer controls";bar.Size=new Size(354,40);bar.BackColor=Color.FromArgb(28,30,34);bar.ForeColor=Color.White;
|
|
115
|
+
bar.Font=new Font("Segoe UI",9);bar.Controls.Add(MakeButton("공",8,()=>Interact("ball")));bar.Controls.Add(MakeButton("간식",52,()=>Interact("treat")));
|
|
116
|
+
bar.Controls.Add(MakeButton("창",102,ChooseTarget));bar.Controls.Add(MakeButton("쉼",146,TogglePause));bar.Controls.Add(MakeButton("종료",190,Quit));
|
|
117
|
+
status.AutoSize=false;status.Location=new Point(240,12);status.Size=new Size(110,22);status.Text="준비 중";bar.Controls.Add(status);
|
|
118
|
+
ContextMenuStrip menu=new ContextMenuStrip();menu.Items.Add("다른 창 선택",null,(s,e)=>ChooseTarget());menu.Items.Add("숨기기 / 다시 표시",null,(s,e)=>TogglePause());menu.Items.Add("종료",null,(s,e)=>Quit());
|
|
119
|
+
tray.Icon=SystemIcons.Application;tray.Text="Retrieviewer · 창 선택";tray.ContextMenuStrip=menu;tray.Visible=true;tray.DoubleClick+=(s,e)=>ChooseTarget();
|
|
120
|
+
// Handles are created on the UI thread before the reader can signal EOF.
|
|
121
|
+
IntPtr unused=bar.Handle;unused=pet.Handle;
|
|
122
|
+
Thread reader=new Thread(ReadInput);reader.IsBackground=true;reader.Start();
|
|
123
|
+
clock.Interval=16;clock.Tick+=(s,e)=>Tick();clock.Start();
|
|
124
|
+
if(testMode){
|
|
125
|
+
testTarget=new QuietWindow();testTarget.Bounds=new Rectangle(-12000,-12000,1280,800);testTarget.Show();
|
|
126
|
+
target=new TargetItem{Window=testTarget.Handle,Process=(uint)Process.GetCurrentProcess().Id,Title="owned diagnostic window"};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
Button MakeButton(string title,int x,Action action){
|
|
130
|
+
Button b=new Button();b.Text=title;b.TabStop=false;b.Size=new Size(title=="간식"?46:40,28);b.Location=new Point(x,6);b.FlatStyle=FlatStyle.Flat;b.FlatAppearance.BorderSize=0;b.BackColor=Color.FromArgb(48,51,58);b.ForeColor=Color.White;b.Click+=(s,e)=>action();return b;
|
|
131
|
+
}
|
|
132
|
+
void ReadInput(){
|
|
133
|
+
try{
|
|
134
|
+
string line;while((line=Program.Input.ReadLine())!=null){
|
|
135
|
+
if(line.Length>800000)break;
|
|
136
|
+
Dictionary<string,object> value=json.Deserialize<Dictionary<string,object>>(line);
|
|
137
|
+
lock(inputLock){if(messages.Count>4)break;messages.Enqueue(value);}
|
|
138
|
+
}
|
|
139
|
+
}catch{}
|
|
140
|
+
try{bar.BeginInvoke(new Action(Quit));}catch{}
|
|
141
|
+
}
|
|
142
|
+
void Interact(string kind){if(!loaded||paused)return;Program.Emit(new {type="interact",kind=kind});}
|
|
143
|
+
void TogglePause(){paused=!paused;Program.Emit(new {type="pause",paused=paused});Track(true);}
|
|
144
|
+
void Tick(){
|
|
145
|
+
if(quitting)return;
|
|
146
|
+
try{
|
|
147
|
+
if(!announced){announced=true;if(!testMode)ChooseTarget();Program.Emit(new {type="ready",pickerVisible=pickerWindow!=null&&pickerWindow.Visible});}
|
|
148
|
+
Track(false);
|
|
149
|
+
for(;;){Dictionary<string,object> m;lock(inputLock){if(messages.Count==0)break;m=messages.Dequeue();}
|
|
150
|
+
string type=Program.Str(m,"type");
|
|
151
|
+
if(type=="quit"){Quit();return;}
|
|
152
|
+
if(type=="loaded"){loaded=true;status.Text="함께 노는 중";if(testMode)((Button)bar.Controls[0]).PerformClick();continue;}
|
|
153
|
+
if(type=="interaction"){status.Text=Program.Bool(m,"accepted")?"좋아!":"잠깐 기다려";continue;}
|
|
154
|
+
if(type=="error"){status.Text="엔진 오류";Quit();return;}
|
|
155
|
+
if(type=="frame"){
|
|
156
|
+
// Always acknowledge discarded frames too, otherwise resizing can deadlock the sender.
|
|
157
|
+
bool painted=visible&&!paused&&Program.Int(m,"revision")==revision;
|
|
158
|
+
if(painted){using(Bitmap image=pet.PaintFrame(m,cell,canvas)){} }
|
|
159
|
+
Program.Emit(new {type="ack",painted=painted});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}catch(Exception error){Program.Emit(new {type="error",message=error.Message});Quit();}
|
|
163
|
+
}
|
|
164
|
+
void Track(bool force){
|
|
165
|
+
Rectangle next=Rectangle.Empty;bool show=false;uint pid;
|
|
166
|
+
if(target!=null&&(!Native.IsWindow(target.Window)||Native.GetWindowThreadProcessId(target.Window,out pid)==0||pid!=target.Process)){target=null;}
|
|
167
|
+
if(target!=null&&!Native.IsIconic(target.Window)&&Native.IsWindowVisible(target.Window)){
|
|
168
|
+
next=Native.ClientBounds(target.Window);IntPtr foreground=Native.GetForegroundWindow();show=!picking&&(testMode||foreground==target.Window||foreground==bar.Handle)&&next.Width>=160&&next.Height>=120;
|
|
169
|
+
}
|
|
170
|
+
int nextCell=8;while(next.Width/nextCell*(next.Height/(nextCell*2))>125000&&nextCell<16)nextCell+=2;
|
|
171
|
+
if(nextCell==14)nextCell=16;
|
|
172
|
+
int columns=next.Width/nextCell,rows=next.Height/(nextCell*2);
|
|
173
|
+
if(force||show!=visible||next!=canvas||nextCell!=cell){
|
|
174
|
+
visible=show;canvas=next;cell=nextCell;revision++;pet.Hide();
|
|
175
|
+
Program.Emit(new {type="viewport",revision=revision,visible=show&&!paused,selected=target!=null,columns=columns,rows=rows,cellWidth=cell,cellHeight=cell*2});
|
|
176
|
+
}
|
|
177
|
+
if(visible){
|
|
178
|
+
if(!bar.Visible)bar.Show();Native.Place(bar.Handle,new Rectangle(canvas.Right-bar.Width-10,canvas.Top+10,bar.Width,bar.Height));
|
|
179
|
+
if(paused)pet.Hide();
|
|
180
|
+
}else{pet.Hide();bar.Hide();}
|
|
181
|
+
}
|
|
182
|
+
List<TargetItem> Targets(){
|
|
183
|
+
List<TargetItem> result=new List<TargetItem>();uint mine=(uint)Process.GetCurrentProcess().Id;
|
|
184
|
+
Native.EnumWindows((hwnd,data)=>{
|
|
185
|
+
uint pid;Native.GetWindowThreadProcessId(hwnd,out pid);if(pid==mine||!Native.IsWindowVisible(hwnd))return true;
|
|
186
|
+
StringBuilder name=new StringBuilder(512);Native.GetWindowText(hwnd,name,name.Capacity);if(name.Length==0)return true;
|
|
187
|
+
string process="";try{using(Process p=Process.GetProcessById((int)pid)){process=p.ProcessName;}}catch{}
|
|
188
|
+
bool terminal=process.Equals("WindowsTerminal",StringComparison.OrdinalIgnoreCase)||process.IndexOf("wezterm",StringComparison.OrdinalIgnoreCase)>=0||process=="OpenConsole"||process=="conhost"||process=="mintty";
|
|
189
|
+
result.Add(new TargetItem{Window=hwnd,Process=pid,Title=(terminal?"[터미널] ":"")+name.ToString()+" · "+process,Terminal=terminal});return true;
|
|
190
|
+
},IntPtr.Zero);
|
|
191
|
+
result.Sort((a,b)=>a.Terminal==b.Terminal?String.Compare(a.Title,b.Title,StringComparison.CurrentCulture):a.Terminal?-1:1);return result;
|
|
192
|
+
}
|
|
193
|
+
void ChooseTarget(){
|
|
194
|
+
if(picking||quitting)return;picking=true;Track(true);
|
|
195
|
+
Form picker=new Form();pickerWindow=picker;
|
|
196
|
+
{
|
|
197
|
+
picker.Text="Retrieviewer · 함께 있을 창";picker.Size=new Size(650,230);picker.StartPosition=FormStartPosition.CenterScreen;picker.FormBorderStyle=FormBorderStyle.FixedDialog;picker.MaximizeBox=false;picker.MinimizeBox=false;picker.Font=new Font("Segoe UI",10);
|
|
198
|
+
picker.ShowInTaskbar=true;picker.TopMost=true;
|
|
199
|
+
Label hint=new Label();hint.Text="Codex를 사용하는 터미널 창을 선택하세요.\n내용을 읽지 않고 창의 위치·크기만 따라갑니다. 탭은 구별하지 않습니다.";hint.SetBounds(18,16,610,50);
|
|
200
|
+
ComboBox list=new ComboBox();list.DropDownStyle=ComboBoxStyle.DropDownList;list.SetBounds(18,76,600,28);foreach(TargetItem item in Targets())list.Items.Add(item);if(list.Items.Count>0)list.SelectedIndex=0;
|
|
201
|
+
Button refresh=new Button();refresh.Text="새로 고침";refresh.SetBounds(18,124,100,34);refresh.Click+=(s,e)=>{list.Items.Clear();foreach(TargetItem item in Targets())list.Items.Add(item);if(list.Items.Count>0)list.SelectedIndex=0;};
|
|
202
|
+
Button attach=new Button();attach.Text="이 창에 붙이기";attach.SetBounds(378,124,140,34);attach.Click+=(s,e)=>{if(list.SelectedItem!=null){target=(TargetItem)list.SelectedItem;paused=false;Program.Emit(new {type="pause",paused=false});picker.Close();}};
|
|
203
|
+
Button cancel=new Button();cancel.Text="취소";cancel.SetBounds(528,124,90,34);cancel.Click+=(s,e)=>picker.Close();
|
|
204
|
+
picker.Controls.AddRange(new Control[]{hint,list,refresh,attach,cancel});
|
|
205
|
+
picker.FormClosed+=(s,e)=>{pickerWindow=null;picking=false;Program.Emit(new {type="picker",visible=false});if(!quitting){if(target==null)status.Text="트레이에서 창 선택";tray.Text=target==null?"Retrieviewer · 창 선택":"Retrieviewer · 창에 붙어 있음";Track(true);}picker.Dispose();};
|
|
206
|
+
picker.Show();Native.ShowWindow(picker.Handle,5);Program.Emit(new {type="picker",visible=Native.IsWindowVisible(picker.Handle)});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
void Quit(){if(quitting)return;quitting=true;clock.Stop();if(pickerWindow!=null)pickerWindow.Close();pet.Close();bar.Close();tray.Visible=false;tray.Dispose();Program.Emit(new {type="quit"});ExitThread();}
|
|
210
|
+
protected override void Dispose(bool disposing){if(disposing){clock.Dispose();pet.Dispose();bar.Dispose();tray.Dispose();if(testTarget!=null)testTarget.Dispose();if(mutex!=null){mutex.ReleaseMutex();mutex.Dispose();mutex=null;}}base.Dispose(disposing);}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static class Program {
|
|
214
|
+
static readonly object outputLock=new object();
|
|
215
|
+
internal static TextReader Input;static TextWriter Output;
|
|
216
|
+
internal static void Emit(object value){lock(outputLock){try{Output.WriteLine(new JavaScriptSerializer().Serialize(value));Output.Flush();}catch{Environment.Exit(0);}}}
|
|
217
|
+
internal static int Int(Dictionary<string,object> m,string key){return Convert.ToInt32(m[key]);}
|
|
218
|
+
internal static string Str(Dictionary<string,object> m,string key){return Convert.ToString(m[key]);}
|
|
219
|
+
internal static bool Bool(Dictionary<string,object> m,string key){return Convert.ToBoolean(m[key]);}
|
|
220
|
+
[STAThread] static int Main(string[] args){
|
|
221
|
+
try{
|
|
222
|
+
// GUI-subsystem processes have redirected handles but no console code
|
|
223
|
+
// page. Setting Console.*Encoding can throw before startup on Windows.
|
|
224
|
+
Input=new StreamReader(Console.OpenStandardInput(),new UTF8Encoding(false),false);
|
|
225
|
+
Output=new StreamWriter(Console.OpenStandardOutput(),new UTF8Encoding(false));
|
|
226
|
+
try{Native.SetProcessDpiAwarenessContext(new IntPtr(-4));}catch(EntryPointNotFoundException){Native.SetProcessDPIAware();}
|
|
227
|
+
Application.EnableVisualStyles();Application.SetCompatibleTextRenderingDefault(false);
|
|
228
|
+
if(args.Length==2&&args[0]=="--self-test")return SelfTest(args[1]);
|
|
229
|
+
bool ipcTest=args.Length==1&&args[0]=="--ipc-test";
|
|
230
|
+
if(args.Length!=0&&!ipcTest)throw new Exception("Unsupported native painter argument");
|
|
231
|
+
// Diagnostics own only off-screen windows and must not stop or contend
|
|
232
|
+
// with a user's running companion during a release verification.
|
|
233
|
+
string mutexName="Local\\BiteFoundry.Retrieviewer.Overlay."+System.Security.Principal.WindowsIdentity.GetCurrent().User.Value+(ipcTest?".Diagnostics."+Process.GetCurrentProcess().Id:"");
|
|
234
|
+
bool first;Mutex single=new Mutex(true,mutexName,out first);
|
|
235
|
+
if(!first){single.Dispose();Emit(new {type="error",message="A Retrieviewer companion is already running"});return 2;}
|
|
236
|
+
using(Companion context=new Companion(single,ipcTest))Application.Run(context);return 0;
|
|
237
|
+
}catch(Exception error){Emit(new {type="error",message=error.Message});return 1;}
|
|
238
|
+
}
|
|
239
|
+
static int SelfTest(string proof){
|
|
240
|
+
// Uses only our own off-screen windows. Does NOT click/type in or capture a user application.
|
|
241
|
+
IntPtr foreground=Native.GetForegroundWindow();
|
|
242
|
+
Dictionary<string,object> frame=new JavaScriptSerializer().Deserialize<Dictionary<string,object>>(Input.ReadLine());
|
|
243
|
+
using(QuietWindow target=new QuietWindow())using(PetWindow pet=new PetWindow()){
|
|
244
|
+
target.Bounds=new Rectangle(-12000,-12000,1600,1000);target.Show();Application.DoEvents();
|
|
245
|
+
Rectangle first=Native.ClientBounds(target.Handle);using(Bitmap bitmap=pet.PaintFrame(frame,8,first)){bitmap.Save(proof,ImageFormat.Png);}
|
|
246
|
+
int styles=Native.GetWindowLong(pet.Handle,-20);bool clickThrough=(styles&0x20)!=0,noActivate=(styles&0x08000000)!=0,layered=(styles&0x80000)!=0;
|
|
247
|
+
target.Bounds=new Rectangle(-11000,-11500,1800,1100);Application.DoEvents();Rectangle moved=Native.ClientBounds(target.Handle);
|
|
248
|
+
using(Bitmap bitmap=pet.PaintFrame(frame,8,moved)){}
|
|
249
|
+
Native.RECT actual;Native.GetWindowRect(pet.Handle,out actual);
|
|
250
|
+
int expectedX=moved.Left+Int(frame,"x")*8,expectedY=moved.Top+Int(frame,"y")*16;
|
|
251
|
+
bool follows=actual.Left==expectedX&&actual.Top==expectedY;
|
|
252
|
+
Native.ShowWindow(target.Handle,7);Application.DoEvents();bool minimized=Native.IsIconic(target.Handle);pet.Hide();bool hidden=!pet.Visible;
|
|
253
|
+
Native.ShowWindow(target.Handle,4);Application.DoEvents();bool restored=!Native.IsIconic(target.Handle);
|
|
254
|
+
bool focus=Native.GetForegroundWindow()==foreground;
|
|
255
|
+
bool passed=clickThrough&&noActivate&&layered&&follows&&minimized&&hidden&&restored&&focus&&pet.PaintedDots>0;
|
|
256
|
+
Emit(new {type="self-test",passed=passed,clickThroughStyle=clickThrough,noActivateStyle=noActivate,layered=layered,followGeometry=follows,expectedX=expectedX,expectedY=expectedY,actualX=actual.Left,actualY=actual.Top,minimize=minimized,hide=hidden,restore=restored,foregroundPreserved=focus,dots=pet.PaintedDots,proof=proof});return passed?0:1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Explicit Windows companion launcher. Never imports the pet on help/status/stop.
|
|
2
|
+
import {spawn,execFile} from 'node:child_process';
|
|
3
|
+
import {promisify} from 'node:util';
|
|
4
|
+
import {readFile,mkdir,lstat,unlink,open,rename,readdir} from 'node:fs/promises';
|
|
5
|
+
import {createHash,randomUUID} from 'node:crypto';
|
|
6
|
+
import {join,dirname} from 'node:path';
|
|
7
|
+
import {fileURLToPath} from 'node:url';
|
|
8
|
+
import {createConnection} from 'node:net';
|
|
9
|
+
import {getConfigPaths} from '../../retrieviewer-config.mjs';
|
|
10
|
+
const exec=promisify(execFile);
|
|
11
|
+
const SOURCE=new URL('./Overlay.cs',import.meta.url);
|
|
12
|
+
export function desktopPaths(options={}){const root=join(getConfigPaths(options).root,'desktop');return {root,session:join(root,'session.json')};}
|
|
13
|
+
export async function noLink(path){try{if((await lstat(path)).isSymbolicLink())throw Error('Refusing desktop symlink: '+path);}catch(e){if(e.code!=='ENOENT')throw e;}}
|
|
14
|
+
export async function prepareRoot(options={}){
|
|
15
|
+
const parent=getConfigPaths(options).root,{root}=desktopPaths(options);await noLink(dirname(parent));await noLink(parent);await noLink(root);await mkdir(root,{recursive:true,mode:0o700});return desktopPaths(options);
|
|
16
|
+
}
|
|
17
|
+
export async function readSession(options={}){
|
|
18
|
+
const {root,session}=desktopPaths(options);await noLink(dirname(root));await noLink(root);await noLink(session);
|
|
19
|
+
let data;try{data=JSON.parse(await readFile(session,'utf8'));}catch(e){if(e.code==='ENOENT')return null;throw e;}
|
|
20
|
+
if(data?.owner!=='bitefoundry-retrieviewer-overlay-v1'||!Number.isInteger(data.pid)||data.pid<=0||!/^\\\\\.\\pipe\\retrieviewer-overlay-[a-f0-9-]{36}$/.test(data.pipe)||!/^[a-f0-9]{64}$/.test(data.token))throw Error('Unrecognized desktop session; not modifying it');
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
export function requestSession(session,command){
|
|
24
|
+
return new Promise((resolve,reject)=>{
|
|
25
|
+
const socket=createConnection(session.pipe);let text='',done=false;
|
|
26
|
+
const finish=(error,value)=>{if(done)return;done=true;socket.destroy();error?reject(error):resolve(value);};
|
|
27
|
+
socket.setTimeout(1800,()=>finish(Error('Desktop control timed out')));
|
|
28
|
+
socket.on('connect',()=>socket.write(JSON.stringify({token:session.token,command})+'\n'));
|
|
29
|
+
socket.on('data',bytes=>{text+=bytes.toString('utf8');if(text.length>65536)return finish(Error('Oversized desktop response'));if(text.includes('\n'))try{finish(null,JSON.parse(text.split('\n')[0]));}catch(e){finish(e);}});
|
|
30
|
+
socket.on('error',e=>finish(e));socket.on('end',()=>{if(!done)finish(Error('Desktop control closed without a response'));});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function desktopStatus(options={}){
|
|
34
|
+
const session=await readSession(options);if(!session)return {running:false};
|
|
35
|
+
try{return await requestSession(session,'status');}catch(error){let alive=true;try{process.kill(session.pid,0);}catch(e){if(e.code==='ESRCH')alive=false;}
|
|
36
|
+
return {running:alive,reachable:false,stale:!alive,pid:session.pid,reason:error.code??error.message};}
|
|
37
|
+
}
|
|
38
|
+
export async function stopDesktop(options={}){
|
|
39
|
+
const session=await readSession(options);if(!session)return {running:false,stopped:true};
|
|
40
|
+
const result=await desktopStatus(options);
|
|
41
|
+
if(result.stale){await unlink(desktopPaths(options).session);return {running:false,stopped:true,staleSessionRemoved:true};}
|
|
42
|
+
if(result.reachable===false)throw Error('Desktop process is not responding; close it from its tray icon. No PID was killed.');
|
|
43
|
+
await requestSession(session,'stop');
|
|
44
|
+
// The stop response acknowledges the request, not native process termination.
|
|
45
|
+
// Wait before advertising success so a following cleanup cannot race an in-use exe.
|
|
46
|
+
for(let i=0;i<60;i++){
|
|
47
|
+
try{process.kill(session.pid,0);}catch(e){if(e.code==='ESRCH')return {running:false,stopped:true};throw e;}
|
|
48
|
+
await new Promise(resolve=>setTimeout(resolve,100));
|
|
49
|
+
}
|
|
50
|
+
throw Error('Desktop accepted stop but has not exited; cache preserved. Retry after it closes.');
|
|
51
|
+
}
|
|
52
|
+
export async function buildHelper(options={}){
|
|
53
|
+
if(process.platform!=='win32')throw Error('The transparent companion currently supports Windows only');
|
|
54
|
+
const {root}=await prepareRoot(options),source=await readFile(SOURCE),hash=createHash('sha256').update(source).digest('hex').slice(0,20);
|
|
55
|
+
const executable=join(root,`overlay-${hash}.exe`);await noLink(executable);
|
|
56
|
+
try{await lstat(executable);return executable;}catch(e){if(e.code!=='ENOENT')throw e;}
|
|
57
|
+
const compiler=join(process.env.SystemRoot??'C:\\Windows','Microsoft.NET','Framework64','v4.0.30319','csc.exe');
|
|
58
|
+
const temporary=join(root,`build-${randomUUID()}.exe`);
|
|
59
|
+
try{
|
|
60
|
+
await exec(compiler,['/nologo','/target:winexe','/platform:anycpu','/optimize+','/codepage:65001','/reference:System.Windows.Forms.dll','/reference:System.Drawing.dll','/reference:System.Web.Extensions.dll','/out:'+temporary,fileURLToPath(SOURCE)],{windowsHide:true,timeout:30000,maxBuffer:1024*1024});
|
|
61
|
+
await rename(temporary,executable);
|
|
62
|
+
}catch(error){await unlink(temporary).catch(()=>{});throw Error('Windows overlay build failed: '+(error.stdout??error.message));}
|
|
63
|
+
return executable;
|
|
64
|
+
}
|
|
65
|
+
export async function startDesktop(options={}){
|
|
66
|
+
const status=await desktopStatus(options);if(status.running)return {...status,alreadyRunning:true};
|
|
67
|
+
if(status.stale)await stopDesktop(options);
|
|
68
|
+
const executable=await buildHelper(options);
|
|
69
|
+
return new Promise((resolve,reject)=>{
|
|
70
|
+
const child=spawn(process.execPath,[fileURLToPath(new URL('./worker.mjs',import.meta.url)),executable],{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore','ipc'],env:{...process.env,RETRIEVIEWER_HOME:getConfigPaths(options).root}});
|
|
71
|
+
let settled=false;const finish=(error,message)=>{if(settled)return;settled=true;clearTimeout(timer);if(child.connected)child.disconnect();child.unref();error?reject(error):resolve(message);};
|
|
72
|
+
const timer=setTimeout(()=>{if(child.connected)child.send({command:'abort'});finish(Error('Desktop startup timed out; no agent settings changed'));},20000);
|
|
73
|
+
child.on('error',error=>finish(error));child.on('message',message=>message.error?finish(Error(message.error)):finish(null,message));child.on('exit',code=>finish(Error('Desktop exited during startup ('+code+')')));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export async function cleanDesktop(options={}){
|
|
77
|
+
const status=await desktopStatus(options);if(status.running)throw Error('Stop the desktop companion before removing its cache');
|
|
78
|
+
const {root,session}=desktopPaths(options);if(status.stale)await unlink(session);
|
|
79
|
+
let files;try{await noLink(root);files=await readdir(root);}catch(e){if(e.code==='ENOENT')return {removed:[]};throw e;}
|
|
80
|
+
const removed=[];
|
|
81
|
+
for(const name of files)if(/^overlay-[a-f0-9]{20}\.exe$/.test(name)||/^build-[a-f0-9-]{36}\.exe$/.test(name)){
|
|
82
|
+
const path=join(root,name);await noLink(path);if(!(await lstat(path)).isFile())throw Error('Refusing non-file desktop cache entry');await unlink(path);removed.push(name);
|
|
83
|
+
}
|
|
84
|
+
// Unknown files and all other Retrieviewer/agent settings are deliberately kept.
|
|
85
|
+
return {removed,preservedOtherFiles:true};
|
|
86
|
+
}
|
|
87
|
+
export async function removeDesktop(options={}){await stopDesktop(options);return {stopped:true,...await cleanDesktop(options)};}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Pure transport helpers: no art, timers, processes or I/O on import.
|
|
2
|
+
export function viewportOf(message){
|
|
3
|
+
if(message?.type!=='viewport'||!Number.isSafeInteger(message.revision)||message.revision<1||typeof message.visible!=='boolean')throw Error('Invalid overlay viewport');
|
|
4
|
+
if(!message.visible)return null;
|
|
5
|
+
const {columns,rows,cellWidth,cellHeight}=message;
|
|
6
|
+
if(!Number.isInteger(columns)||!Number.isInteger(rows)||columns<1||rows<1||columns*rows>125000||![6,8,10,12,16].includes(cellWidth)||cellHeight!==cellWidth*2)throw Error('Invalid overlay grid');
|
|
7
|
+
return {columns,rows,cellAspect:.5,revision:message.revision,cellWidth,cellHeight};
|
|
8
|
+
}
|
|
9
|
+
export function cropFrame(frame,revision){
|
|
10
|
+
const lines=frame.text.split('\n');
|
|
11
|
+
if(lines.length!==frame.rows||lines.some(s=>s.length!==frame.columns))throw Error('Invalid frame dimensions');
|
|
12
|
+
let left=frame.columns,top=frame.rows,right=-1,bottom=-1;
|
|
13
|
+
for(let y=0;y<lines.length;y++)for(let x=0;x<lines[y].length;x++){
|
|
14
|
+
const c=lines[y].charCodeAt(x);if(c<0x2800||c>0x28ff)throw Error('Only Braille cells may cross the overlay bridge');
|
|
15
|
+
if(c!==0x2800){left=Math.min(left,x);right=Math.max(right,x);top=Math.min(top,y);bottom=Math.max(bottom,y);}
|
|
16
|
+
}
|
|
17
|
+
return {type:'frame',revision,x:right<0?0:left,y:right<0?0:top,columns:right<0?0:right-left+1,rows:right<0?0:bottom-top+1,
|
|
18
|
+
text:right<0?'':lines.slice(top,bottom+1).map(s=>s.slice(left,right+1)).join('\n')};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Request/ack rendering: never queue obsolete frames behind a slow native painter.
|
|
22
|
+
export function createFramePump({pet,send,now=()=>performance.now()}){
|
|
23
|
+
let viewport=null,inFlight=false,last=null,revision=0,paused=false,disposed=false;
|
|
24
|
+
const alive=()=>{if(disposed)throw Error('Overlay pump is disposed');};
|
|
25
|
+
return {
|
|
26
|
+
viewport(message){alive();const next=viewportOf(message);revision=message.revision;viewport=next;last=null;},
|
|
27
|
+
pause(value){alive();paused=!!value;last=null;},
|
|
28
|
+
ack(){alive();inFlight=false;},
|
|
29
|
+
tick(){
|
|
30
|
+
alive();if(inFlight||!viewport||paused)return false;
|
|
31
|
+
const t=now(),dt=last===null?0:Math.max(0,(t-last)/1000);last=t;
|
|
32
|
+
pet.step(dt);const frame=pet.frame(viewport);inFlight=true;
|
|
33
|
+
try{send(cropFrame(frame,revision));}catch(error){inFlight=false;throw error;}return true;
|
|
34
|
+
},
|
|
35
|
+
interact(kind){alive();if(!['ball','treat'].includes(kind))throw Error('Invalid overlay interaction');return pet.interact(kind);},
|
|
36
|
+
get hidden(){return !viewport||paused;},
|
|
37
|
+
dispose(){if(!disposed){disposed=true;viewport=null;pet.dispose();}},
|
|
38
|
+
};
|
|
39
|
+
}
|