try-cli 1.9.3 → 1.10.1

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 +3 -2
  5. data/try.rb +108 -24
  6. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07dc2d2cbbb24c4c934bffceead77e583ef973e1c7f7a08361863c340a373783
4
- data.tar.gz: 8c110d86811dbecdd9b7a0778605f76d3564f6f9eec697a05da809a98f39b5dd
3
+ metadata.gz: 6bc9090ddafc7354bd7d4207e5247e9afd3f52df65e43c1600e21f717c3644cb
4
+ data.tar.gz: cc3fbc4c3868d6d504e326702329fa14d377257d9300c468c07f929f5137c176
5
5
  SHA512:
6
- metadata.gz: c93e4020653e00136115a24cd831e409e00b3e1e9ae48013794b794115f37eae3df599af93c01bd4e571ca45933188c460a4e808e4ad6a78eef5b2384c814ff6
7
- data.tar.gz: 5487462b36084f6f4b5b7ebe553724d9ac1bddd42246a5d9bea5a53f2702a1be311cfbae448686c68fe73a8134ab73af1bda5d2403ed0d19f5bc62589f33b09c
6
+ metadata.gz: 21c62da72659e06ca9614d1ee82f8063f80f13b153e53af39102cdb21e083ab00c46d0d97c42a91cbbe8e0142d5030448660fd9594790bb4da753a0f8314de20
7
+ data.tar.gz: 83376a51f4be2be9fe9dcb4c0de857174fd53d1409ec65b1e1f6cc5d9728a4bcff5bfa800fe180e63f799aab1063c207ab8a6ab11196351715454a3f934819ab
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.3
1
+ 1.10.1
data/lib/tui.rb CHANGED
@@ -90,14 +90,15 @@ 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[1;33m" # Bold yellow remains readable on the controlled selected background
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 = ANSI.bg(238) # Preserve the dark selected-row appearance
101
+ SELECTED_FG = ANSI.fg(255) # Keep selected content readable on every theme
101
102
  DANGER_BG = ANSI.bg(52)
102
103
  end
103
104
 
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
@@ -389,15 +389,15 @@ class TrySelector
389
389
 
390
390
  def render_entry_line(screen, entry, is_selected, width)
391
391
  is_marked = @marked_for_deletion.include?(entry[:path])
392
- # Marked items always show red; selection shows via arrow only
392
+ # Marked items keep the danger background; selected rows add a readable foreground.
393
393
  background = if is_marked
394
- Tui::Palette::DANGER_BG
394
+ Tui::Palette::DANGER_BG + (is_selected ? Tui::Palette::SELECTED_FG : "")
395
395
  elsif is_selected
396
- Tui::Palette::SELECTED_BG
396
+ Tui::Palette::SELECTED_BG + Tui::Palette::SELECTED_FG
397
397
  end
398
398
 
399
399
  line = screen.body.add_line(background: background)
400
- line.write << (is_selected ? Tui::Text.highlight("→ ") : " ")
400
+ line.write << (is_selected ? Tui::Text.highlight("→ ") + selected_foreground : " ")
401
401
  icon = if is_marked
402
402
  emoji("🗑️")
403
403
  elsif entry[:is_symlink]
@@ -407,7 +407,7 @@ class TrySelector
407
407
  end
408
408
  line.write << icon << " "
409
409
 
410
- plain_name, rendered_name = formatted_entry_name(entry)
410
+ plain_name, rendered_name = formatted_entry_name(entry, selected: is_selected)
411
411
  prefix_width = 5
412
412
  meta_text = "#{format_relative_time(entry[:mtime])}, #{format('%.1f', entry[:score])}"
413
413
 
@@ -422,13 +422,15 @@ class TrySelector
422
422
  line.write << display_rendered
423
423
 
424
424
  # Right content is lower layer - will be overwritten by left if they overlap
425
- line.right.write_dim(meta_text)
425
+ line.right.write(is_selected ? meta_text : Tui::Text.dim(meta_text))
426
426
  end
427
427
 
428
428
  def render_create_line(screen, is_selected, width)
429
- background = is_selected ? Tui::Palette::SELECTED_BG : nil
429
+ background = if is_selected
430
+ Tui::Palette::SELECTED_BG + Tui::Palette::SELECTED_FG
431
+ end
430
432
  line = screen.body.add_line(background: background)
431
- line.write << (is_selected ? Tui::Text.highlight("→ ") : " ")
433
+ line.write << (is_selected ? Tui::Text.highlight("→ ") + selected_foreground : " ")
432
434
  date_prefix = Time.now.strftime("%Y-%m-%d")
433
435
  label = if @input_buffer.empty?
434
436
  "📂 Create new: #{date_prefix}-"
@@ -438,7 +440,7 @@ class TrySelector
438
440
  line.write << label
439
441
  end
440
442
 
441
- def formatted_entry_name(entry)
443
+ def formatted_entry_name(entry, selected: false)
442
444
  basename = entry[:basename]
443
445
  positions = entry[:highlight_positions] || []
444
446
 
@@ -447,17 +449,25 @@ class TrySelector
447
449
  name_part = $2
448
450
  date_len = date_part.length + 1 # +1 for the hyphen
449
451
 
450
- rendered = Tui::Text.dim(date_part)
452
+ rendered = selected ? date_part : Tui::Text.dim(date_part)
451
453
  # Highlight hyphen if it's in positions
452
- rendered += positions.include?(10) ? Tui::Text.highlight('-') : Tui::Text.dim('-')
453
- rendered += highlight_with_positions(name_part, positions, date_len)
454
+ hyphen = if positions.include?(10)
455
+ Tui::Text.highlight('-')
456
+ elsif selected
457
+ '-'
458
+ else
459
+ Tui::Text.dim('-')
460
+ end
461
+ rendered += hyphen
462
+ rendered += selected_foreground if selected && positions.include?(10)
463
+ rendered += highlight_with_positions(name_part, positions, date_len, selected: selected)
454
464
  ["#{date_part}-#{name_part}", rendered]
455
465
  else
456
- [basename, highlight_with_positions(basename, positions, 0)]
466
+ [basename, highlight_with_positions(basename, positions, 0, selected: selected)]
457
467
  end
458
468
  end
459
469
 
460
- def highlight_with_positions(text, positions, offset)
470
+ def highlight_with_positions(text, positions, offset, selected: false)
461
471
  pos_set = positions.is_a?(Set) ? positions : positions.to_set
462
472
  result = String.new
463
473
  chars = text.chars
@@ -469,6 +479,7 @@ class TrySelector
469
479
  i += 1
470
480
  i += 1 while i < chars.length && pos_set.include?(i + offset)
471
481
  result << Tui::Text.highlight(chars[batch_start...i].join)
482
+ result << selected_foreground if selected
472
483
  else
473
484
  result << chars[i]
474
485
  i += 1
@@ -477,6 +488,10 @@ class TrySelector
477
488
  result
478
489
  end
479
490
 
491
+ def selected_foreground
492
+ Tui.colors_enabled? ? Tui::Palette::SELECTED_FG : ""
493
+ end
494
+
480
495
  # Find the position of the previous word boundary for Ctrl-W deletion.
481
496
  # Skips non-alphanumeric chars, then skips alphanumeric chars.
482
497
  def word_boundary_backward(buffer, cursor)
@@ -955,7 +970,7 @@ end
955
970
  # Main execution with OptionParser subcommands
956
971
  if __FILE__ == $0
957
972
 
958
- VERSION = "1.9.3"
973
+ VERSION = "1.10.1"
959
974
 
960
975
  def print_global_help
961
976
  text = <<~HELP
@@ -984,6 +999,7 @@ if __FILE__ == $0
984
999
  try Open interactive selector
985
1000
  try project Selector with initial filter
986
1001
  try clone https://github.com/user/repo
1002
+ try https://github.com/user/repo/pull/123
987
1003
  try worktree feature-branch
988
1004
 
989
1005
  Manual mode (without alias):
@@ -1053,19 +1069,44 @@ if __FILE__ == $0
1053
1069
  # https://gitlab.com/user/repo or other git hosts
1054
1070
  host, user, repo = $1, $2, $3
1055
1071
  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
1072
+ elsif uri.match(%r{^git@([^:]+):([^/]+)/(.+)})
1073
+ # git@host:user/path/to/repo
1074
+ host, user, path = $1, $2, $3
1075
+ repo = File.basename(path)
1076
+ return { user: user, repo: repo, host: host }
1077
+ elsif uri.match(%r{^ssh://[^@/]+@([^/]+)/([^/]+)/(.+)})
1078
+ # ssh://user@host:port/user/repo
1079
+ host, user, path = $1, $2, $3
1080
+ repo = File.basename(path)
1081
+ return { user: user, repo: repo, host: host }
1082
+ elsif uri.match(%r{^([^@/:]+)@([^:]+):(.+)})
1083
+ # SCP-style SSH: user@host:path/to/repo
1084
+ user, host, path = $1, $2, $3
1085
+ repo = File.basename(path)
1059
1086
  return { user: user, repo: repo, host: host }
1060
1087
  else
1061
1088
  return nil
1062
1089
  end
1063
1090
  end
1064
1091
 
1092
+ def github_pr_details(uri)
1093
+ return nil unless uri
1094
+
1095
+ match = uri.match(%r{\Ahttps?://(?:www\.)?github\.com/([^/]+)/([^/]+)/pull/(\d+)/?\z})
1096
+ return nil unless match
1097
+
1098
+ {
1099
+ user: match[1],
1100
+ repo: match[2].sub(/\.git\z/, ''),
1101
+ pr_id: match[3],
1102
+ git_uri: "https://github.com/#{match[1]}/#{match[2].sub(/\.git\z/, '')}.git"
1103
+ }
1104
+ end
1105
+
1065
1106
  def generate_clone_directory_name(git_uri, custom_name = nil)
1066
1107
  return custom_name if custom_name && !custom_name.empty?
1067
1108
 
1068
- parsed = parse_git_uri(git_uri)
1109
+ parsed = github_pr_details(git_uri) || parse_git_uri(git_uri)
1069
1110
  return nil unless parsed
1070
1111
 
1071
1112
  date_prefix = Time.now.strftime("%Y-%m-%d")
@@ -1166,7 +1207,12 @@ if __FILE__ == $0
1166
1207
  exit 1
1167
1208
  end
1168
1209
 
1169
- script_clone(File.join(tries_path, dir_name), git_uri)
1210
+ path = File.join(tries_path, dir_name)
1211
+ if pr = github_pr_details(git_uri)
1212
+ script_clone_pr(path, pr[:git_uri], pr[:pr_id])
1213
+ else
1214
+ script_clone(path, git_uri)
1215
+ end
1170
1216
  end
1171
1217
 
1172
1218
  def cmd_init!(args, tries_path)
@@ -1355,7 +1401,11 @@ if __FILE__ == $0
1355
1401
  exit 1
1356
1402
  end
1357
1403
  full_path = File.join(tries_path, dir_name)
1358
- return script_clone(full_path, git_uri)
1404
+ if pr = github_pr_details(git_uri)
1405
+ return script_clone_pr(full_path, pr[:git_uri], pr[:pr_id])
1406
+ else
1407
+ return script_clone(full_path, git_uri)
1408
+ end
1359
1409
  end
1360
1410
 
1361
1411
  # Regular interactive selector
@@ -1408,8 +1458,31 @@ if __FILE__ == $0
1408
1458
  end
1409
1459
  end
1410
1460
 
1461
+ # Emit best-effort terminal manager rename commands. Keep these as the final
1462
+ # commands so a missing or broken optional CLI never prevents the directory
1463
+ # change from completing.
1464
+ def terminal_rename_commands(path)
1465
+ name = File.basename(path).sub(/\A\d{4}-\d{2}-\d{2}-/, '')
1466
+ label = "try: #{name}"
1467
+
1468
+ if ENV['HERDR_ENV'] == '1' && ENV['HERDR_PANE_ID'] && !ENV['HERDR_PANE_ID'].empty?
1469
+ commands = [
1470
+ "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"
1471
+ ]
1472
+ if ENV['HERDR_PANE_ID'].match?(/:p1\z/) && ENV['HERDR_WORKSPACE_ID'] && !ENV['HERDR_WORKSPACE_ID'].empty?
1473
+ commands << "command -v herdr >/dev/null 2>&1 && herdr workspace rename #{q(ENV['HERDR_WORKSPACE_ID'])} #{q(label)} >/dev/null 2>&1 || true"
1474
+ end
1475
+ commands
1476
+ elsif (ENV['CMUX_SOCKET_PATH'] && !ENV['CMUX_SOCKET_PATH'].empty?) ||
1477
+ (ENV['CMUX_BUNDLE_ID'] && !ENV['CMUX_BUNDLE_ID'].empty?)
1478
+ ["command -v cmux >/dev/null 2>&1 && cmux rename-tab #{q(label)} >/dev/null 2>&1 || true"]
1479
+ else
1480
+ []
1481
+ end
1482
+ end
1483
+
1411
1484
  def script_cd(path)
1412
- ["touch #{q(path)}", "echo #{q(path)}", "cd #{q(path)}"]
1485
+ ["touch #{q(path)}", "echo #{q(path)}", "cd #{q(path)}"] + terminal_rename_commands(path)
1413
1486
  end
1414
1487
 
1415
1488
  def script_mkdir_cd(path)
@@ -1420,6 +1493,17 @@ if __FILE__ == $0
1420
1493
  ["mkdir -p #{q(path)}", "echo #{q("Using git clone to create this trial from #{uri}.")}", "git clone '#{uri}' #{q(path)}"] + script_cd(path)
1421
1494
  end
1422
1495
 
1496
+ def script_clone_pr(path, uri, pr_id)
1497
+ ref = "pull/#{pr_id}/head"
1498
+ [
1499
+ "mkdir -p #{q(path)}",
1500
+ "echo #{q("Using git clone to create this trial from #{uri} PR ##{pr_id}.")}",
1501
+ "git clone #{q(uri)} #{q(path)}",
1502
+ "git -C #{q(path)} fetch origin #{q(ref)}",
1503
+ "git -C #{q(path)} checkout --detach FETCH_HEAD"
1504
+ ] + script_cd(path)
1505
+ end
1506
+
1423
1507
  def script_worktree(path, repo = nil)
1424
1508
  r = repo ? q(repo) : nil
1425
1509
  worktree_cmd = if r
@@ -1463,7 +1547,7 @@ if __FILE__ == $0
1463
1547
  "mv #{q(old_name)} #{q(new_name)}",
1464
1548
  "echo #{q(new_path)}",
1465
1549
  "cd #{q(new_path)}"
1466
- ]
1550
+ ] + terminal_rename_commands(new_path)
1467
1551
  end
1468
1552
 
1469
1553
  # Return a unique directory name under tries_path by appending -2, -3, ... if needed
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.3
4
+ version: 1.10.1
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.