@x1a0f3n9/dsh-native-command 0.1.5-rc.5 → 0.1.7-rc.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.
@@ -0,0 +1,26 @@
1
+ /** Decoded application icon bytes suitable for an image response or data URL. */
2
+ export interface DesktopIcon {
3
+ readonly bytes: Buffer;
4
+ readonly contentType: 'image/png' | 'image/svg+xml';
5
+ }
6
+ /**
7
+ * Read the main desktop-entry section without interpreting executable commands.
8
+ * @param text - installed desktop-entry text.
9
+ * @returns its literal field values; action sections are excluded.
10
+ */
11
+ export declare function desktopEntryFields(text: string): Readonly<Record<string, string>>;
12
+ /**
13
+ * Resolve XDG application and icon roots in desktop precedence order.
14
+ * @param home - user's home directory.
15
+ * @param env - desktop environment values.
16
+ * @returns data-home followed by system data directories.
17
+ */
18
+ export declare function desktopDataDirectories(home: string, env: Readonly<Record<string, string | undefined>>): readonly string[];
19
+ /**
20
+ * Resolve an absolute icon path or an installed hicolor/pixmaps icon name.
21
+ * @param name - desktop-entry Icon field.
22
+ * @param directories - XDG data roots in precedence order.
23
+ * @returns image bytes and media type, or null when artwork is unavailable.
24
+ */
25
+ export declare function desktopApplicationIcon(name: string, directories: readonly string[]): Promise<DesktopIcon | null>;
26
+ //# sourceMappingURL=desktop-entry.d.ts.map
@@ -0,0 +1,79 @@
1
+ /** Shared XDG desktop-entry fields and icon lookup for directory and file application catalogs. */
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { isAbsolute, join } from 'node:path';
4
+ /**
5
+ * Read the main desktop-entry section without interpreting executable commands.
6
+ * @param text - installed desktop-entry text.
7
+ * @returns its literal field values; action sections are excluded.
8
+ */
9
+ export function desktopEntryFields(text) {
10
+ let main = false;
11
+ const fields = new Map();
12
+ for (const line of text.split(/\r?\n/)) {
13
+ const trimmed = line.trim();
14
+ if (trimmed.startsWith('[')) {
15
+ main = trimmed === '[Desktop Entry]';
16
+ continue;
17
+ }
18
+ if (!main || trimmed.startsWith('#'))
19
+ continue;
20
+ const separator = trimmed.indexOf('=');
21
+ if (separator > 0)
22
+ fields.set(trimmed.slice(0, separator).trim(), trimmed.slice(separator + 1).trim());
23
+ }
24
+ return Object.fromEntries(fields);
25
+ }
26
+ /**
27
+ * Resolve XDG application and icon roots in desktop precedence order.
28
+ * @param home - user's home directory.
29
+ * @param env - desktop environment values.
30
+ * @returns data-home followed by system data directories.
31
+ */
32
+ export function desktopDataDirectories(home, env) {
33
+ return [env.XDG_DATA_HOME ?? join(home, '.local', 'share'),
34
+ ...(env.XDG_DATA_DIRS ?? '/usr/local/share:/usr/share').split(':').filter(Boolean)];
35
+ }
36
+ /** Read an installed PNG or SVG icon; absent paths and directories have no pixels. */
37
+ async function readIcon(path) {
38
+ const contentType = path.endsWith('.png') ? 'image/png' : path.endsWith('.svg') ? 'image/svg+xml' : null;
39
+ if (contentType === null)
40
+ return null;
41
+ try {
42
+ if (!(await stat(path)).isFile())
43
+ return null;
44
+ return { bytes: await readFile(path), contentType };
45
+ }
46
+ catch (_error) {
47
+ // Missing or unreadable artwork does not remove an otherwise usable application.
48
+ return null;
49
+ }
50
+ }
51
+ /**
52
+ * Resolve an absolute icon path or an installed hicolor/pixmaps icon name.
53
+ * @param name - desktop-entry Icon field.
54
+ * @param directories - XDG data roots in precedence order.
55
+ * @returns image bytes and media type, or null when artwork is unavailable.
56
+ */
57
+ export async function desktopApplicationIcon(name, directories) {
58
+ if (isAbsolute(name))
59
+ return readIcon(name);
60
+ for (const directory of directories) {
61
+ for (const size of ['512x512', '256x256', '128x128', '64x64', '48x48', '32x32']) {
62
+ for (const extension of ['png', 'svg']) {
63
+ const icon = await readIcon(join(directory, 'icons', 'hicolor', size, 'apps', `${name}.${extension}`));
64
+ if (icon !== null)
65
+ return icon;
66
+ }
67
+ }
68
+ const scalable = await readIcon(join(directory, 'icons', 'hicolor', 'scalable', 'apps', `${name}.svg`));
69
+ if (scalable !== null)
70
+ return scalable;
71
+ for (const extension of ['png', 'svg']) {
72
+ const icon = await readIcon(join(directory, 'pixmaps', `${name}.${extension}`));
73
+ if (icon !== null)
74
+ return icon;
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+ //# sourceMappingURL=desktop-entry.js.map
@@ -0,0 +1,12 @@
1
+ import type { NativeCommandRunner } from './runner.ts';
2
+ import type { NativeFileApplication } from './types.ts';
3
+ /**
4
+ * Query all registered file handlers from GIO without executing desktop-entry command text.
5
+ * @param path - verified local file path.
6
+ * @param signal - caller cancellation.
7
+ * @param run - native command runner.
8
+ * @param env - XDG and locale settings.
9
+ * @returns registered applications with the desktop's default marked.
10
+ */
11
+ export declare function linuxFileApplications(path: string, signal: AbortSignal, run: NativeCommandRunner, env: NodeJS.ProcessEnv): Promise<readonly NativeFileApplication[]>;
12
+ //# sourceMappingURL=file-applications-linux.d.ts.map
@@ -0,0 +1,70 @@
1
+ /** Linux file associations from GIO, with shared XDG metadata and artwork lookup. */
2
+ import { readdir, readFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join, relative, sep } from 'node:path';
5
+ import { desktopApplicationIcon, desktopDataDirectories, desktopEntryFields } from "./desktop-entry.js";
6
+ /** Find a desktop id, including ids derived from nested application directories. */
7
+ async function desktopFile(root, id, directory = root) {
8
+ let entries;
9
+ try {
10
+ entries = await readdir(directory, { withFileTypes: true });
11
+ }
12
+ catch (_error) {
13
+ // An absent XDG root contributes no applications.
14
+ return null;
15
+ }
16
+ for (const entry of entries) {
17
+ const path = join(directory, entry.name);
18
+ if (!entry.isDirectory() && relative(root, path).split(sep).join('-') === id)
19
+ return path;
20
+ }
21
+ for (const entry of entries) {
22
+ if (entry.isDirectory()) {
23
+ const found = await desktopFile(root, id, join(directory, entry.name));
24
+ if (found !== null)
25
+ return found;
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ /**
31
+ * Query all registered file handlers from GIO without executing desktop-entry command text.
32
+ * @param path - verified local file path.
33
+ * @param signal - caller cancellation.
34
+ * @param run - native command runner.
35
+ * @param env - XDG and locale settings.
36
+ * @returns registered applications with the desktop's default marked.
37
+ */
38
+ export async function linuxFileApplications(path, signal, run, env) {
39
+ const info = await run('gio', ['info', '-a', 'standard::content-type', path], signal);
40
+ const mime = /standard::content-type:\s*(\S+)/.exec(info.stdout)?.[1];
41
+ if (mime === undefined)
42
+ throw new Error('GIO did not identify the file content type');
43
+ const result = await run('env', ['LC_ALL=C', 'gio', 'mime', mime], signal);
44
+ const preferred = /^Default application.*:\s*(.+\.desktop)\s*$/m.exec(result.stdout)?.[1];
45
+ const ids = [...new Set([...(preferred === undefined ? [] : [preferred]), ...result.stdout.split(/\r?\n/)
46
+ .filter(line => /^\s+.*\.desktop\s*$/.test(line)).map(line => line.trim())])];
47
+ const directories = desktopDataDirectories(env.HOME ?? homedir(), env);
48
+ const locale = (env.LC_ALL ?? env.LC_MESSAGES ?? env.LANG ?? '').replace(/\..*$/, '');
49
+ const applications = [];
50
+ for (const id of ids) {
51
+ signal.throwIfAborted();
52
+ let entryPath = null;
53
+ for (const directory of directories) {
54
+ entryPath = await desktopFile(join(directory, 'applications'), id);
55
+ if (entryPath !== null)
56
+ break;
57
+ }
58
+ if (entryPath === null)
59
+ continue;
60
+ const fields = desktopEntryFields(await readFile(entryPath, 'utf8'));
61
+ const name = fields[`Name[${locale}]`] ?? fields[`Name[${locale.replace(/_.*/, '')}]`] ?? fields.Name;
62
+ if (name === undefined || fields.Hidden === 'true')
63
+ continue;
64
+ const icon = fields.Icon === undefined ? null : await desktopApplicationIcon(fields.Icon, directories);
65
+ applications.push({ id: entryPath, name, default: id === preferred,
66
+ icon: icon === null ? null : `data:${icon.contentType};base64,${icon.bytes.toString('base64')}` });
67
+ }
68
+ return applications;
69
+ }
70
+ //# sourceMappingURL=file-applications-linux.js.map
@@ -0,0 +1,12 @@
1
+ import type { NativeCommandRunner } from './runner.ts';
2
+ /**
3
+ * Execute the Windows Shell adapter in a Unicode STA PowerShell process; the adapter
4
+ * source is a private temporary script file, removed once the call settles.
5
+ * @param path - Windows file path, translated by the caller for WSL.
6
+ * @param application - registered handler to invoke; null requests the application list.
7
+ * @param signal - caller cancellation.
8
+ * @param run - native command runner.
9
+ * @returns adapter output; query mode emits a JSON array.
10
+ */
11
+ export declare function windowsFileApplications(path: string, application: string | null, signal: AbortSignal, run: NativeCommandRunner): Promise<string>;
12
+ //# sourceMappingURL=file-applications-windows.d.ts.map
@@ -0,0 +1,273 @@
1
+ /** Windows Shell association queries and invocation; paths are encoded data, never PowerShell expressions. */
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ /** Shell interfaces are declared in their native vtable order; Invoke preserves packaged-app and DDE handling. */
6
+ const WINDOWS_ASSOCIATIONS = String.raw `
7
+ using System;
8
+ using System.Collections.Generic;
9
+ using System.Drawing;
10
+ using System.Drawing.Imaging;
11
+ using System.IO;
12
+ using System.Runtime.InteropServices;
13
+ using System.Runtime.InteropServices.ComTypes;
14
+ using System.Text;
15
+
16
+ public static class DshFileAssociations {
17
+ [ComImport, Guid("973810ae-9599-4b88-9e4d-6ee98c9552da"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
18
+ public interface IEnumHandlers {
19
+ [PreserveSig] int Next(uint count, out IHandler handler, out uint fetched);
20
+ }
21
+ [ComImport, Guid("f04061ac-1659-4a3f-a954-775aa57fc083"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
22
+ public interface IHandler {
23
+ void GetName([MarshalAs(UnmanagedType.LPWStr)] out string name);
24
+ void GetUIName([MarshalAs(UnmanagedType.LPWStr)] out string name);
25
+ void GetIconLocation([MarshalAs(UnmanagedType.LPWStr)] out string path, out int index);
26
+ [PreserveSig] int IsRecommended();
27
+ void MakeDefault([MarshalAs(UnmanagedType.LPWStr)] string description);
28
+ void Invoke(IDataObject data);
29
+ void CreateInvoker(IDataObject data, [MarshalAs(UnmanagedType.Interface)] out object invoker);
30
+ }
31
+ [ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
32
+ public interface IShellItem {
33
+ void BindToHandler(IntPtr context, ref Guid handler, ref Guid iid, [MarshalAs(UnmanagedType.Interface)] out IDataObject data);
34
+ }
35
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
36
+ static extern void SHAssocEnumHandlers(string extension, uint filter, out IEnumHandlers handlers);
37
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
38
+ static extern void SHCreateItemFromParsingName(string path, IntPtr context, ref Guid iid, out IShellItem item);
39
+ [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
40
+ static extern int AssocQueryString(uint flags, uint kind, string association, string extra, StringBuilder output, ref uint size);
41
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
42
+ static extern int SHDefExtractIcon(string path, int index, uint flags, out IntPtr large, out IntPtr small, uint size);
43
+ [DllImport("user32.dll")]
44
+ static extern bool DestroyIcon(IntPtr icon);
45
+ [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
46
+ static extern int SHLoadIndirectString(string source, StringBuilder output, uint size, IntPtr reserved);
47
+
48
+ [DllImport("shlwapi.dll", PreserveSig = false)]
49
+ static extern void SHCreateThreadRef(IntPtr count, out IntPtr reference);
50
+ [DllImport("shlwapi.dll", PreserveSig = false)]
51
+ static extern void SHSetThreadRef(IntPtr reference);
52
+ [DllImport("shell32.dll")]
53
+ static extern void SHSetInstanceExplorer(IntPtr reference);
54
+ [StructLayout(LayoutKind.Sequential)]
55
+ struct Message {
56
+ public IntPtr window;
57
+ public uint message;
58
+ public UIntPtr wParam;
59
+ public IntPtr lParam;
60
+ public uint time;
61
+ public int x, y;
62
+ public uint reserved;
63
+ }
64
+ [DllImport("user32.dll", SetLastError = true)]
65
+ static extern UIntPtr SetTimer(IntPtr window, UIntPtr id, uint milliseconds, IntPtr callback);
66
+ [DllImport("user32.dll")]
67
+ static extern bool KillTimer(IntPtr window, UIntPtr id);
68
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
69
+ static extern int GetMessage(out Message message, IntPtr window, uint min, uint max);
70
+ [DllImport("user32.dll")]
71
+ static extern bool TranslateMessage(ref Message message);
72
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
73
+ static extern IntPtr DispatchMessage(ref Message message);
74
+
75
+ // Shell handlers may return before their asynchronous launch work releases the host.
76
+ // The shared Shell reference, rather than an arbitrary delay, owns that lifetime.
77
+ static void WithShellLifetime(Action action) {
78
+ IntPtr count = Marshal.AllocHGlobal(sizeof(int));
79
+ IntPtr reference = IntPtr.Zero;
80
+ try {
81
+ Marshal.WriteInt32(count, 0);
82
+ SHCreateThreadRef(count, out reference);
83
+ SHSetThreadRef(reference);
84
+ SHSetInstanceExplorer(reference);
85
+ int owned = Marshal.ReadInt32(count);
86
+ try { action(); }
87
+ finally {
88
+ if (Marshal.ReadInt32(count) > owned) {
89
+ // USER_TIMER_MINIMUM wakes the STA to inspect the reference count while dispatching COM work.
90
+ UIntPtr timer = SetTimer(IntPtr.Zero, UIntPtr.Zero, 10, IntPtr.Zero);
91
+ if (timer == UIntPtr.Zero) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
92
+ try {
93
+ while (Marshal.ReadInt32(count) > owned) {
94
+ Message message;
95
+ if (GetMessage(out message, IntPtr.Zero, 0, 0) <= 0) throw new InvalidOperationException("Shell handoff message loop ended");
96
+ TranslateMessage(ref message);
97
+ DispatchMessage(ref message);
98
+ }
99
+ } finally { KillTimer(IntPtr.Zero, timer); }
100
+ }
101
+ }
102
+ } finally {
103
+ SHSetInstanceExplorer(IntPtr.Zero);
104
+ SHSetThreadRef(IntPtr.Zero);
105
+ int remaining = reference == IntPtr.Zero ? 0 : Marshal.Release(reference);
106
+ // An interrupted drain leaves its counter owned by the terminating helper process.
107
+ if (remaining == 0) Marshal.FreeHGlobal(count);
108
+ }
109
+ }
110
+
111
+ public sealed class Application {
112
+ public string id;
113
+ public string name;
114
+ public string icon;
115
+ public bool @default;
116
+ }
117
+ static string Associated(string extension, uint kind) {
118
+ uint size = 0;
119
+ AssocQueryString(0, kind, extension, null, null, ref size);
120
+ if (size == 0) return null;
121
+ var text = new StringBuilder((int)size);
122
+ return AssocQueryString(0, kind, extension, null, text, ref size) == 0 ? text.ToString() : null;
123
+ }
124
+ // Resolve a packaged application's indirect icon reference, for example
125
+ // @{Microsoft.WindowsNotepad_...?ms-resource://.../NotepadAppList.png}, to
126
+ // the resource file the Shell would draw; an unresolved reference is null.
127
+ static string ResolveIndirectIcon(string source) {
128
+ var output = new StringBuilder(1024);
129
+ return SHLoadIndirectString(source, output, (uint)output.Capacity, IntPtr.Zero) == 0 ? output.ToString() : null;
130
+ }
131
+ // Read one icon source into a 32px PNG: an image file directly, otherwise the
132
+ // resource the Shell extracts at the given index. A missing resource is null.
133
+ static string PngFromIconSource(string source, int index) {
134
+ IntPtr large = IntPtr.Zero, small = IntPtr.Zero;
135
+ try {
136
+ if (Path.GetExtension(source).Equals(".png", StringComparison.OrdinalIgnoreCase)) {
137
+ using (var original = Image.FromFile(source))
138
+ using (var resized = new Bitmap(original, new Size(32, 32)))
139
+ using (var stream = new MemoryStream()) {
140
+ resized.Save(stream, ImageFormat.Png);
141
+ return "data:image/png;base64," + Convert.ToBase64String(stream.ToArray());
142
+ }
143
+ }
144
+ if (SHDefExtractIcon(source, index, 0, out large, out small, 32) != 0 || large == IntPtr.Zero) return null;
145
+ using (var image = Icon.FromHandle(large))
146
+ using (var bitmap = image.ToBitmap())
147
+ using (var stream = new MemoryStream()) {
148
+ bitmap.Save(stream, ImageFormat.Png);
149
+ return "data:image/png;base64," + Convert.ToBase64String(stream.ToArray());
150
+ }
151
+ } catch (Exception) {
152
+ // Missing icon resources do not make the application unusable.
153
+ return null;
154
+ } finally {
155
+ if (large != IntPtr.Zero) DestroyIcon(large);
156
+ if (small != IntPtr.Zero) DestroyIcon(small);
157
+ }
158
+ }
159
+ // Best-effort fallback: some handlers return their executable path from GetName, but a
160
+ // packaged handler's name may be an AUMID or family name, which extracts no icon.
161
+ static string IconData(IHandler handler, string id) {
162
+ string source = null; int index = 0;
163
+ try { handler.GetIconLocation(out source, out index); } catch (Exception) {
164
+ // A handler without a drawable icon location falls back to its name.
165
+ }
166
+ if (!String.IsNullOrEmpty(source)) {
167
+ source = Environment.ExpandEnvironmentVariables(source);
168
+ if (source.StartsWith("@{", StringComparison.Ordinal) && source.EndsWith("}", StringComparison.Ordinal)) {
169
+ var resolved = ResolveIndirectIcon(source);
170
+ if (!String.IsNullOrEmpty(resolved)) source = resolved;
171
+ }
172
+ var icon = PngFromIconSource(source, index);
173
+ if (icon != null) return icon;
174
+ }
175
+ return String.IsNullOrEmpty(id) || String.Equals(id, source, StringComparison.OrdinalIgnoreCase)
176
+ ? null
177
+ : PngFromIconSource(id, 0);
178
+ }
179
+ static void Visit(string path, Action<IHandler> visit) {
180
+ var extension = Path.GetExtension(path);
181
+ if (extension.Length == 0) return;
182
+ IEnumHandlers handlers;
183
+ SHAssocEnumHandlers(extension, 0, out handlers);
184
+ try {
185
+ while (true) {
186
+ IHandler handler; uint fetched;
187
+ int result = handlers.Next(1, out handler, out fetched);
188
+ Marshal.ThrowExceptionForHR(result);
189
+ if (result != 0 || fetched == 0) break;
190
+ try { visit(handler); } finally { Marshal.FinalReleaseComObject(handler); }
191
+ }
192
+ } finally { Marshal.FinalReleaseComObject(handlers); }
193
+ }
194
+ public static Application[] List(string path) {
195
+ var extension = Path.GetExtension(path);
196
+ var executable = extension.Length == 0 ? null : Associated(extension, 2);
197
+ var appId = extension.Length == 0 ? null : Associated(extension, 21);
198
+ var apps = new List<Application>();
199
+ var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
200
+ Visit(path, delegate(IHandler handler) {
201
+ string id, name;
202
+ handler.GetName(out id); handler.GetUIName(out name);
203
+ if (!seen.Add(id)) return;
204
+ apps.Add(new Application { id = id, name = name, icon = IconData(handler, id),
205
+ @default = String.Equals(id, executable, StringComparison.OrdinalIgnoreCase) || String.Equals(id, appId, StringComparison.OrdinalIgnoreCase) });
206
+ });
207
+ return apps.ToArray();
208
+ }
209
+ public static void Open(string path, string application) {
210
+ WithShellLifetime(delegate { OpenRegistered(path, application); });
211
+ }
212
+ static void OpenRegistered(string path, string application) {
213
+ bool opened = false;
214
+ Visit(path, delegate(IHandler handler) {
215
+ string id; handler.GetName(out id);
216
+ if (opened || !String.Equals(id, application, StringComparison.OrdinalIgnoreCase)) return;
217
+ var iid = typeof(IShellItem).GUID;
218
+ IShellItem item;
219
+ SHCreateItemFromParsingName(path, IntPtr.Zero, ref iid, out item);
220
+ IDataObject data = null;
221
+ try {
222
+ var bhid = new Guid("b8c0bd9f-ed24-455c-83e6-d5390c4fe8c4");
223
+ var dataIid = typeof(IDataObject).GUID;
224
+ item.BindToHandler(IntPtr.Zero, ref bhid, ref dataIid, out data);
225
+ handler.Invoke(data);
226
+ opened = true;
227
+ } finally {
228
+ if (data != null) Marshal.FinalReleaseComObject(data);
229
+ Marshal.FinalReleaseComObject(item);
230
+ }
231
+ });
232
+ if (!opened) throw new InvalidOperationException("Application is not registered for this file");
233
+ }
234
+ }`;
235
+ /**
236
+ * Execute the Windows Shell adapter in a Unicode STA PowerShell process; the adapter
237
+ * source is a private temporary script file, removed once the call settles.
238
+ * @param path - Windows file path, translated by the caller for WSL.
239
+ * @param application - registered handler to invoke; null requests the application list.
240
+ * @param signal - caller cancellation.
241
+ * @param run - native command runner.
242
+ * @returns adapter output; query mode emits a JSON array.
243
+ */
244
+ export async function windowsFileApplications(path, application, signal, run) {
245
+ const encodedPath = Buffer.from(path).toString('base64');
246
+ const encodedApplication = Buffer.from(application ?? '').toString('base64');
247
+ const script = `$ErrorActionPreference = 'Stop'
248
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
249
+ Add-Type -ReferencedAssemblies System,System.Core,System.Drawing -TypeDefinition @'
250
+ ${WINDOWS_ASSOCIATIONS}
251
+ '@
252
+ $path = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedPath}'))
253
+ $application = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedApplication}'))
254
+ ${application === null ? 'ConvertTo-Json -InputObject @([DshFileAssociations]::List($path)) -Depth 4 -Compress' : '[DshFileAssociations]::Open($path, $application)'}
255
+ `;
256
+ // The script travels as a file, not as command-line data: -EncodedCommand grew with the
257
+ // embedded C# and overflowed the 32767-character CreateProcess limit for long paths.
258
+ // PowerShell 5.1 decodes a -File script as ANSI unless it carries a BOM; the BOM keeps
259
+ // the embedded UTF-8 source intact.
260
+ const directory = await mkdtemp(join(tmpdir(), 'dsh-native-command-'));
261
+ const scriptPath = join(directory, 'associations.ps1');
262
+ try {
263
+ await writeFile(scriptPath, `\uFEFF${script}`, 'utf8');
264
+ const result = await run('powershell.exe', [
265
+ '-NoProfile', '-NonInteractive', '-STA', '-ExecutionPolicy', 'Bypass', '-File', scriptPath,
266
+ ], signal);
267
+ return result.stdout;
268
+ }
269
+ finally {
270
+ await rm(directory, { recursive: true, force: true });
271
+ }
272
+ }
273
+ //# sourceMappingURL=file-applications-windows.js.map
@@ -0,0 +1,22 @@
1
+ import type { PathOpenerInternals } from './path-opener.ts';
2
+ import { type NativeFileApplication } from './types.ts';
3
+ /**
4
+ * List registered handlers in OS preference order, including the current default and application icons.
5
+ * @param path - verified absolute local file path.
6
+ * @param signal - caller lifetime, propagated to the OS query.
7
+ * @param internals - platform and command adapter for deterministic tests.
8
+ * @returns current file handlers; on macOS, copies sharing a bundle identifier and display name collapse
9
+ * to one entry; an empty list when the platform has no association query.
10
+ */
11
+ export declare function nativeFileApplications(path: string, signal: AbortSignal, internals?: PathOpenerInternals): Promise<readonly NativeFileApplication[]>;
12
+ /**
13
+ * Open a file in a currently registered handler; stale or arbitrary application identifiers are rejected.
14
+ * Validation checks the complete registered list, so macOS copies collapsed out of the display list stay openable.
15
+ * @param path - verified absolute local file path.
16
+ * @param application - identifier returned by the file association query.
17
+ * @param signal - caller lifetime, propagated to query and launch.
18
+ * @param internals - platform and command adapter for deterministic tests.
19
+ * @returns after the system launcher accepts the file.
20
+ */
21
+ export declare function openNativeFileApplication(path: string, application: string, signal: AbortSignal, internals?: PathOpenerInternals): Promise<void>;
22
+ //# sourceMappingURL=file-applications.d.ts.map