try-cli 1.7.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +259 -0
- data/VERSION +1 -0
- data/bin/try +4 -0
- data/lib/fuzzy.rb +133 -0
- data/lib/tui.rb +892 -0
- data/try.rb +1281 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a6b118821db0d2fc53ac1fdb38f5574d13bee02e25b39d7362f51ab7aa128976
|
|
4
|
+
data.tar.gz: a3eb513bf97a2a9fe0e082d1b3166049c320739261158768a40880aa9d04ca54
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bbcc44f8afa56ae146d95a03dadec98bea06b0726fafed760926d9662c30321f19632c171b766956878c88b0b5e318f201c31894a73be571509d0ec0eda91ebf
|
|
7
|
+
data.tar.gz: 6759723862c35789b434bb0339397334b25cb046d06bba86632b8639eeaf2bb593cc65ff82aef891e122bc966d38d1c98c48357807a4a63e643f985944514442
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tobi Lutke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# try - fresh directories for every vibe
|
|
2
|
+
|
|
3
|
+
*Your experiments deserve a home.* 🏠
|
|
4
|
+
|
|
5
|
+
> For everyone who constantly creates new projects for little experiments, a one-file Ruby script to quickly manage and navigate to keep them somewhat organized
|
|
6
|
+
|
|
7
|
+
Ever find yourself with 50 directories named `test`, `test2`, `new-test`, `actually-working-test`, scattered across your filesystem? Or worse, just coding in `/tmp` and losing everything?
|
|
8
|
+
|
|
9
|
+
**try** is here for your beautifully chaotic mind.
|
|
10
|
+
|
|
11
|
+
# What it does
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
*[View interactive version on asciinema](https://asciinema.org/a/ve8AXBaPhkKz40YbqPTlVjqgs)*
|
|
16
|
+
|
|
17
|
+
Instantly navigate through all your experiment directories with:
|
|
18
|
+
- **Fuzzy search** that just works
|
|
19
|
+
- **Smart sorting** - recently used stuff bubbles to the top
|
|
20
|
+
- **Auto-dating** - creates directories like `2025-08-17-redis-experiment`
|
|
21
|
+
- **Zero config** - just one Ruby file, no dependencies
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
curl -sL https://raw.githubusercontent.com/tobi/try/refs/heads/main/try.rb > ~/.local/try.rb
|
|
27
|
+
|
|
28
|
+
# Make "try" executable so it can be run directly
|
|
29
|
+
chmod +x ~/.local/try.rb
|
|
30
|
+
|
|
31
|
+
# Add to your shell (bash/zsh)
|
|
32
|
+
echo 'eval "$(ruby ~/.local/try.rb init ~/src/tries)"' >> ~/.zshrc
|
|
33
|
+
|
|
34
|
+
# for fish shell users
|
|
35
|
+
echo 'eval (~/.local/try.rb init ~/src/tries | string collect)' >> ~/.config/fish/config.fish
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## The Problem
|
|
39
|
+
|
|
40
|
+
You're learning Redis. You create `/tmp/redis-test`. Then `~/Desktop/redis-actually`. Then `~/projects/testing-redis-again`. Three weeks later you can't find that brilliant connection pooling solution you wrote at 2am.
|
|
41
|
+
|
|
42
|
+
## The Solution
|
|
43
|
+
|
|
44
|
+
All your experiments in one place, with instant fuzzy search:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
$ try pool
|
|
48
|
+
→ 2025-08-14-redis-connection-pool 2h, 18.5
|
|
49
|
+
2025-08-03-thread-pool 3d, 12.1
|
|
50
|
+
2025-07-22-db-pooling 2w, 8.3
|
|
51
|
+
+ Create new: pool
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Type, arrow down, enter. You're there.
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
### 🎯 Smart Fuzzy Search
|
|
59
|
+
Not just substring matching - it's smart:
|
|
60
|
+
- `rds` matches `redis-server`
|
|
61
|
+
- `connpool` matches `connection-pool`
|
|
62
|
+
- Recent stuff scores higher
|
|
63
|
+
- Shorter names win on equal matches
|
|
64
|
+
|
|
65
|
+
### ⏰ Time-Aware
|
|
66
|
+
- Shows how long ago you touched each project
|
|
67
|
+
- Recently accessed directories float to the top
|
|
68
|
+
- Perfect for "what was I working on yesterday?"
|
|
69
|
+
|
|
70
|
+
### 🎨 Pretty TUI
|
|
71
|
+
- Clean, minimal interface
|
|
72
|
+
- Highlights matches as you type
|
|
73
|
+
- Shows scores so you know why things are ranked
|
|
74
|
+
- Dark mode by default (because obviously)
|
|
75
|
+
|
|
76
|
+
### 📁 Organized Chaos
|
|
77
|
+
- Everything lives in `~/src/tries` (configurable via `TRY_PATH`)
|
|
78
|
+
- Auto-prefixes with dates: `2025-08-17-your-idea`
|
|
79
|
+
- Skip the date prompt if you already typed a name
|
|
80
|
+
|
|
81
|
+
### Shell Integration
|
|
82
|
+
|
|
83
|
+
- Bash/Zsh:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# default is ~/src/tries
|
|
87
|
+
eval "$(~/.local/try.rb init)"
|
|
88
|
+
# or pick a path
|
|
89
|
+
eval "$(~/.local/try.rb init ~/src/tries)"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- Fish:
|
|
93
|
+
|
|
94
|
+
```fish
|
|
95
|
+
eval (~/.local/try.rb init | string collect)
|
|
96
|
+
# or pick a path
|
|
97
|
+
eval (~/.local/try.rb init ~/src/tries | string collect)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Notes:
|
|
101
|
+
- The runtime commands printed by `try` are shell-neutral (absolute paths, quoted). Only the small wrapper function differs per shell.
|
|
102
|
+
|
|
103
|
+
## Usage
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
try # Browse all experiments
|
|
107
|
+
try redis # Jump to redis experiment or create new
|
|
108
|
+
try new api # Start with "2025-08-17-new-api"
|
|
109
|
+
try . [name] # Create a dated worktree dir for current repo
|
|
110
|
+
try ./path/to/repo [name] # Use another repo as the worktree source
|
|
111
|
+
try worktree dir [name] # Same as above, explicit CLI form
|
|
112
|
+
try clone https://github.com/user/repo.git # Clone repo into date-prefixed directory
|
|
113
|
+
try https://github.com/user/repo.git # Shorthand for clone (same as above)
|
|
114
|
+
try --help # See all options
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Notes on worktrees (`try .` / `try worktree dir`):
|
|
118
|
+
- With a custom [name], uses that; otherwise uses cwd’s basename. Both are prefixed with today’s date.
|
|
119
|
+
- Inside a Git repo: adds a detached HEAD git worktree to the created directory.
|
|
120
|
+
- Outside a repo: simply creates the directory and changes into it.
|
|
121
|
+
|
|
122
|
+
### Git Repository Cloning
|
|
123
|
+
|
|
124
|
+
**try** can automatically clone git repositories into properly named experiment directories:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Clone with auto-generated directory name
|
|
128
|
+
try clone https://github.com/tobi/try.git
|
|
129
|
+
# Creates: 2025-08-27-tobi-try
|
|
130
|
+
|
|
131
|
+
# Clone with custom name
|
|
132
|
+
try clone https://github.com/tobi/try.git my-fork
|
|
133
|
+
# Creates: my-fork
|
|
134
|
+
|
|
135
|
+
# Shorthand syntax (no need to type 'clone')
|
|
136
|
+
try https://github.com/tobi/try.git
|
|
137
|
+
# Creates: 2025-08-27-tobi-try
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Supported git URI formats:
|
|
141
|
+
- `https://github.com/user/repo.git` (HTTPS GitHub)
|
|
142
|
+
- `git@github.com:user/repo.git` (SSH GitHub)
|
|
143
|
+
- `https://gitlab.com/user/repo.git` (GitLab)
|
|
144
|
+
- `git@host.com:user/repo.git` (SSH other hosts)
|
|
145
|
+
|
|
146
|
+
The `.git` suffix is automatically removed from URLs when generating directory names.
|
|
147
|
+
|
|
148
|
+
### Keyboard Shortcuts
|
|
149
|
+
|
|
150
|
+
- `↑/↓` or `Ctrl-P/N/J/K` - Navigate
|
|
151
|
+
- `Enter` - Select or create
|
|
152
|
+
- `Backspace` - Delete character
|
|
153
|
+
- `Ctrl-D` - Delete directory (with confirmation)
|
|
154
|
+
- `ESC` - Cancel
|
|
155
|
+
- Just type to filter
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
Set `TRY_PATH` to change where experiments are stored:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
export TRY_PATH=~/code/sketches
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Default: `~/src/tries`
|
|
166
|
+
|
|
167
|
+
## Nix
|
|
168
|
+
|
|
169
|
+
### Quick start
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
nix run github:tobi/try
|
|
173
|
+
nix run github:tobi/try -- --help
|
|
174
|
+
nix run github:tobi/try init ~/my-tries
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Home Manager
|
|
178
|
+
|
|
179
|
+
```nix
|
|
180
|
+
{
|
|
181
|
+
inputs.try.url = "github:tobi/try";
|
|
182
|
+
|
|
183
|
+
imports = [ inputs.try.homeManagerModules.default ];
|
|
184
|
+
|
|
185
|
+
programs.try = {
|
|
186
|
+
enable = true;
|
|
187
|
+
path = "~/experiments"; # optional, defaults to ~/src/tries
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Homebrew
|
|
193
|
+
|
|
194
|
+
### Quick start
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
brew tap tobi/try https://github.com/tobi/try
|
|
198
|
+
brew install try
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
After installation, add to your shell:
|
|
202
|
+
|
|
203
|
+
- Bash/Zsh:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# default is ~/src/tries
|
|
207
|
+
eval "$(try init)"
|
|
208
|
+
# or pick a path
|
|
209
|
+
eval "$(try init ~/src/tries)"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- Fish:
|
|
213
|
+
|
|
214
|
+
```fish
|
|
215
|
+
eval "(try init | string collect)"
|
|
216
|
+
# or pick a path
|
|
217
|
+
eval "(try init ~/src/tries | string collect)"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Why Ruby?
|
|
221
|
+
|
|
222
|
+
- One file, no dependencies
|
|
223
|
+
- Works on any system with Ruby (macOS has it built-in)
|
|
224
|
+
- Fast enough for thousands of directories
|
|
225
|
+
- Easy to hack on
|
|
226
|
+
|
|
227
|
+
## The Philosophy
|
|
228
|
+
|
|
229
|
+
Your brain doesn't work in neat folders. You have ideas, you try things, you context-switch like a caffeinated squirrel. This tool embraces that.
|
|
230
|
+
|
|
231
|
+
Every experiment gets a home. Every home is instantly findable. Your 2am coding sessions are no longer lost to the void.
|
|
232
|
+
|
|
233
|
+
## FAQ
|
|
234
|
+
|
|
235
|
+
**Q: Why not just use `cd` and `ls`?**
|
|
236
|
+
A: Because you have 200 directories and can't remember if you called it `test-redis`, `redis-test`, or `new-redis-thing`.
|
|
237
|
+
|
|
238
|
+
**Q: Why not use `fzf`?**
|
|
239
|
+
A: fzf is great for files. This is specifically for project directories, with time-awareness and auto-creation built in.
|
|
240
|
+
|
|
241
|
+
**Q: Can I use this for real projects?**
|
|
242
|
+
A: You can, but it's designed for experiments. Real projects deserve real names in real locations.
|
|
243
|
+
|
|
244
|
+
**Q: What if I have thousands of experiments?**
|
|
245
|
+
A: First, welcome to the club. Second, it handles it fine - the scoring algorithm ensures relevant stuff stays on top.
|
|
246
|
+
|
|
247
|
+
## Contributing
|
|
248
|
+
|
|
249
|
+
It's one file. If you want to change something, just edit it. Send a PR if you think others would like it too.
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
MIT - Do whatever you want with it.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
*Built for developers with ADHD by developers with ADHD.*
|
|
258
|
+
|
|
259
|
+
*Your experiments deserve a home.* 🏠
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.7.1
|
data/bin/try
ADDED
data/lib/fuzzy.rb
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Fuzzy string matching with scoring and highlight positions
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# entries = [
|
|
7
|
+
# { text: "2024-01-15-project", base_score: 3.2 },
|
|
8
|
+
# { text: "2024-02-20-another", base_score: 1.5 },
|
|
9
|
+
# ]
|
|
10
|
+
# fuzzy = Fuzzy.new(entries)
|
|
11
|
+
#
|
|
12
|
+
# # Get all matches
|
|
13
|
+
# fuzzy.match("proj").each do |entry, positions, score|
|
|
14
|
+
# puts "#{entry[:text]} score=#{score} highlight=#{positions.inspect}"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # Limit results
|
|
18
|
+
# fuzzy.match("proj").limit(10).each { |entry, positions, score| ... }
|
|
19
|
+
#
|
|
20
|
+
class Fuzzy
|
|
21
|
+
Entry = Data.define(:data, :text, :text_lower, :base_score)
|
|
22
|
+
|
|
23
|
+
def initialize(entries)
|
|
24
|
+
@entries = entries.map do |e|
|
|
25
|
+
text = e[:text] || e["text"]
|
|
26
|
+
Entry.new(
|
|
27
|
+
data: e,
|
|
28
|
+
text: text,
|
|
29
|
+
text_lower: text.downcase,
|
|
30
|
+
base_score: e[:base_score] || e["base_score"] || 0.0
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns a MatchResult enumerator for the query
|
|
36
|
+
def match(query)
|
|
37
|
+
MatchResult.new(@entries, query.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Enumerator wrapper that supports .limit() and .each
|
|
41
|
+
class MatchResult
|
|
42
|
+
include Enumerable
|
|
43
|
+
|
|
44
|
+
def initialize(entries, query)
|
|
45
|
+
@entries = entries
|
|
46
|
+
@query = query
|
|
47
|
+
@query_lower = query.downcase
|
|
48
|
+
@query_chars = @query_lower.chars
|
|
49
|
+
@limit = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Set maximum number of results
|
|
53
|
+
def limit(n)
|
|
54
|
+
@limit = n
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Iterate over matches: yields (entry_data, highlight_positions, score)
|
|
59
|
+
def each(&block)
|
|
60
|
+
return enum_for(:each) unless block_given?
|
|
61
|
+
|
|
62
|
+
results = []
|
|
63
|
+
|
|
64
|
+
@entries.each do |entry|
|
|
65
|
+
score, positions = calculate_match(entry)
|
|
66
|
+
next if score.nil? # No match
|
|
67
|
+
|
|
68
|
+
results << [entry.data, positions, score]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Sort by score descending
|
|
72
|
+
results.sort_by! { |_, _, score| -score }
|
|
73
|
+
|
|
74
|
+
# Apply limit
|
|
75
|
+
results = results.first(@limit) if @limit
|
|
76
|
+
|
|
77
|
+
results.each(&block)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Pre-computed sqrt values for proximity bonus (gap 0-15)
|
|
83
|
+
SQRT_TABLE = (0..16).map { |n| 2.0 / Math.sqrt(n + 1) }.freeze
|
|
84
|
+
|
|
85
|
+
def calculate_match(entry)
|
|
86
|
+
positions = []
|
|
87
|
+
score = entry.base_score
|
|
88
|
+
|
|
89
|
+
# Empty query = match all with base score only
|
|
90
|
+
return [score, positions] if @query.empty?
|
|
91
|
+
|
|
92
|
+
text = entry.text_lower
|
|
93
|
+
query_chars = @query_chars
|
|
94
|
+
query_len = query_chars.length
|
|
95
|
+
|
|
96
|
+
last_pos = -1
|
|
97
|
+
pos = 0
|
|
98
|
+
|
|
99
|
+
query_chars.each do |qc|
|
|
100
|
+
# Find next occurrence of query char starting from pos
|
|
101
|
+
found = text.index(qc, pos)
|
|
102
|
+
return nil unless found # No match
|
|
103
|
+
|
|
104
|
+
positions << found
|
|
105
|
+
|
|
106
|
+
# Base match point
|
|
107
|
+
score += 1.0
|
|
108
|
+
|
|
109
|
+
# Word boundary bonus (start of string or after non-alphanumeric)
|
|
110
|
+
if found == 0 || text[found - 1].match?(/[^a-z0-9]/)
|
|
111
|
+
score += 1.0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Proximity bonus (consecutive chars score higher)
|
|
115
|
+
if last_pos >= 0
|
|
116
|
+
gap = found - last_pos - 1
|
|
117
|
+
score += gap < 16 ? SQRT_TABLE[gap] : (2.0 / Math.sqrt(gap + 1))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
last_pos = found
|
|
121
|
+
pos = found + 1
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Density bonus: prefer shorter spans
|
|
125
|
+
score *= (query_len.to_f / (last_pos + 1)) if last_pos >= 0
|
|
126
|
+
|
|
127
|
+
# Length penalty: shorter strings score higher
|
|
128
|
+
score *= (10.0 / (entry.text.length + 10.0))
|
|
129
|
+
|
|
130
|
+
[score, positions]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|