@optique/core 0.5.0 → 0.6.0-dev.102

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,513 @@
1
+ const require_message = require('./message.cjs');
2
+
3
+ //#region src/completion.ts
4
+ /**
5
+ * The Bash shell completion generator.
6
+ * @since 0.6.0
7
+ */
8
+ const bash = {
9
+ name: "bash",
10
+ generateScript(programName, args = []) {
11
+ const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
12
+ return `
13
+ function _${programName} () {
14
+ COMPREPLY=()
15
+ local current="\${COMP_WORDS[COMP_CWORD]}"
16
+ local prev=("\${COMP_WORDS[@]:1:COMP_CWORD-1}")
17
+ while IFS= read -r line; do
18
+ if [[ "$line" == __FILE__:* ]]; then
19
+ # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
20
+ IFS=':' read -r _ type extensions pattern hidden <<< "$line"
21
+
22
+ # Generate file completions based on type
23
+ case "$type" in
24
+ file)
25
+ # Complete files only
26
+ if [[ -n "$extensions" ]]; then
27
+ # Complete with extension filtering
28
+ local ext_pattern="\${extensions//,/|}"
29
+ for file in "$current"*; do
30
+ [[ -e "$file" && "$file" =~ \\.($ext_pattern)$ ]] && COMPREPLY+=("$file")
31
+ done
32
+ else
33
+ # Complete files only, exclude directories
34
+ while IFS= read -r -d '' item; do
35
+ [[ -f "$item" ]] && COMPREPLY+=("$item")
36
+ done < <(compgen -f -z -- "$current")
37
+ fi
38
+ ;;
39
+ directory)
40
+ # Complete directories only
41
+ while IFS= read -r -d '' dir; do
42
+ COMPREPLY+=("$dir/")
43
+ done < <(compgen -d -z -- "$current")
44
+ ;;
45
+ any)
46
+ # Complete both files and directories
47
+ if [[ -n "$extensions" ]]; then
48
+ # Files with extension filtering + directories
49
+ # Files with extension filtering
50
+ local ext_pattern="\${extensions//,/|}"
51
+ for item in "$current"*; do
52
+ if [[ -d "$item" ]]; then
53
+ COMPREPLY+=("$item/")
54
+ elif [[ -f "$item" && "$item" =~ \\.($ext_pattern)$ ]]; then
55
+ COMPREPLY+=("$item")
56
+ fi
57
+ done
58
+ else
59
+ # Complete files and directories, add slash to directories
60
+ while IFS= read -r -d '' item; do
61
+ if [[ -d "$item" ]]; then
62
+ COMPREPLY+=("$item/")
63
+ else
64
+ COMPREPLY+=("$item")
65
+ fi
66
+ done < <(compgen -f -z -- "$current")
67
+ fi
68
+ ;;
69
+ esac
70
+
71
+ # Filter out hidden files unless requested
72
+ if [[ "$hidden" != "1" && "$current" != .* ]]; then
73
+ local filtered=()
74
+ for item in "\${COMPREPLY[@]}"; do
75
+ [[ "$(basename "$item")" != .* ]] && filtered+=("$item")
76
+ done
77
+ COMPREPLY=("\${filtered[@]}")
78
+ fi
79
+ else
80
+ # Regular literal completion
81
+ COMPREPLY+=("$line")
82
+ fi
83
+ done < <(${programName} ${escapedArgs} "\${prev[@]}" "$current" 2>/dev/null)
84
+ }
85
+
86
+ complete -F _${programName} ${programName}
87
+ `;
88
+ },
89
+ *encodeSuggestions(suggestions) {
90
+ let i = 0;
91
+ for (const suggestion of suggestions) {
92
+ if (i > 0) yield "\n";
93
+ if (suggestion.kind === "literal") yield `${suggestion.text}`;
94
+ else {
95
+ const extensions = suggestion.extensions?.join(",") || "";
96
+ const hidden = suggestion.includeHidden ? "1" : "0";
97
+ yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}`;
98
+ }
99
+ i++;
100
+ }
101
+ }
102
+ };
103
+ /**
104
+ * The Zsh shell completion generator.
105
+ * @since 0.6.0
106
+ */
107
+ const zsh = {
108
+ name: "zsh",
109
+ generateScript(programName, args = []) {
110
+ const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ");
111
+ return `
112
+ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
113
+ local current="\$words[CURRENT]"
114
+ local -a prev
115
+ # Extract previous arguments, skipping empty ones
116
+ prev=()
117
+ local i
118
+ for (( i=2; i < CURRENT; i++ )); do
119
+ if [[ -n "\$words[i]" ]]; then
120
+ prev+=("\$words[i]")
121
+ fi
122
+ done
123
+
124
+ # Call the completion function and capture output
125
+ local output
126
+ if (( \${#prev[@]} == 0 )); then
127
+ output=\$(${programName} ${escapedArgs} "\$current" 2>/dev/null)
128
+ else
129
+ output=\$(${programName} ${escapedArgs} "\${prev[@]}" "\$current" 2>/dev/null)
130
+ fi
131
+
132
+ # Split output into lines and process each line
133
+ local -a completions descriptions
134
+ local line value desc
135
+ local has_file_completion=0
136
+
137
+ while IFS= read -r line; do
138
+ if [[ -n "\$line" ]]; then
139
+ # Split by null character - first part is value, second is description
140
+ value=\${line%%\$'\\0'*}
141
+ desc=\${line#*\$'\\0'}
142
+ desc=\${desc%%\$'\\0'*}
143
+
144
+ if [[ "\$value" == __FILE__:* ]]; then
145
+ # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
146
+ local type extensions pattern hidden
147
+ IFS=':' read -r _ type extensions pattern hidden <<< "\$value"
148
+ has_file_completion=1
149
+
150
+ # Use zsh's native file completion
151
+ case "\$type" in
152
+ file)
153
+ if [[ -n "\$extensions" ]]; then
154
+ # Complete files with extension filtering
155
+ local ext_pattern="*.(\\$\{extensions//,/|\})"
156
+ _files -g "\\$ext_pattern"
157
+ else
158
+ _files -g "*"
159
+ fi
160
+ ;;
161
+ directory)
162
+ _directories
163
+ ;;
164
+ any)
165
+ if [[ -n "\$extensions" ]]; then
166
+ # Complete both files and directories, with extension filtering for files
167
+ local ext_pattern="*.(\\$\{extensions//,/|\})"
168
+ _files -g "\\$ext_pattern" && _directories
169
+ else
170
+ _files
171
+ fi
172
+ ;;
173
+ esac
174
+
175
+ # Note: zsh's _files and _directories handle hidden file filtering automatically
176
+ # based on the completion context and user settings
177
+ else
178
+ # Regular literal completion
179
+ if [[ -n "\$value" ]]; then
180
+ completions+=("\$value")
181
+ descriptions+=("\$desc")
182
+ fi
183
+ fi
184
+ fi
185
+ done <<< "\$output"
186
+
187
+ # Add literal completions with descriptions if we have any
188
+ if (( \${#completions[@]} > 0 )); then
189
+ # Prepare completion with descriptions for _describe
190
+ local -a matches
191
+ local -i i
192
+ for (( i=1; i <= \${#completions[@]}; i++ )); do
193
+ if [[ -n "\${descriptions[i]}" ]]; then
194
+ matches+=("\${completions[i]}:\${descriptions[i]}")
195
+ else
196
+ matches+=("\${completions[i]}")
197
+ fi
198
+ done
199
+ _describe 'commands' matches
200
+ fi
201
+ }
202
+
203
+ compdef _${programName.replace(/[^a-zA-Z0-9]/g, "_")} ${programName}
204
+ `;
205
+ },
206
+ *encodeSuggestions(suggestions) {
207
+ for (const suggestion of suggestions) if (suggestion.kind === "literal") {
208
+ const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
209
+ yield `${suggestion.text}\0${description}\0`;
210
+ } else {
211
+ const extensions = suggestion.extensions?.join(",") || "";
212
+ const hidden = suggestion.includeHidden ? "1" : "0";
213
+ const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
214
+ yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\0${description}\0`;
215
+ }
216
+ }
217
+ };
218
+ /**
219
+ * The fish shell completion generator.
220
+ * @since 0.6.0
221
+ */
222
+ const fish = {
223
+ name: "fish",
224
+ generateScript(programName, args = []) {
225
+ const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "\\'")}'`).join(" ");
226
+ const functionName = `__${programName.replace(/[^a-zA-Z0-9]/g, "_")}_complete`;
227
+ return `
228
+ function ${functionName}
229
+ set -l tokens (commandline -poc)
230
+ set -l current (commandline -ct)
231
+
232
+ # Extract previous arguments (skip the command name)
233
+ set -l prev
234
+ set -l count (count $tokens)
235
+ if test $count -gt 1
236
+ set prev $tokens[2..$count]
237
+ end
238
+
239
+ # Call completion command and capture output
240
+ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current 2>/dev/null)\n` : ` set -l output (${programName} $prev $current 2>/dev/null)\n`}
241
+ # Process each line of output
242
+ for line in $output
243
+ if string match -q '__FILE__:*' -- $line
244
+ # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
245
+ set -l parts (string split ':' -- $line)
246
+ set -l type $parts[2]
247
+ set -l extensions $parts[3]
248
+ set -l pattern $parts[4]
249
+ set -l hidden $parts[5]
250
+
251
+ # Generate file completions based on type
252
+ set -l items
253
+ switch $type
254
+ case file
255
+ # Complete files only
256
+ for item in $current*
257
+ if test -f $item
258
+ set -a items $item
259
+ end
260
+ end
261
+ case directory
262
+ # Complete directories only
263
+ for item in $current*
264
+ if test -d $item
265
+ set -a items $item/
266
+ end
267
+ end
268
+ case any
269
+ # Complete both files and directories
270
+ for item in $current*
271
+ if test -d $item
272
+ set -a items $item/
273
+ else if test -f $item
274
+ set -a items $item
275
+ end
276
+ end
277
+ end
278
+
279
+ # Filter by extensions if specified
280
+ if test -n "$extensions" -a "$type" != directory
281
+ set -l filtered
282
+ set -l ext_list (string split ',' -- $extensions)
283
+ for item in $items
284
+ # Skip directories, they don't have extensions
285
+ if string match -q '*/' -- $item
286
+ set -a filtered $item
287
+ continue
288
+ end
289
+ # Check if file matches any extension
290
+ for ext in $ext_list
291
+ if string match -q "*.$ext" -- $item
292
+ set -a filtered $item
293
+ break
294
+ end
295
+ end
296
+ end
297
+ set items $filtered
298
+ end
299
+
300
+ # Filter out hidden files unless requested
301
+ if test "$hidden" != "1" -a (string sub -l 1 -- $current) != "."
302
+ set -l filtered
303
+ for item in $items
304
+ set -l basename (basename $item)
305
+ if not string match -q '.*' -- $basename
306
+ set -a filtered $item
307
+ end
308
+ end
309
+ set items $filtered
310
+ end
311
+
312
+ # Output file completions
313
+ for item in $items
314
+ echo $item
315
+ end
316
+ else
317
+ # Regular literal completion - split by tab
318
+ set -l parts (string split \\t -- $line)
319
+ if test (count $parts) -ge 2
320
+ # value\tdescription format
321
+ echo $parts[1]\\t$parts[2]
322
+ else
323
+ # Just value
324
+ echo $line
325
+ end
326
+ end
327
+ end
328
+ end
329
+
330
+ complete -c ${programName} -f -a '(${functionName})'
331
+ `;
332
+ },
333
+ *encodeSuggestions(suggestions) {
334
+ let i = 0;
335
+ for (const suggestion of suggestions) {
336
+ if (i > 0) yield "\n";
337
+ if (suggestion.kind === "literal") {
338
+ const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
339
+ yield `${suggestion.text}\t${description}`;
340
+ } else {
341
+ const extensions = suggestion.extensions?.join(",") || "";
342
+ const hidden = suggestion.includeHidden ? "1" : "0";
343
+ const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
344
+ yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\t${description}`;
345
+ }
346
+ i++;
347
+ }
348
+ }
349
+ };
350
+ /**
351
+ * The PowerShell completion generator.
352
+ * @since 0.6.0
353
+ */
354
+ const pwsh = {
355
+ name: "pwsh",
356
+ generateScript(programName, args = []) {
357
+ const escapedArgs = args.map((arg) => `'${arg.replace(/'/g, "''")}'`).join(", ");
358
+ return `
359
+ Register-ArgumentCompleter -Native -CommandName ${programName} -ScriptBlock {
360
+ param(\$wordToComplete, \$commandAst, \$cursorPosition)
361
+
362
+ # Extract arguments from AST (handles quoted strings properly)
363
+ \$arguments = @()
364
+ \$commandElements = \$commandAst.CommandElements
365
+
366
+ # Determine the range of elements to extract
367
+ # Exclude the last element if it matches wordToComplete (partial input case)
368
+ \$maxIndex = \$commandElements.Count - 1
369
+ if (\$commandElements.Count -gt 1) {
370
+ \$lastElement = \$commandElements[\$commandElements.Count - 1]
371
+ \$lastText = if (\$lastElement -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
372
+ \$lastElement.Value
373
+ } else {
374
+ \$lastElement.Extent.Text
375
+ }
376
+ if (\$lastText -eq \$wordToComplete) {
377
+ \$maxIndex = \$commandElements.Count - 2
378
+ }
379
+ }
380
+
381
+ for (\$i = 1; \$i -le \$maxIndex; \$i++) {
382
+ \$element = \$commandElements[\$i]
383
+
384
+ if (\$element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
385
+ \$arguments += \$element.Value
386
+ } else {
387
+ \$arguments += \$element.Extent.Text
388
+ }
389
+ }
390
+
391
+ # Build arguments array for completion command
392
+ \$completionArgs = @()
393
+ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
394
+ ` : ""} \$completionArgs += \$arguments
395
+ \$completionArgs += \$wordToComplete
396
+
397
+ # Call completion command and capture output
398
+ try {
399
+ \$output = & ${programName} \$completionArgs 2>\$null
400
+ if (-not \$output) { return }
401
+
402
+ # Parse tab-separated output and create CompletionResult objects
403
+ \$output -split "\`n" | ForEach-Object {
404
+ \$line = \$_.Trim()
405
+ if (-not \$line) { return }
406
+
407
+ if (\$line -match '^__FILE__:') {
408
+ # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
409
+ \$parts = \$line -split ':', 5
410
+ \$type = \$parts[1]
411
+ \$extensions = \$parts[2]
412
+ \$pattern = \$parts[3]
413
+ \$hidden = \$parts[4] -eq '1'
414
+
415
+ # Determine current prefix for file matching
416
+ \$prefix = if (\$wordToComplete) { \$wordToComplete } else { '' }
417
+
418
+ # Get file system items based on type
419
+ \$items = @()
420
+ switch (\$type) {
421
+ 'file' {
422
+ if (\$extensions) {
423
+ # Filter by extensions
424
+ \$extList = \$extensions -split ','
425
+ \$items = Get-ChildItem -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
426
+ Where-Object {
427
+ \$ext = \$_.Extension
428
+ \$extList | ForEach-Object { if (\$ext -eq ".\$_") { return \$true } }
429
+ }
430
+ } else {
431
+ \$items = Get-ChildItem -File -Path "\${prefix}*" -ErrorAction SilentlyContinue
432
+ }
433
+ }
434
+ 'directory' {
435
+ \$items = Get-ChildItem -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
436
+ }
437
+ 'any' {
438
+ if (\$extensions) {
439
+ # Get directories and filtered files
440
+ \$dirs = Get-ChildItem -Directory -Path "\${prefix}*" -ErrorAction SilentlyContinue
441
+ \$extList = \$extensions -split ','
442
+ \$files = Get-ChildItem -File -Path "\${prefix}*" -ErrorAction SilentlyContinue |
443
+ Where-Object {
444
+ \$ext = \$_.Extension
445
+ \$extList | ForEach-Object { if (\$ext -eq ".\$_") { return \$true } }
446
+ }
447
+ \$items = \$dirs + \$files
448
+ } else {
449
+ \$items = Get-ChildItem -Path "\${prefix}*" -ErrorAction SilentlyContinue
450
+ }
451
+ }
452
+ }
453
+
454
+ # Filter hidden files unless requested
455
+ if (-not \$hidden) {
456
+ \$items = \$items | Where-Object { -not \$_.Attributes.HasFlag([System.IO.FileAttributes]::Hidden) }
457
+ }
458
+
459
+ # Create completion results for files
460
+ \$items | ForEach-Object {
461
+ \$completionText = if (\$_.PSIsContainer) { "\$(\$_.Name)/" } else { \$_.Name }
462
+ \$itemType = if (\$_.PSIsContainer) { 'Directory' } else { 'File' }
463
+ [System.Management.Automation.CompletionResult]::new(
464
+ \$completionText,
465
+ \$completionText,
466
+ 'ParameterValue',
467
+ \$itemType
468
+ )
469
+ }
470
+ } else {
471
+ # Parse literal completion: text\\tlistItemText\\tdescription
472
+ \$parts = \$line -split "\`t", 3
473
+ \$completionText = \$parts[0]
474
+ \$listItemText = if (\$parts.Length -gt 1 -and \$parts[1]) { \$parts[1] } else { \$completionText }
475
+ \$toolTip = if (\$parts.Length -gt 2 -and \$parts[2]) { \$parts[2] } else { \$completionText }
476
+
477
+ [System.Management.Automation.CompletionResult]::new(
478
+ \$completionText,
479
+ \$listItemText,
480
+ 'ParameterValue',
481
+ \$toolTip
482
+ )
483
+ }
484
+ }
485
+ } catch {
486
+ # Silently ignore errors
487
+ }
488
+ }
489
+ `;
490
+ },
491
+ *encodeSuggestions(suggestions) {
492
+ let i = 0;
493
+ for (const suggestion of suggestions) {
494
+ if (i > 0) yield "\n";
495
+ if (suggestion.kind === "literal") {
496
+ const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
497
+ yield `${suggestion.text}\t${suggestion.text}\t${description}`;
498
+ } else {
499
+ const extensions = suggestion.extensions?.join(",") || "";
500
+ const hidden = suggestion.includeHidden ? "1" : "0";
501
+ const description = suggestion.description == null ? "" : require_message.formatMessage(suggestion.description, { colors: false });
502
+ yield `__FILE__:${suggestion.type}:${extensions}:${suggestion.pattern || ""}:${hidden}\t[file]\t${description}`;
503
+ }
504
+ i++;
505
+ }
506
+ }
507
+ };
508
+
509
+ //#endregion
510
+ exports.bash = bash;
511
+ exports.fish = fish;
512
+ exports.pwsh = pwsh;
513
+ exports.zsh = zsh;
@@ -0,0 +1,51 @@
1
+ import { Suggestion } from "./parser.cjs";
2
+
3
+ //#region src/completion.d.ts
4
+
5
+ /**
6
+ * A shell completion generator.
7
+ * @since 0.6.0
8
+ */
9
+ interface ShellCompletion {
10
+ /**
11
+ * The name of the shell.
12
+ */
13
+ readonly name: string;
14
+ /**
15
+ * Generates a shell completion script for the given program name.
16
+ * @param programName The name of the program.
17
+ * @param args The arguments passed to the program. If omitted, an empty
18
+ * array is used.
19
+ * @returns The shell completion script.
20
+ */
21
+ generateScript(programName: string, args?: readonly string[]): string;
22
+ /**
23
+ * Encodes {@link Suggestion}s into chunks of strings suitable for the shell.
24
+ * All chunks will be joined without any separator.
25
+ * @param suggestions The suggestions to encode.
26
+ * @returns The encoded suggestions.
27
+ */
28
+ encodeSuggestions(suggestions: readonly Suggestion[]): Iterable<string>;
29
+ }
30
+ /**
31
+ * The Bash shell completion generator.
32
+ * @since 0.6.0
33
+ */
34
+ declare const bash: ShellCompletion;
35
+ /**
36
+ * The Zsh shell completion generator.
37
+ * @since 0.6.0
38
+ */
39
+ declare const zsh: ShellCompletion;
40
+ /**
41
+ * The fish shell completion generator.
42
+ * @since 0.6.0
43
+ */
44
+ declare const fish: ShellCompletion;
45
+ /**
46
+ * The PowerShell completion generator.
47
+ * @since 0.6.0
48
+ */
49
+ declare const pwsh: ShellCompletion;
50
+ //#endregion
51
+ export { ShellCompletion, bash, fish, pwsh, zsh };
@@ -0,0 +1,51 @@
1
+ import { Suggestion } from "./parser.js";
2
+
3
+ //#region src/completion.d.ts
4
+
5
+ /**
6
+ * A shell completion generator.
7
+ * @since 0.6.0
8
+ */
9
+ interface ShellCompletion {
10
+ /**
11
+ * The name of the shell.
12
+ */
13
+ readonly name: string;
14
+ /**
15
+ * Generates a shell completion script for the given program name.
16
+ * @param programName The name of the program.
17
+ * @param args The arguments passed to the program. If omitted, an empty
18
+ * array is used.
19
+ * @returns The shell completion script.
20
+ */
21
+ generateScript(programName: string, args?: readonly string[]): string;
22
+ /**
23
+ * Encodes {@link Suggestion}s into chunks of strings suitable for the shell.
24
+ * All chunks will be joined without any separator.
25
+ * @param suggestions The suggestions to encode.
26
+ * @returns The encoded suggestions.
27
+ */
28
+ encodeSuggestions(suggestions: readonly Suggestion[]): Iterable<string>;
29
+ }
30
+ /**
31
+ * The Bash shell completion generator.
32
+ * @since 0.6.0
33
+ */
34
+ declare const bash: ShellCompletion;
35
+ /**
36
+ * The Zsh shell completion generator.
37
+ * @since 0.6.0
38
+ */
39
+ declare const zsh: ShellCompletion;
40
+ /**
41
+ * The fish shell completion generator.
42
+ * @since 0.6.0
43
+ */
44
+ declare const fish: ShellCompletion;
45
+ /**
46
+ * The PowerShell completion generator.
47
+ * @since 0.6.0
48
+ */
49
+ declare const pwsh: ShellCompletion;
50
+ //#endregion
51
+ export { ShellCompletion, bash, fish, pwsh, zsh };