try-cli 1.9.2 → 1.10.0

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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -4
  3. data/VERSION +1 -1
  4. data/lib/tui.rb +2 -2
  5. data/try.rb +213 -36
  6. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00a24afe865c51ab7042ceb3800d665c9446524c48aca062463fad9aeb54a894
4
- data.tar.gz: 3433f233d6191fdd578d37e09ac2f229ee7fe030a13c0ec5ab4a1bce60e5821e
3
+ metadata.gz: 2886940b1f4b531eac99e082e57083e26e0aedc60f073355868870fc364118c7
4
+ data.tar.gz: 64712394de4b5d48535ce8dc31abb2f50da33e446aed3d01ec465640191c6b0e
5
5
  SHA512:
6
- metadata.gz: 89ee4cecc2f8f7d05ba6ac56037db149b43b52efb111615753d975fea0a835e04adb9905c0864ccc3242a3f080510365d1a25b89fb077c04aa958a767a9b91b5
7
- data.tar.gz: 1adc18aa91717d0984dfc468c7844ffe95357d50a1314bea7c2143b38666f7d7531cd933a3d8533e2b22a4f90359ee367580852c08229dfbb99a9dea629148ba
6
+ metadata.gz: a13726660cb156fde7e39a351b439d3a64870fcc637a4fe4189c54a0b3a2ad417f3685ec9a010ed65b7b1f254ee26b46e6b20f1c25ff8ca1caf5ae0d1b1852f1
7
+ data.tar.gz: 448bb7c503410c82052e93ce8dc6b22922be70c80dd1a8c0a9f8c7e84e7f50bc0efa4d435c8c8bec4c0578cb8969185cc624fcd3169e9421eead312aa84f4223
data/README.md CHANGED
@@ -10,7 +10,7 @@ Ever find yourself with 50 directories named `test`, `test2`, `new-test`, `actua
10
10
 
11
11
  **try** is here for your beautifully chaotic mind.
12
12
 
13
- # What it does
13
+ # What it does
14
14
 
15
15
  ![Fuzzy Search Demo](assets/try-fuzzy-search-demo.gif)
16
16
 
@@ -155,6 +155,10 @@ try clone https://github.com/tobi/try.git my-fork
155
155
  # Shorthand syntax (no need to type 'clone')
156
156
  try https://github.com/tobi/try.git
157
157
  # Creates: 2025-08-27-tobi-try
158
+
159
+ # Paste a GitHub pull request URL to clone and check out that PR
160
+ try https://github.com/tobi/try/pull/124
161
+ # Creates: 2025-08-27-tobi-try
158
162
  ```
159
163
 
160
164
  Supported git URI formats:
@@ -162,8 +166,14 @@ Supported git URI formats:
162
166
  - `git@github.com:user/repo.git` (SSH GitHub)
163
167
  - `https://gitlab.com/user/repo.git` (GitLab)
164
168
  - `git@host.com:user/repo.git` (SSH other hosts)
169
+ - `ssh://git@host.com:port/user/repo.git` (SSH other hosts with custom port)
170
+ - `user@host:path/to/repo.git` (nested SCP-style SSH URLs)
171
+ - `https://github.com/user/repo/pull/123` (GitHub pull requests)
165
172
 
166
- The `.git` suffix is automatically removed from URLs when generating directory names.
173
+ A GitHub pull request URL clones the main repository, fetches the PR ref, and
174
+ checks it out in detached HEAD state. The directory name is based on the main
175
+ repository URL, not the `/pull/<number>` suffix. The `.git` suffix is
176
+ automatically removed from URLs when generating directory names.
167
177
 
168
178
  ### Keyboard Shortcuts
169
179
 
@@ -199,9 +209,9 @@ nix run github:tobi/try init ~/my-tries
199
209
  ```nix
200
210
  {
201
211
  inputs.try.url = "github:tobi/try";
202
-
212
+
203
213
  imports = [ inputs.try.homeManagerModules.default ];
204
-
214
+
205
215
  programs.try = {
206
216
  enable = true;
207
217
  path = "~/experiments"; # optional, defaults to ~/src/tries
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.9.2
1
+ 1.10.0
data/lib/tui.rb CHANGED
@@ -90,14 +90,14 @@ module Tui
90
90
  module Palette
91
91
  HEADER = ANSI.sgr(1, "38;5;114")
92
92
  ACCENT = ANSI.sgr(1, "38;5;214")
93
- HIGHLIGHT = "\e[1;33m" # Bold yellow (matches C version)
93
+ HIGHLIGHT = "\e[1m" # Bold only, no fixed color: readable on any theme
94
94
  MUTED = ANSI.fg(245)
95
95
  MATCH = ANSI.sgr(1, "38;5;226")
96
96
  INPUT_HINT = ANSI.fg(244)
97
97
  INPUT_CURSOR_ON = "\e[7m"
98
98
  INPUT_CURSOR_OFF = "\e[27m"
99
99
 
100
- SELECTED_BG = ANSI.bg(238)
100
+ SELECTED_BG = "\e[7m" # reverse video: adapts to terminal's own colors, not just dark themes
101
101
  DANGER_BG = ANSI.bg(52)
102
102
  end
103
103
 
data/try.rb CHANGED
@@ -77,7 +77,7 @@ class TrySelector
77
77
  STDERR.print("#{Tui::ANSI::ALT_SCREEN_ON}#{Tui::ANSI.set_title("try")}#{Tui::ANSI::CURSOR_BLINK}")
78
78
  end
79
79
 
80
- @old_winch_handler = Signal.trap('WINCH') { @needs_redraw = true }
80
+ @old_winch_handler = Signal.trap('WINCH') { @needs_redraw = true } if Signal.list.key?('WINCH')
81
81
  end
82
82
 
83
83
  def restore_terminal
@@ -955,7 +955,7 @@ end
955
955
  # Main execution with OptionParser subcommands
956
956
  if __FILE__ == $0
957
957
 
958
- VERSION = "1.9.2"
958
+ VERSION = "1.10.0"
959
959
 
960
960
  def print_global_help
961
961
  text = <<~HELP
@@ -984,6 +984,7 @@ if __FILE__ == $0
984
984
  try Open interactive selector
985
985
  try project Selector with initial filter
986
986
  try clone https://github.com/user/repo
987
+ try https://github.com/user/repo/pull/123
987
988
  try worktree feature-branch
988
989
 
989
990
  Manual mode (without alias):
@@ -1053,19 +1054,44 @@ if __FILE__ == $0
1053
1054
  # https://gitlab.com/user/repo or other git hosts
1054
1055
  host, user, repo = $1, $2, $3
1055
1056
  return { user: user, repo: repo, host: host }
1056
- elsif uri.match(%r{^git@([^:]+):([^/]+)/([^/]+)})
1057
- # git@host:user/repo
1058
- host, user, repo = $1, $2, $3
1057
+ elsif uri.match(%r{^git@([^:]+):([^/]+)/(.+)})
1058
+ # git@host:user/path/to/repo
1059
+ host, user, path = $1, $2, $3
1060
+ repo = File.basename(path)
1061
+ return { user: user, repo: repo, host: host }
1062
+ elsif uri.match(%r{^ssh://[^@/]+@([^/]+)/([^/]+)/(.+)})
1063
+ # ssh://user@host:port/user/repo
1064
+ host, user, path = $1, $2, $3
1065
+ repo = File.basename(path)
1066
+ return { user: user, repo: repo, host: host }
1067
+ elsif uri.match(%r{^([^@/:]+)@([^:]+):(.+)})
1068
+ # SCP-style SSH: user@host:path/to/repo
1069
+ user, host, path = $1, $2, $3
1070
+ repo = File.basename(path)
1059
1071
  return { user: user, repo: repo, host: host }
1060
1072
  else
1061
1073
  return nil
1062
1074
  end
1063
1075
  end
1064
1076
 
1077
+ def github_pr_details(uri)
1078
+ return nil unless uri
1079
+
1080
+ match = uri.match(%r{\Ahttps?://(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+)/?\z})
1081
+ return nil unless match
1082
+
1083
+ {
1084
+ user: match[1],
1085
+ repo: match[2].sub(/\.git\z/, ''),
1086
+ pr_id: match[3],
1087
+ git_uri: "https://github.com/#{match[1]}/#{match[2].sub(/\.git\z/, '')}.git"
1088
+ }
1089
+ end
1090
+
1065
1091
  def generate_clone_directory_name(git_uri, custom_name = nil)
1066
1092
  return custom_name if custom_name && !custom_name.empty?
1067
1093
 
1068
- parsed = parse_git_uri(git_uri)
1094
+ parsed = github_pr_details(git_uri) || parse_git_uri(git_uri)
1069
1095
  return nil unless parsed
1070
1096
 
1071
1097
  date_prefix = Time.now.strftime("%Y-%m-%d")
@@ -1166,7 +1192,12 @@ if __FILE__ == $0
1166
1192
  exit 1
1167
1193
  end
1168
1194
 
1169
- script_clone(File.join(tries_path, dir_name), git_uri)
1195
+ path = File.join(tries_path, dir_name)
1196
+ if pr = github_pr_details(git_uri)
1197
+ script_clone_pr(path, pr[:git_uri], pr[:pr_id])
1198
+ else
1199
+ script_clone(path, git_uri)
1200
+ end
1170
1201
  end
1171
1202
 
1172
1203
  def cmd_init!(args, tries_path)
@@ -1176,37 +1207,142 @@ if __FILE__ == $0
1176
1207
  File.expand_path(args.shift)
1177
1208
  end
1178
1209
 
1179
- # Priority: explicit init argument > $TRY_PATH (runtime) > default
1180
1210
  default_path = tries_path || File.expand_path("~/src/tries")
1181
- path_arg = explicit_path ? " --path '#{explicit_path}'" : " --path \"${TRY_PATH:-#{default_path}}\""
1182
- bash_or_zsh_script = <<~SHELL
1183
- try() {
1184
- local out
1185
- out=$(/usr/bin/env ruby '#{script_path}' exec#{path_arg} "$@" 2>/dev/tty)
1186
- if [ $? -eq 0 ]; then
1187
- eval "$out"
1188
- else
1189
- echo "$out"
1190
- fi
1191
- }
1192
- SHELL
1193
-
1194
- fish_path_arg = explicit_path ? " --path '#{explicit_path}'" : " --path (if set -q TRY_PATH; echo \"$TRY_PATH\"; else; echo '#{default_path}'; end)"
1195
- fish_script = <<~SHELL
1196
- function try
1197
- set -l out (/usr/bin/env ruby '#{script_path}' exec#{fish_path_arg} $argv 2>/dev/tty | string collect)
1198
- if test $pipestatus[1] -eq 0
1199
- eval $out
1200
- else
1201
- echo $out
1202
- end
1203
- end
1204
- SHELL
1211
+ shell = fish? ? 'fish' : 'bash'
1212
+ puts init_snippet(shell, script_path, explicit_path, default_path)
1213
+ exit 0
1214
+ end
1215
+
1216
+ def cmd_install!(args, tries_path)
1217
+ script_path = File.expand_path($0)
1218
+
1219
+ explicit_path = if args[0] && args[0].start_with?('/')
1220
+ File.expand_path(args.shift)
1221
+ end
1222
+
1223
+ default_path = tries_path || File.expand_path("~/src/tries")
1224
+
1225
+ shell = detect_shell
1226
+ rc_file = shell_rc_file(shell)
1227
+ snippet = init_snippet(shell, script_path, explicit_path, default_path)
1228
+
1229
+ unless rc_file
1230
+ STDERR.puts "Error: could not determine shell config file"
1231
+ STDERR.puts "Your shell was detected as: #{shell || 'unknown'}"
1232
+ STDERR.puts "Run 'try init' and manually add the output to your shell config."
1233
+ exit 1
1234
+ end
1235
+
1236
+ rc_path = File.expand_path(rc_file)
1237
+
1238
+ # Check if already installed
1239
+ if File.exist?(rc_path) && File.read(rc_path).include?("# try shell integration")
1240
+ STDERR.puts "try is already installed in #{rc_path}"
1241
+ STDERR.puts "To reinstall, remove the '# try shell integration' block first."
1242
+ exit 0
1243
+ end
1244
+
1245
+ block = "\n# try shell integration\n#{snippet}"
1205
1246
 
1206
- puts fish? ? fish_script : bash_or_zsh_script
1247
+ if File.exist?(rc_path) && !File.writable?(rc_path)
1248
+ STDERR.puts "Warning: #{rc_path} is read-only, skipping."
1249
+ STDERR.puts "Run 'try init' and manually add the output to your shell config."
1250
+ exit 1
1251
+ end
1252
+
1253
+ FileUtils.mkdir_p(File.dirname(rc_path))
1254
+ File.open(rc_path, 'a') { |f| f.write(block) }
1255
+ STDERR.puts "Added try shell integration to #{rc_path}"
1256
+ STDERR.puts "Restart your shell or run: source #{rc_path}" unless shell == 'pwsh'
1257
+ STDERR.puts "Restart your shell or run: . $PROFILE" if shell == 'pwsh'
1207
1258
  exit 0
1208
1259
  end
1209
1260
 
1261
+ def detect_shell
1262
+ # Check SHELL env (Unix), then parent process, then PSModulePath for PowerShell
1263
+ shell_env = ENV["SHELL"].to_s
1264
+ return 'fish' if shell_env.include?('fish')
1265
+ return 'zsh' if shell_env.include?('zsh')
1266
+ return 'bash' if shell_env.include?('bash')
1267
+
1268
+ # PowerShell detection: PSModulePath is set in pwsh sessions
1269
+ return 'pwsh' if ENV["PSModulePath"] && !ENV["PSModulePath"].empty?
1270
+
1271
+ # Fallback: check parent process name
1272
+ parent = (`ps c -p #{Process.ppid} -o 'ucomm='`.strip rescue nil)
1273
+ return 'fish' if parent&.include?('fish')
1274
+ return 'zsh' if parent&.include?('zsh')
1275
+ return 'bash' if parent&.include?('bash')
1276
+ return 'pwsh' if parent&.match?(/pwsh|powershell/i)
1277
+
1278
+ nil
1279
+ end
1280
+
1281
+ def shell_rc_file(shell)
1282
+ case shell
1283
+ when 'fish' then '~/.config/fish/config.fish'
1284
+ when 'zsh' then '~/.zshrc'
1285
+ when 'bash'
1286
+ # Prefer .bashrc, fall back to .bash_profile on macOS
1287
+ File.exist?(File.expand_path('~/.bashrc')) ? '~/.bashrc' : '~/.bash_profile'
1288
+ when 'pwsh'
1289
+ # PowerShell profile path from $PROFILE, or the standard location
1290
+ ENV["PROFILE"] || (Gem.win_platform? ?
1291
+ File.join(ENV["USERPROFILE"] || Dir.home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1") :
1292
+ File.join(Dir.home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"))
1293
+ end
1294
+ end
1295
+
1296
+ def init_snippet(shell, script_path, explicit_path, default_path)
1297
+ case shell
1298
+ when 'fish'
1299
+ fish_path_arg = explicit_path ? " --path '#{explicit_path}'" : " --path (if set -q TRY_PATH; echo \"$TRY_PATH\"; else; echo '#{default_path}'; end)"
1300
+ <<~FISH
1301
+ function try
1302
+ set -l out (/usr/bin/env ruby '#{script_path}' exec#{fish_path_arg} $argv 2>/dev/tty | string collect)
1303
+ if test $pipestatus[1] -eq 0
1304
+ eval $out
1305
+ else
1306
+ echo $out
1307
+ end
1308
+ end
1309
+ FISH
1310
+ when 'pwsh'
1311
+ ps_path_expr = if explicit_path
1312
+ "'#{explicit_path}'"
1313
+ else
1314
+ "$(if ($env:TRY_PATH) { $env:TRY_PATH } else { '#{default_path}' })"
1315
+ end
1316
+ <<~PWSH
1317
+ function try {
1318
+ $tryPath = #{ps_path_expr}
1319
+ $tempErr = [System.IO.Path]::GetTempFileName()
1320
+ $out = & ruby '#{script_path}' exec --path $tryPath @args 2>$tempErr
1321
+ if ($LASTEXITCODE -eq 0) {
1322
+ $out | Invoke-Expression
1323
+ } else {
1324
+ Get-Content $tempErr | Write-Host
1325
+ $out | Write-Output
1326
+ }
1327
+ Remove-Item $tempErr -ErrorAction SilentlyContinue
1328
+ }
1329
+ PWSH
1330
+ else # bash, zsh
1331
+ path_arg = explicit_path ? " --path '#{explicit_path}'" : " --path \"${TRY_PATH:-#{default_path}}\""
1332
+ <<~SH
1333
+ try() {
1334
+ local out
1335
+ out=$(/usr/bin/env ruby '#{script_path}' exec#{path_arg} "$@" 2>/dev/tty)
1336
+ if [ $? -eq 0 ]; then
1337
+ eval "$out"
1338
+ else
1339
+ echo "$out"
1340
+ fi
1341
+ }
1342
+ SH
1343
+ end
1344
+ end
1345
+
1210
1346
  def cmd_cd!(args, tries_path, and_type, and_exit, and_keys, and_confirm)
1211
1347
  if args.first == "clone"
1212
1348
  return cmd_clone!(args[1..-1] || [], tries_path)
@@ -1250,7 +1386,11 @@ if __FILE__ == $0
1250
1386
  exit 1
1251
1387
  end
1252
1388
  full_path = File.join(tries_path, dir_name)
1253
- return script_clone(full_path, git_uri)
1389
+ if pr = github_pr_details(git_uri)
1390
+ return script_clone_pr(full_path, pr[:git_uri], pr[:pr_id])
1391
+ else
1392
+ return script_clone(full_path, git_uri)
1393
+ end
1254
1394
  end
1255
1395
 
1256
1396
  # Regular interactive selector
@@ -1303,8 +1443,31 @@ if __FILE__ == $0
1303
1443
  end
1304
1444
  end
1305
1445
 
1446
+ # Emit best-effort terminal manager rename commands. Keep these as the final
1447
+ # commands so a missing or broken optional CLI never prevents the directory
1448
+ # change from completing.
1449
+ def terminal_rename_commands(path)
1450
+ name = File.basename(path).sub(/\A\d{4}-\d{2}-\d{2}-/, '')
1451
+ label = "try: #{name}"
1452
+
1453
+ if ENV['HERDR_ENV'] == '1' && ENV['HERDR_PANE_ID'] && !ENV['HERDR_PANE_ID'].empty?
1454
+ commands = [
1455
+ "command -v herdr >/dev/null 2>&1 && herdr pane report-metadata #{q(ENV['HERDR_PANE_ID'])} --source try --title #{q(label)} >/dev/null 2>&1 || true"
1456
+ ]
1457
+ if ENV['HERDR_PANE_ID'].match?(/:p1\z/) && ENV['HERDR_WORKSPACE_ID'] && !ENV['HERDR_WORKSPACE_ID'].empty?
1458
+ commands << "command -v herdr >/dev/null 2>&1 && herdr workspace rename #{q(ENV['HERDR_WORKSPACE_ID'])} #{q(label)} >/dev/null 2>&1 || true"
1459
+ end
1460
+ commands
1461
+ elsif (ENV['CMUX_SOCKET_PATH'] && !ENV['CMUX_SOCKET_PATH'].empty?) ||
1462
+ (ENV['CMUX_BUNDLE_ID'] && !ENV['CMUX_BUNDLE_ID'].empty?)
1463
+ ["command -v cmux >/dev/null 2>&1 && cmux rename-tab #{q(label)} >/dev/null 2>&1 || true"]
1464
+ else
1465
+ []
1466
+ end
1467
+ end
1468
+
1306
1469
  def script_cd(path)
1307
- ["touch #{q(path)}", "echo #{q(path)}", "cd #{q(path)}"]
1470
+ ["touch #{q(path)}", "echo #{q(path)}", "cd #{q(path)}"] + terminal_rename_commands(path)
1308
1471
  end
1309
1472
 
1310
1473
  def script_mkdir_cd(path)
@@ -1315,6 +1478,17 @@ if __FILE__ == $0
1315
1478
  ["mkdir -p #{q(path)}", "echo #{q("Using git clone to create this trial from #{uri}.")}", "git clone '#{uri}' #{q(path)}"] + script_cd(path)
1316
1479
  end
1317
1480
 
1481
+ def script_clone_pr(path, uri, pr_id)
1482
+ ref = "pull/#{pr_id}/head"
1483
+ [
1484
+ "mkdir -p #{q(path)}",
1485
+ "echo #{q("Using git clone to create this trial from #{uri} PR ##{pr_id}.")}",
1486
+ "git clone #{q(uri)} #{q(path)}",
1487
+ "git -C #{q(path)} fetch origin #{q(ref)}",
1488
+ "git -C #{q(path)} checkout --detach FETCH_HEAD"
1489
+ ] + script_cd(path)
1490
+ end
1491
+
1318
1492
  def script_worktree(path, repo = nil)
1319
1493
  r = repo ? q(repo) : nil
1320
1494
  worktree_cmd = if r
@@ -1358,7 +1532,7 @@ if __FILE__ == $0
1358
1532
  "mv #{q(old_name)} #{q(new_name)}",
1359
1533
  "echo #{q(new_path)}",
1360
1534
  "cd #{q(new_path)}"
1361
- ]
1535
+ ] + terminal_rename_commands(new_path)
1362
1536
  end
1363
1537
 
1364
1538
  # Return a unique directory name under tries_path by appending -2, -3, ... if needed
@@ -1427,6 +1601,9 @@ if __FILE__ == $0
1427
1601
  when 'init'
1428
1602
  cmd_init!(ARGV, tries_path)
1429
1603
  exit 0
1604
+ when 'install'
1605
+ cmd_install!(ARGV, tries_path)
1606
+ exit 0
1430
1607
  when 'exec'
1431
1608
  sub = ARGV.first
1432
1609
  case sub
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: try-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.2
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobi Lutke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-10 00:00:00.000000000 Z
11
+ date: 2026-08-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A CLI tool for managing experimental projects. Creates dated directories
14
14
  for your tries, with fuzzy search and easy navigation.