@optique/core 1.0.15-dev.2813 → 1.0.15-dev.2822

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.
@@ -1174,62 +1174,189 @@ const nu = {
1174
1174
  const safeName = programName.replace(/[^a-zA-Z0-9]+/g, "-");
1175
1175
  const functionName = `nu-complete-${safeName}`;
1176
1176
  return `
1177
- # Helper to split args respecting quotes and whitespace
1177
+ # Helper to split a command line into the arguments Nushell would pass to
1178
+ # an external command. Words are found the way Nushell's lexer finds them
1179
+ # (quotes and raw strings may contain whitespace), then each word is decoded
1180
+ # like an external argument. A final empty argument is added when the line
1181
+ # ends in unquoted whitespace, for the word about to be completed.
1178
1182
  def args-split []: string -> list<string> {
1179
- let STATE_NORMAL = 0
1180
- let STATE_IN_SINGLE_QUOTE = 1
1181
- let STATE_IN_DOUBLE_QUOTE = 2
1182
- let STATE_ESCAPE = 3
1183
1183
  let WHITESPACES = [" " "\\t" "\\n" "\\r"]
1184
+ let QUOTES = ["'" '"' "\`"]
1184
1185
 
1185
- mut state = $STATE_NORMAL
1186
- mut current_token = ""
1187
- mut result: list<string> = []
1188
- mut prev_state = $STATE_NORMAL
1186
+ # Decodes one word. An unterminated word is the one still being typed,
1187
+ # so its open quote is taken as if it were closed.
1188
+ let decode = {|word: string, unterminated: bool|
1189
+ let hashes = if ($word | str starts-with "r#") {
1190
+ $word | str replace -r '(?s)^r(#+).*$' '$1'
1191
+ } else {
1192
+ ""
1193
+ }
1194
+ if ($hashes | is-not-empty) {
1195
+ let prefix = "r" + $hashes + "'"
1196
+ let suffix = "'" + $hashes
1197
+ let size = ($hashes | str length)
1198
+ if ($word | str starts-with $prefix) and ($word | str ends-with $suffix) and (($word | str length) >= ($size * 2 + 3)) {
1199
+ $word | str replace $prefix "" | str replace -r ($suffix + '$') ""
1200
+ } else if $unterminated and ($word | str starts-with $prefix) {
1201
+ $word | str replace $prefix ""
1202
+ } else {
1203
+ $word
1204
+ }
1205
+ } else {
1206
+ mut result = ""
1207
+ mut quote = ""
1208
+ mut quoted = ""
1209
+ mut escaped = false
1210
+ for char in ($word | split chars) {
1211
+ if $quote == "" {
1212
+ if ($char in $QUOTES) {
1213
+ $quote = $char
1214
+ $quoted = ""
1215
+ } else {
1216
+ $result = $result + $char
1217
+ }
1218
+ } else if $escaped {
1219
+ $quoted = $quoted + $char
1220
+ $escaped = false
1221
+ } else if $char == $quote {
1222
+ let body = $quoted
1223
+ $result = $result + (if $quote == '"' {
1224
+ try { '"' + $body + '"' | from nuon } catch { $body }
1225
+ } else {
1226
+ $body
1227
+ })
1228
+ $quote = ""
1229
+ } else {
1230
+ $quoted = $quoted + $char
1231
+ $escaped = ($quote == '"' and $char == '\\')
1232
+ }
1233
+ }
1234
+ if $quote != "" {
1235
+ let body = $quoted
1236
+ $result = $result + (if not $unterminated {
1237
+ $quote + $body
1238
+ } else if $quote == '"' {
1239
+ try { '"' + $body + '"' | from nuon } catch { $body }
1240
+ } else {
1241
+ $body
1242
+ })
1243
+ }
1244
+ $result
1245
+ }
1246
+ }
1247
+
1248
+ mut words: list<string> = []
1249
+ mut word = ""
1250
+ mut in_word = false
1251
+ mut quote = ""
1252
+ mut escaped = false
1253
+ mut raw_hashes = ""
1254
+ mut raw_pending = ""
1255
+ mut prev = ""
1256
+ mut trailing_space = false
1189
1257
 
1190
1258
  for char in ($in | split chars) {
1191
- if $state == $STATE_ESCAPE {
1192
- $current_token = $current_token + $char
1193
- $state = $prev_state
1194
- } else if $char == '\\\\' {
1195
- $prev_state = $state
1196
- $state = $STATE_ESCAPE
1197
- } else if $state == $STATE_NORMAL {
1198
- if $char == "'" {
1199
- $state = $STATE_IN_SINGLE_QUOTE
1200
- } else if $char == '"' {
1201
- $state = $STATE_IN_DOUBLE_QUOTE
1202
- } else if ($char in $WHITESPACES) {
1203
- if ($current_token | is-not-empty) {
1204
- $result = $result | append $current_token
1205
- $current_token = ""
1206
- }
1259
+ $trailing_space = false
1260
+ if $quote == "#" and $char != "#" and $char != "'" {
1261
+ # Not a raw string after all; lex this character normally
1262
+ $quote = ""
1263
+ }
1264
+ if $quote == "r" {
1265
+ # Inside a raw string, which ends at ' followed by the same number of #
1266
+ if ($raw_pending | is-not-empty) and $char == "#" {
1267
+ $raw_pending = $raw_pending + $char
1268
+ } else if $char == "'" {
1269
+ $raw_pending = $char
1207
1270
  } else {
1208
- $current_token = $current_token + $char
1271
+ $raw_pending = ""
1272
+ }
1273
+ if $raw_pending == ("'" + $raw_hashes) {
1274
+ $quote = ""
1275
+ $raw_pending = ""
1209
1276
  }
1210
- } else if $state == $STATE_IN_SINGLE_QUOTE {
1211
- if $char == "'" {
1212
- $state = $STATE_NORMAL
1277
+ $word = $word + $char
1278
+ } else if $quote == "#" {
1279
+ # Between r# and the opening ' of a raw string
1280
+ $word = $word + $char
1281
+ if $char == "#" {
1282
+ $raw_hashes = $raw_hashes + $char
1213
1283
  } else {
1214
- $current_token = $current_token + $char
1284
+ $quote = "r"
1215
1285
  }
1216
- } else if $state == $STATE_IN_DOUBLE_QUOTE {
1217
- if $char == '"' {
1218
- $state = $STATE_NORMAL
1286
+ } else if $quote != "" {
1287
+ $word = $word + $char
1288
+ if $escaped {
1289
+ $escaped = false
1290
+ } else if $char == $quote {
1291
+ $quote = ""
1219
1292
  } else {
1220
- $current_token = $current_token + $char
1293
+ $escaped = ($quote == '"' and $char == '\\')
1294
+ }
1295
+ } else if ($char in $WHITESPACES) {
1296
+ if $in_word {
1297
+ $words = $words | append (do $decode $word false)
1298
+ $word = ""
1299
+ $in_word = false
1300
+ }
1301
+ $trailing_space = true
1302
+ } else {
1303
+ if ($char in $QUOTES) {
1304
+ $quote = $char
1305
+ } else if $char == "#" and $prev == "r" {
1306
+ $quote = "#"
1307
+ $raw_hashes = "#"
1221
1308
  }
1309
+ $word = $word + $char
1310
+ $in_word = true
1222
1311
  }
1312
+ $prev = $char
1223
1313
  }
1224
- if ($current_token | is-not-empty) {
1225
- $result = $result | append $current_token
1314
+ if $in_word {
1315
+ $words = $words | append (do $decode $word ($quote != ""))
1316
+ } else if $trailing_space {
1317
+ $words = $words | append ""
1318
+ }
1319
+ $words
1320
+ }
1321
+
1322
+ # Helper to tell whether Nushell would not read a value back verbatim as a
1323
+ # single argument. A leading ! is included because the line editor expands
1324
+ # history (e.g., !!) there. With --allow-expansion, a leading tilde and n-dot
1325
+ # path segments (e.g., ...) are allowed to expand.
1326
+ def needs-quote [--allow-expansion]: string -> bool {
1327
+ let value = $in
1328
+ let special = ["'" '"' '\`' "#" "(" ")" "[" "]" "{" "}" "|" ";" "&" "$" "*" "?" "<" ">"]
1329
+ (
1330
+ ($value =~ '\\s')
1331
+ or ($special | any {|c| $value | str contains $c })
1332
+ or ($value =~ '^(\\+\\+|[-+*/])?=$')
1333
+ or ($value | str starts-with '!')
1334
+ or ((not $allow_expansion) and (
1335
+ ($value | str starts-with '~')
1336
+ or ($value | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
1337
+ ))
1338
+ )
1339
+ }
1340
+
1341
+ # Helper to quote a value so that Nushell reads it back verbatim as a
1342
+ # single argument. The line editor expands history at a ! that follows
1343
+ # whitespace even inside quotes, so such a ! is put between two quoted parts,
1344
+ # right after a quote, where it is left alone.
1345
+ def quote-arg []: string -> string {
1346
+ let value = $in
1347
+ if ($value | is-empty) or not ($value | needs-quote) {
1348
+ $value
1349
+ } else if ($value | str contains "'") {
1350
+ '"' + ($value | str replace -a '\\' '\\\\' | str replace -a '"' '\\"' | str replace -ar '(\\s)!' '\${1}"!"') + '"'
1351
+ } else {
1352
+ "'" + ($value | str replace -ar '(\\s)!' "\${1}'!'") + "'"
1226
1353
  }
1227
- $result
1228
1354
  }
1229
1355
 
1230
1356
  # Completion command that calls back to the CLI
1231
1357
  export def "${functionName}" [context: string] {
1232
- # Split context into tokens, handling quotes properly
1358
+ # Split context into the arguments Nushell would pass, including an empty
1359
+ # one for a new word when the context ends in unquoted whitespace
1233
1360
  let tokens = $context | args-split
1234
1361
 
1235
1362
  # Remove the command name (first token) to get arguments
@@ -1239,18 +1366,11 @@ export def "${functionName}" [context: string] {
1239
1366
  []
1240
1367
  }
1241
1368
 
1242
- # If context ends with whitespace, add empty string to represent the new argument
1243
- let args_with_empty = if ($context | str ends-with ' ') {
1244
- $args | append ''
1245
- } else {
1246
- $args
1247
- }
1248
-
1249
1369
  # Ensure at least one argument (empty string) to get completions not script
1250
- let final_args = if ($args_with_empty | is-empty) {
1370
+ let final_args = if ($args | is-empty) {
1251
1371
  ['']
1252
1372
  } else {
1253
- $args_with_empty
1373
+ $args
1254
1374
  }
1255
1375
 
1256
1376
  let output = try {
@@ -1432,19 +1552,45 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1432
1552
  if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
1433
1553
  }
1434
1554
 
1435
- # Format file completions
1555
+ # Format file completions, quoting names that Nushell would not read
1556
+ # back as a single argument
1436
1557
  $filtered | each {|item|
1437
- let name = if $item.type == dir {
1438
- $dir_prefix + ($item.name | path basename) + "/"
1558
+ let base = if $item.type == dir {
1559
+ ($item.name | path basename) + "/"
1439
1560
  } else {
1440
- $dir_prefix + ($item.name | path basename)
1561
+ $item.name | path basename
1562
+ }
1563
+ let name = $dir_prefix + $base
1564
+ # Nushell expands a leading tilde even in a file name, but not after ./
1565
+ let spelled = if ($dir_prefix | is-empty) and ($base | str starts-with '~') {
1566
+ './' + $base
1567
+ } else {
1568
+ $name
1569
+ }
1570
+ # A tilde or n-dot prefix typed by the user is meant to be expanded
1571
+ # by Nushell, which quoting would prevent, so expand such a prefix
1572
+ # here when the value has to be quoted
1573
+ let literal_ndots = ($base | str replace -r '[/\\\\]$' '') =~ '^\\.{3,}$'
1574
+ let expandable = ($dir_prefix | str starts-with '~') or ($dir_prefix | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
1575
+ let value = if not ($literal_ndots or ($spelled | needs-quote --allow-expansion)) {
1576
+ $spelled
1577
+ } else if $expandable {
1578
+ let expanded_prefix = ($dir_prefix | path expand --no-symlink)
1579
+ let expanded_prefix = if ($expanded_prefix | str ends-with "/") { $expanded_prefix } else { $expanded_prefix + "/" }
1580
+ ($expanded_prefix + $base) | quote-arg
1581
+ } else {
1582
+ $spelled | quote-arg
1583
+ }
1584
+ if $value == $name {
1585
+ { value: $value }
1586
+ } else {
1587
+ { value: $value, display_override: $name }
1441
1588
  }
1442
- { value: $name }
1443
1589
  }
1444
1590
  } else {
1445
1591
  # Regular literal completion - split by tab
1446
1592
  let parts = ($line | split row "\t")
1447
- if ($parts | length) >= 2 {
1593
+ let record = if ($parts | length) >= 2 {
1448
1594
  # value\\tdescription format
1449
1595
  { value: ($parts | get 0), description: ($parts | get 1) }
1450
1596
  } else if ($parts | length) == 1 and ($parts | get 0 | is-not-empty) {
@@ -1453,6 +1599,18 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1453
1599
  } else {
1454
1600
  null
1455
1601
  }
1602
+ # Quote values that Nushell would not read back as a single argument,
1603
+ # while the menu keeps showing them as they are
1604
+ if $record == null {
1605
+ null
1606
+ } else {
1607
+ let value = ($record.value | quote-arg)
1608
+ if $value == $record.value {
1609
+ $record
1610
+ } else {
1611
+ $record | upsert value $value | insert display_override $record.value
1612
+ }
1613
+ }
1456
1614
  }
1457
1615
  } | flatten | compact
1458
1616
  }
@@ -1174,62 +1174,189 @@ const nu = {
1174
1174
  const safeName = programName.replace(/[^a-zA-Z0-9]+/g, "-");
1175
1175
  const functionName = `nu-complete-${safeName}`;
1176
1176
  return `
1177
- # Helper to split args respecting quotes and whitespace
1177
+ # Helper to split a command line into the arguments Nushell would pass to
1178
+ # an external command. Words are found the way Nushell's lexer finds them
1179
+ # (quotes and raw strings may contain whitespace), then each word is decoded
1180
+ # like an external argument. A final empty argument is added when the line
1181
+ # ends in unquoted whitespace, for the word about to be completed.
1178
1182
  def args-split []: string -> list<string> {
1179
- let STATE_NORMAL = 0
1180
- let STATE_IN_SINGLE_QUOTE = 1
1181
- let STATE_IN_DOUBLE_QUOTE = 2
1182
- let STATE_ESCAPE = 3
1183
1183
  let WHITESPACES = [" " "\\t" "\\n" "\\r"]
1184
+ let QUOTES = ["'" '"' "\`"]
1184
1185
 
1185
- mut state = $STATE_NORMAL
1186
- mut current_token = ""
1187
- mut result: list<string> = []
1188
- mut prev_state = $STATE_NORMAL
1186
+ # Decodes one word. An unterminated word is the one still being typed,
1187
+ # so its open quote is taken as if it were closed.
1188
+ let decode = {|word: string, unterminated: bool|
1189
+ let hashes = if ($word | str starts-with "r#") {
1190
+ $word | str replace -r '(?s)^r(#+).*$' '$1'
1191
+ } else {
1192
+ ""
1193
+ }
1194
+ if ($hashes | is-not-empty) {
1195
+ let prefix = "r" + $hashes + "'"
1196
+ let suffix = "'" + $hashes
1197
+ let size = ($hashes | str length)
1198
+ if ($word | str starts-with $prefix) and ($word | str ends-with $suffix) and (($word | str length) >= ($size * 2 + 3)) {
1199
+ $word | str replace $prefix "" | str replace -r ($suffix + '$') ""
1200
+ } else if $unterminated and ($word | str starts-with $prefix) {
1201
+ $word | str replace $prefix ""
1202
+ } else {
1203
+ $word
1204
+ }
1205
+ } else {
1206
+ mut result = ""
1207
+ mut quote = ""
1208
+ mut quoted = ""
1209
+ mut escaped = false
1210
+ for char in ($word | split chars) {
1211
+ if $quote == "" {
1212
+ if ($char in $QUOTES) {
1213
+ $quote = $char
1214
+ $quoted = ""
1215
+ } else {
1216
+ $result = $result + $char
1217
+ }
1218
+ } else if $escaped {
1219
+ $quoted = $quoted + $char
1220
+ $escaped = false
1221
+ } else if $char == $quote {
1222
+ let body = $quoted
1223
+ $result = $result + (if $quote == '"' {
1224
+ try { '"' + $body + '"' | from nuon } catch { $body }
1225
+ } else {
1226
+ $body
1227
+ })
1228
+ $quote = ""
1229
+ } else {
1230
+ $quoted = $quoted + $char
1231
+ $escaped = ($quote == '"' and $char == '\\')
1232
+ }
1233
+ }
1234
+ if $quote != "" {
1235
+ let body = $quoted
1236
+ $result = $result + (if not $unterminated {
1237
+ $quote + $body
1238
+ } else if $quote == '"' {
1239
+ try { '"' + $body + '"' | from nuon } catch { $body }
1240
+ } else {
1241
+ $body
1242
+ })
1243
+ }
1244
+ $result
1245
+ }
1246
+ }
1247
+
1248
+ mut words: list<string> = []
1249
+ mut word = ""
1250
+ mut in_word = false
1251
+ mut quote = ""
1252
+ mut escaped = false
1253
+ mut raw_hashes = ""
1254
+ mut raw_pending = ""
1255
+ mut prev = ""
1256
+ mut trailing_space = false
1189
1257
 
1190
1258
  for char in ($in | split chars) {
1191
- if $state == $STATE_ESCAPE {
1192
- $current_token = $current_token + $char
1193
- $state = $prev_state
1194
- } else if $char == '\\\\' {
1195
- $prev_state = $state
1196
- $state = $STATE_ESCAPE
1197
- } else if $state == $STATE_NORMAL {
1198
- if $char == "'" {
1199
- $state = $STATE_IN_SINGLE_QUOTE
1200
- } else if $char == '"' {
1201
- $state = $STATE_IN_DOUBLE_QUOTE
1202
- } else if ($char in $WHITESPACES) {
1203
- if ($current_token | is-not-empty) {
1204
- $result = $result | append $current_token
1205
- $current_token = ""
1206
- }
1259
+ $trailing_space = false
1260
+ if $quote == "#" and $char != "#" and $char != "'" {
1261
+ # Not a raw string after all; lex this character normally
1262
+ $quote = ""
1263
+ }
1264
+ if $quote == "r" {
1265
+ # Inside a raw string, which ends at ' followed by the same number of #
1266
+ if ($raw_pending | is-not-empty) and $char == "#" {
1267
+ $raw_pending = $raw_pending + $char
1268
+ } else if $char == "'" {
1269
+ $raw_pending = $char
1207
1270
  } else {
1208
- $current_token = $current_token + $char
1271
+ $raw_pending = ""
1272
+ }
1273
+ if $raw_pending == ("'" + $raw_hashes) {
1274
+ $quote = ""
1275
+ $raw_pending = ""
1209
1276
  }
1210
- } else if $state == $STATE_IN_SINGLE_QUOTE {
1211
- if $char == "'" {
1212
- $state = $STATE_NORMAL
1277
+ $word = $word + $char
1278
+ } else if $quote == "#" {
1279
+ # Between r# and the opening ' of a raw string
1280
+ $word = $word + $char
1281
+ if $char == "#" {
1282
+ $raw_hashes = $raw_hashes + $char
1213
1283
  } else {
1214
- $current_token = $current_token + $char
1284
+ $quote = "r"
1215
1285
  }
1216
- } else if $state == $STATE_IN_DOUBLE_QUOTE {
1217
- if $char == '"' {
1218
- $state = $STATE_NORMAL
1286
+ } else if $quote != "" {
1287
+ $word = $word + $char
1288
+ if $escaped {
1289
+ $escaped = false
1290
+ } else if $char == $quote {
1291
+ $quote = ""
1219
1292
  } else {
1220
- $current_token = $current_token + $char
1293
+ $escaped = ($quote == '"' and $char == '\\')
1294
+ }
1295
+ } else if ($char in $WHITESPACES) {
1296
+ if $in_word {
1297
+ $words = $words | append (do $decode $word false)
1298
+ $word = ""
1299
+ $in_word = false
1300
+ }
1301
+ $trailing_space = true
1302
+ } else {
1303
+ if ($char in $QUOTES) {
1304
+ $quote = $char
1305
+ } else if $char == "#" and $prev == "r" {
1306
+ $quote = "#"
1307
+ $raw_hashes = "#"
1221
1308
  }
1309
+ $word = $word + $char
1310
+ $in_word = true
1222
1311
  }
1312
+ $prev = $char
1223
1313
  }
1224
- if ($current_token | is-not-empty) {
1225
- $result = $result | append $current_token
1314
+ if $in_word {
1315
+ $words = $words | append (do $decode $word ($quote != ""))
1316
+ } else if $trailing_space {
1317
+ $words = $words | append ""
1318
+ }
1319
+ $words
1320
+ }
1321
+
1322
+ # Helper to tell whether Nushell would not read a value back verbatim as a
1323
+ # single argument. A leading ! is included because the line editor expands
1324
+ # history (e.g., !!) there. With --allow-expansion, a leading tilde and n-dot
1325
+ # path segments (e.g., ...) are allowed to expand.
1326
+ def needs-quote [--allow-expansion]: string -> bool {
1327
+ let value = $in
1328
+ let special = ["'" '"' '\`' "#" "(" ")" "[" "]" "{" "}" "|" ";" "&" "$" "*" "?" "<" ">"]
1329
+ (
1330
+ ($value =~ '\\s')
1331
+ or ($special | any {|c| $value | str contains $c })
1332
+ or ($value =~ '^(\\+\\+|[-+*/])?=$')
1333
+ or ($value | str starts-with '!')
1334
+ or ((not $allow_expansion) and (
1335
+ ($value | str starts-with '~')
1336
+ or ($value | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
1337
+ ))
1338
+ )
1339
+ }
1340
+
1341
+ # Helper to quote a value so that Nushell reads it back verbatim as a
1342
+ # single argument. The line editor expands history at a ! that follows
1343
+ # whitespace even inside quotes, so such a ! is put between two quoted parts,
1344
+ # right after a quote, where it is left alone.
1345
+ def quote-arg []: string -> string {
1346
+ let value = $in
1347
+ if ($value | is-empty) or not ($value | needs-quote) {
1348
+ $value
1349
+ } else if ($value | str contains "'") {
1350
+ '"' + ($value | str replace -a '\\' '\\\\' | str replace -a '"' '\\"' | str replace -ar '(\\s)!' '\${1}"!"') + '"'
1351
+ } else {
1352
+ "'" + ($value | str replace -ar '(\\s)!' "\${1}'!'") + "'"
1226
1353
  }
1227
- $result
1228
1354
  }
1229
1355
 
1230
1356
  # Completion command that calls back to the CLI
1231
1357
  export def "${functionName}" [context: string] {
1232
- # Split context into tokens, handling quotes properly
1358
+ # Split context into the arguments Nushell would pass, including an empty
1359
+ # one for a new word when the context ends in unquoted whitespace
1233
1360
  let tokens = $context | args-split
1234
1361
 
1235
1362
  # Remove the command name (first token) to get arguments
@@ -1239,18 +1366,11 @@ export def "${functionName}" [context: string] {
1239
1366
  []
1240
1367
  }
1241
1368
 
1242
- # If context ends with whitespace, add empty string to represent the new argument
1243
- let args_with_empty = if ($context | str ends-with ' ') {
1244
- $args | append ''
1245
- } else {
1246
- $args
1247
- }
1248
-
1249
1369
  # Ensure at least one argument (empty string) to get completions not script
1250
- let final_args = if ($args_with_empty | is-empty) {
1370
+ let final_args = if ($args | is-empty) {
1251
1371
  ['']
1252
1372
  } else {
1253
- $args_with_empty
1373
+ $args
1254
1374
  }
1255
1375
 
1256
1376
  let output = try {
@@ -1432,19 +1552,45 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1432
1552
  if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
1433
1553
  }
1434
1554
 
1435
- # Format file completions
1555
+ # Format file completions, quoting names that Nushell would not read
1556
+ # back as a single argument
1436
1557
  $filtered | each {|item|
1437
- let name = if $item.type == dir {
1438
- $dir_prefix + ($item.name | path basename) + "/"
1558
+ let base = if $item.type == dir {
1559
+ ($item.name | path basename) + "/"
1439
1560
  } else {
1440
- $dir_prefix + ($item.name | path basename)
1561
+ $item.name | path basename
1562
+ }
1563
+ let name = $dir_prefix + $base
1564
+ # Nushell expands a leading tilde even in a file name, but not after ./
1565
+ let spelled = if ($dir_prefix | is-empty) and ($base | str starts-with '~') {
1566
+ './' + $base
1567
+ } else {
1568
+ $name
1569
+ }
1570
+ # A tilde or n-dot prefix typed by the user is meant to be expanded
1571
+ # by Nushell, which quoting would prevent, so expand such a prefix
1572
+ # here when the value has to be quoted
1573
+ let literal_ndots = ($base | str replace -r '[/\\\\]$' '') =~ '^\\.{3,}$'
1574
+ let expandable = ($dir_prefix | str starts-with '~') or ($dir_prefix | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
1575
+ let value = if not ($literal_ndots or ($spelled | needs-quote --allow-expansion)) {
1576
+ $spelled
1577
+ } else if $expandable {
1578
+ let expanded_prefix = ($dir_prefix | path expand --no-symlink)
1579
+ let expanded_prefix = if ($expanded_prefix | str ends-with "/") { $expanded_prefix } else { $expanded_prefix + "/" }
1580
+ ($expanded_prefix + $base) | quote-arg
1581
+ } else {
1582
+ $spelled | quote-arg
1583
+ }
1584
+ if $value == $name {
1585
+ { value: $value }
1586
+ } else {
1587
+ { value: $value, display_override: $name }
1441
1588
  }
1442
- { value: $name }
1443
1589
  }
1444
1590
  } else {
1445
1591
  # Regular literal completion - split by tab
1446
1592
  let parts = ($line | split row "\t")
1447
- if ($parts | length) >= 2 {
1593
+ let record = if ($parts | length) >= 2 {
1448
1594
  # value\\tdescription format
1449
1595
  { value: ($parts | get 0), description: ($parts | get 1) }
1450
1596
  } else if ($parts | length) == 1 and ($parts | get 0 | is-not-empty) {
@@ -1453,6 +1599,18 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1453
1599
  } else {
1454
1600
  null
1455
1601
  }
1602
+ # Quote values that Nushell would not read back as a single argument,
1603
+ # while the menu keeps showing them as they are
1604
+ if $record == null {
1605
+ null
1606
+ } else {
1607
+ let value = ($record.value | quote-arg)
1608
+ if $value == $record.value {
1609
+ $record
1610
+ } else {
1611
+ $record | upsert value $value | insert display_override $record.value
1612
+ }
1613
+ }
1456
1614
  }
1457
1615
  } | flatten | compact
1458
1616
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.15-dev.2813",
3
+ "version": "1.0.15-dev.2822",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",