Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ jobs:
run: rake

- name: Build gem
run: gem build try-cli.gemspec
run: |
make dist
gem build try-cli.gemspec

- name: Configure trusted publishing credentials
uses: rubygems/configure-rubygems-credentials@main
Expand Down
35 changes: 20 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Repository Guidelines

## Project Structure & Module Organization
- `try.rb`: Single-file Ruby CLI and TUI (no gems).
- `flake.nix`/`flake.lock`: Nix packaging and Home Manager module.
- `try.rb`, `lib/tui.rb`, `lib/fuzzy.rb`: Source of truth (edit these). Do not turn the repo into only the concat.
- `dist/try.rb`: Generated single-file Ruby (`make dist` concatenates lib/tui.rb + lib/fuzzy.rb + try.rb). Gitignored.
- `dist/try`: Optional Spinel native binary (`make native` compiles `dist/try.rb`). Gitignored.
- `bin/try`: Tiny router — execs `dist/try` when that executable exists, otherwise loads `dist/try.rb`.
- Gem (`try-cli`): packages `bin/try` + `dist/try.rb` (and LICENSE). Not the split sources, not `dist/try.c`. A local `make native && gem build` may also include `dist/try`; release CI must not run `make native`.
- `flake.nix`/`flake.lock`: Nix packaging and Home Manager module (`make dist` + bin/try + dist/try.rb).
- `README.md`: Usage, installation, and philosophy.
- Tries live outside this repo (default `~/src/tries`, configurable via `TRY_PATH`).

Expand All @@ -23,25 +27,26 @@
- Emitted commands use absolute, quoted paths and are shell-neutral (bash/zsh/fish). Only the wrapper function generated by `init` differs per shell.

## Build, Test, and Development Commands
- `rake` / `rake test`: lint, unit, MRI spec; if Spinel is present, also emit `dist/try.c`, compile `dist/try`, native spec, and `runner_and_compare.sh`.
- `rake lint`: `ruby -c` always; `spinel -c` on `try.rb` and `lib/**/*.rb` when Spinel is available.
- `rake spec_spinel`: `make native` (writes `dist/try.c` then compiles), spec the binary, compare outputs with MRI.
- `make dist` / `rake dist`: concatenate sources into `dist/try.rb`.
- `rake` / `rake test`: lint, unit, MRI spec on `./try.rb` and `dist/try.rb`; if Spinel is present, also emit `dist/try.c` from the concat, compile `dist/try`, native spec, and `runner_and_compare.sh`.
- `rake lint`: `ruby -c` on source files and generated `dist/try.rb`; `spinel -c` on those same files when Spinel is available.
- `rake spec_spinel`: `make native` (compiles `dist/try.rb`), spec the binary, compare outputs with `dist/try.rb`.
- `nix run`: Run the packaged CLI (e.g., `nix run . -- --help`).
- `nix build`: Build the binary derivation; output at `./result/bin/try`.
- `./try.rb init ~/src/tries`: Emit shell function for your shell config.
- `nix build`: Build the derivation; output at `./result/bin/try` (router + `dist/try.rb`).
- `./try.rb init ~/src/tries`: Emit shell function for your shell config (dev; split sources).
- `./try.rb cd`: Launch interactive selector; prints `cd` script to stdout.
- `./try.rb clone <git-uri> [name]`: Clone into date-prefixed directory.
- `make native SPINEL=/path/to/spinel`: AOT a native `dist/try`.
- `make native SPINEL=/path/to/spinel`: AOT a native `dist/try` from `dist/try.rb`.

## Dual implementation (MRI + Spinel)
`try` must keep working on **both**:
- MRI: `ruby try.rb` / the `try-cli` gem
- Spinel AOT: `spinel try.rb -o dist/try` (see `make native`)
- MRI: `ruby try.rb` (dev, split sources) / `dist/try.rb` / the `try-cli` gem (`bin/try` → `dist/try.rb`)
- Spinel AOT: compiles **`dist/try.rb`** (one compilation unit) → `dist/try` (see `make native`)

A construct that only one of them can run is a bug. Prefer stdlib and Spinel-friendly Ruby (no `eval`, `method_missing`, `FileUtils`, `IO#raw`/`IO.console`, etc.) or keep an MRI-compatible shim like `TryCompat`. `rake lint` syntax-checks with `ruby -c` always, and with `spinel -c` on those same files when Spinel is on `PATH` (or `SPINEL=`). If Spinel is missing, lint **warns** and continues.
A construct that only one of them can run is a bug. Prefer stdlib and Spinel-friendly Ruby (no `eval`, `method_missing`, `FileUtils`, `IO#raw`/`IO.console`, etc.) or keep an MRI-compatible shim like `TryCompat`. `rake lint` syntax-checks with `ruby -c` always, and with `spinel -c` on the source files plus `dist/try.rb` when Spinel is on `PATH` (or `SPINEL=`). If Spinel is missing, lint **warns** and continues.

## Coding Style & Naming Conventions
- Ruby, 2-space indent, standard library only; keep it single-file unless necessary.
- Ruby, 2-space indent, standard library only; edit the split sources, generate `dist/try.rb` for ship/native.
- Prefer small, pure functions; no global state beyond `ENV` reads.
- UI tokens live in `UI::TOKEN_MAP`; add tokens with clear names. UI printing runs through `UI.expand_tokens`.
- Shell emission goes through `UI.emit_tasks_script` (don’t handcraft `$dir` scripts).
Expand All @@ -58,9 +63,9 @@ A construct that only one of them can run is a bug. Prefer stdlib and Spinel-fri
**Important**: Specs must reflect the full feature set of `try`. They serve as the canonical reference for behavior, enabling new implementations (in any language) to be validated against the same test suite. When adding or changing features, update both the relevant spec markdown and add corresponding tests.

## Testing Guidelines
- `rake` runs lint + unit + spec. Lint is `ruby -c` on `try.rb` and `lib/**/*.rb`, plus `spinel -c` on each of `try.rb` and `lib/**/*.rb` when Spinel is installed (`SPINEL=/path/to/spinel` to point at a local build). Missing Spinel is a warning, not a hard fail.
- Primary spec run: `./spec/tests/runner.sh ./try.rb` (MRI).
- Native spec run: `make native-test` / `bash spec/tests/runner.sh dist/try` (Spinel). `make native-compare` / `rake spec_spinel` diffs MRI vs native output (init exec prefix is normalized).
- `rake` runs lint + unit + spec on `./try.rb` and `dist/try.rb`. Lint is `ruby -c` on `try.rb`, `lib/**/*.rb`, and generated `dist/try.rb`, plus `spinel -c` on those files when Spinel is installed (`SPINEL=/path/to/spinel` to point at a local build). Missing Spinel is a warning, not a hard fail.
- Primary spec run: `./spec/tests/runner.sh ./try.rb` (MRI source). Also `make dist-test` / `bash spec/tests/runner.sh dist/try.rb` and `bash spec/tests/runner.sh bin/try` (after `make dist`).
- Native spec run: `make native-test` / `bash spec/tests/runner.sh dist/try` (Spinel, compiled from `dist/try.rb`). `make native-compare` / `rake spec_spinel` diffs `dist/try.rb` vs native output (init exec prefix is normalized).
- Manual flows for exploratory testing:
- `TRY_PATH=$(mktemp -d) ./try.rb cd` then create/select directories.
- Validate delete confirmation and scoring by changing `mtime`/`ctime`.
Expand Down
8 changes: 3 additions & 5 deletions Formula/try.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ class Try < Formula
depends_on "ruby"

def install
bin.install "try.rb" => "try"
if build.head?
(bin/"lib").install "lib/tui.rb"
(bin/"lib").install "lib/fuzzy.rb"
end
system "make", "dist"
bin.install "bin/try"
(prefix/"dist").install "dist/try.rb"
end

def caveats
Expand Down
63 changes: 49 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,54 @@ lint: ## Check Ruby syntax
$(RUBY) -c "$$file"; \
done

.PHONY: dist
dist: dist/try.rb ## Concatenate lib/ + try.rb into dist/try.rb

# One working file: lib/tui.rb, lib/fuzzy.rb, then try.rb (no require_relative).
dist/try.rb: lib/tui.rb lib/fuzzy.rb try.rb
@mkdir -p dist
@{ \
printf '%s\n' '#!/usr/bin/env ruby'; \
printf '%s\n' '# Generated by make dist. Do not edit.'; \
printf '%s\n' '# Source: lib/tui.rb + lib/fuzzy.rb + try.rb'; \
Comment on lines +42 to +44
printf '\n'; \
cat lib/tui.rb; \
printf '\n'; \
cat lib/fuzzy.rb; \
printf '\n'; \
sed -e '/^#!/d' \
-e '/require_relative .lib\/tui./d' \
-e '/require_relative .lib\/fuzzy./d' \
try.rb; \
} > dist/try.rb
@chmod +x dist/try.rb
@echo "Wrote dist/try.rb"

.PHONY: dist-test
dist-test: dist/try.rb ## Run specs against dist/try.rb
bash spec/tests/runner.sh dist/try.rb

.PHONY: install
install: ## Install try.rb to ~/.local/
@echo "Installing $(SCRIPT) to ~/.local/..."
@mkdir -p ~/.local
@cp $(SCRIPT) ~/.local/
@chmod +x ~/.local/$(SCRIPT)
@echo "Installed! Add to your shell:"
@echo " eval \"\$$(~/.local/$(SCRIPT) init ~/src/tries)\""
install: dist/try.rb ## Install bin/try + dist/ to ~/.local/
@echo "Installing try to ~/.local/..."
@mkdir -p ~/.local/bin ~/.local/dist
@cp bin/try ~/.local/bin/try
@chmod +x ~/.local/bin/try
@cp dist/try.rb ~/.local/dist/try.rb
@chmod +x ~/.local/dist/try.rb
@if [ -f dist/try ] && [ -x dist/try ]; then cp dist/try ~/.local/dist/try && chmod +x ~/.local/dist/try; fi
@echo "Installed! Add ~/.local/bin to PATH, then:"
@echo " eval \"\$$(try init ~/src/tries)\""

.PHONY: install-global
install-global: ## Install try.rb to /usr/local/bin/
@echo "Installing $(SCRIPT) to /usr/local/bin/..."
@sudo cp $(SCRIPT) /usr/local/bin/try
install-global: dist/try.rb ## Install bin/try + dist/ to /usr/local/
@echo "Installing try to /usr/local/..."
@sudo mkdir -p /usr/local/bin /usr/local/dist
@sudo cp bin/try /usr/local/bin/try
@sudo chmod +x /usr/local/bin/try
@sudo cp dist/try.rb /usr/local/dist/try.rb
@sudo chmod +x /usr/local/dist/try.rb
@if [ -f dist/try ] && [ -x dist/try ]; then sudo cp dist/try /usr/local/dist/try && sudo chmod +x /usr/local/dist/try; fi
@echo "Installed globally! Add to your shell:"
@echo " eval \"\$$(try init ~/src/tries)\""

Expand Down Expand Up @@ -81,6 +115,7 @@ clean: ## Clean up temporary files
@echo "Cleaning up..."
@find . -name "*.tmp" -delete
@find . -name "*~" -delete
@rm -rf dist
@echo "Clean complete"

.PHONY: check-deps
Expand Down Expand Up @@ -117,7 +152,7 @@ l: lint ## Shortcut for lint
.PHONY: i
i: install ## Shortcut for install

# Native binary via Spinel (optional)
# Native binary via Spinel (optional) — compiles the concat, not the split sources
SPINEL ?= spinel
SPINEL_FLAGS ?= -O s
CC ?= cc
Expand All @@ -129,9 +164,9 @@ SPINEL_LIB ?= $(dir $(SPINEL_BIN))../lib
.PHONY: native native-test native-compare
native: $(NATIVE)

$(NATIVE_C): try.rb lib/tui.rb lib/fuzzy.rb
$(NATIVE_C): dist/try.rb
mkdir -p dist
$(SPINEL) $(SPINEL_FLAGS) -c try.rb -o $(NATIVE_C)
$(SPINEL) $(SPINEL_FLAGS) -c dist/try.rb -o $(NATIVE_C)

$(NATIVE): $(NATIVE_C)
$(CC) -Os -Wno-all -ffunction-sections -fdata-sections \
Expand All @@ -144,4 +179,4 @@ native-test: $(NATIVE)
bash spec/tests/runner.sh $(NATIVE)

native-compare: $(NATIVE)
bash spec/tests/runner_and_compare.sh ./try.rb $(NATIVE)
bash spec/tests/runner_and_compare.sh dist/try.rb $(NATIVE)
37 changes: 23 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Instantly navigate through all your experiment directories with:
- **Fuzzy search** that just works
- **Smart sorting** - recently used stuff bubbles to the top
- **Auto-dating** - creates directories like `2025-08-17-redis-experiment`
- **Zero config** - just one Ruby file, no dependencies
- **Zero config** - no runtime gem dependencies

## Installation

Expand All @@ -30,6 +30,8 @@ Instantly navigate through all your experiment directories with:
gem install try-cli
```

The gem ships `bin/try` plus a single generated `dist/try.rb` (`make dist` concatenates `lib/tui.rb`, `lib/fuzzy.rb`, and `try.rb`). `bin/try` loads that script, or execs `dist/try` when a Spinel native build is present.

Then add to your shell:

```bash
Expand All @@ -42,32 +44,39 @@ try init | source

### Quick Start (Manual)

```bash
curl -sL https://raw.githubusercontent.com/tobi/try/refs/heads/main/try.rb > ~/.local/try.rb
Edit the split sources (`try.rb`, `lib/tui.rb`, `lib/fuzzy.rb`), then build the single-file script:

# Make "try" executable so it can be run directly
chmod +x ~/.local/try.rb
```bash
git clone https://github.com/tobi/try.git
cd try
make dist # writes dist/try.rb
# optional: make native # writes dist/try (Spinel)

# bin/try routes to dist/try when present, otherwise dist/try.rb
chmod +x bin/try
# Add to your shell (bash/zsh)
echo 'eval "$(ruby ~/.local/try.rb init ~/src/tries)"' >> ~/.zshrc
echo 'eval "$(./bin/try init ~/src/tries)"' >> ~/.zshrc

# for fish shell users
echo '~/.local/try.rb init ~/src/tries | source' >> ~/.config/fish/config.fish
echo './bin/try init ~/src/tries | source' >> ~/.config/fish/config.fish
```

Or run `dist/try.rb` directly after `make dist`.


### Native binary (optional)

Compile a native `try` with [Spinel](https://github.com/matz/spinel). Build Spinel from source; [PR 3906](https://github.com/matz/spinel/pull/3906) is required so `IO#tty?` / `#winsize` work on handles that are not statically typed IO.
Compile a native `try` with [Spinel](https://github.com/matz/spinel). `make native` compiles the concatenated `dist/try.rb` (one compilation unit), not the split sources. Build Spinel from source; [PR 3906](https://github.com/matz/spinel/pull/3906) is required so `IO#tty?` / `#winsize` work on handles that are not statically typed IO.

```bash
make native SPINEL=/path/to/spinel # -O s, then strip
make dist
make native SPINEL=/path/to/spinel # compiles dist/try.rb, then strip
./dist/try --help
eval "$(./dist/try init)" # wires the shell function to the binary, not MRI
eval "$(./bin/try init)" # bin/try execs dist/try when present
make native-test SPINEL=/path/to/spinel
```

MRI `ruby try.rb` and the gem keep working. `dist/try init` emits the binary path; `ruby try.rb init` still emits `/usr/bin/env ruby '…/try.rb'`.
MRI `ruby try.rb` (dev), `dist/try.rb`, and the gem keep working. `bin/try` picks `dist/try` when that executable exists, otherwise `dist/try.rb`. The published gem does not include a Linux ELF `dist/try`.

## The Problem

Expand Down Expand Up @@ -263,10 +272,10 @@ After installation, add to your shell:

## Why Ruby?

- One file, no dependencies
- Small codebase, no runtime gem dependencies
- Works on any system with Ruby (macOS has it built-in)
- Fast enough for thousands of directories
- Easy to hack on
- Easy to hack on — edit `try.rb` / `lib/`, then `make dist`

## The Philosophy

Expand All @@ -290,7 +299,7 @@ A: First, welcome to the club. Second, it handles it fine - the scoring algorith

## Contributing

It's one file. If you want to change something, just edit it. Send a PR if you think others would like it too.
Edit `try.rb`, `lib/tui.rb`, and `lib/fuzzy.rb` (those stay the source of truth). `make dist` concatenates them into `dist/try.rb`. Send a PR if you think others would like it too.

## License

Expand Down
28 changes: 23 additions & 5 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@ Rake::TestTask.new(:unit) do |t|
t.pattern = 'test/**/*_test.rb'
end

desc "Build concatenated dist/try.rb"
task :dist do
sh 'make', 'dist'
end

desc "Check syntax with MRI and Spinel (warns if Spinel is missing)"
task :lint do
RUBY_SOURCES.each do |file|
sh 'ruby', '-c', file
end

sh 'make', 'dist/try.rb'
sh 'ruby', '-c', 'dist/try.rb'

unless spinel_available?
warn "warning: spinel not found (#{spinel_cmd}); skipping Spinel syntax check"
warn "warning: try must parse and run on both MRI Ruby and Spinel"
Expand All @@ -33,14 +41,24 @@ task :lint do
sh spinel_cmd, '-c', file, '-o', tmp.path
end
end

Tempfile.create(['try-spinel-dist', '.c']) do |tmp|
sh spinel_cmd, '-c', 'dist/try.rb', '-o', tmp.path
end
end

desc "Run shell spec compliance tests (MRI)"
desc "Run shell spec compliance tests (MRI source)"
task :spec do
sh 'bash', 'spec/tests/runner.sh', './try.rb'
end

desc "Emit dist/try.c, compile dist/try, spec it, and compare with MRI"
desc "Run shell spec compliance tests against dist/try.rb"
task :spec_dist do
sh 'make', 'dist'
sh 'bash', 'spec/tests/runner.sh', 'dist/try.rb'
end

desc "Emit dist/try.c from concat, compile dist/try, spec it, and compare with MRI"
task :spec_spinel do
unless spinel_available?
warn "warning: spinel not found (#{spinel_cmd}); skipping native spec + compare"
Expand All @@ -49,10 +67,10 @@ task :spec_spinel do

sh 'make', 'native', "SPINEL=#{spinel_cmd}"
sh 'bash', 'spec/tests/runner.sh', 'dist/try'
sh 'bash', 'spec/tests/runner_and_compare.sh', './try.rb', 'dist/try'
sh 'bash', 'spec/tests/runner_and_compare.sh', 'dist/try.rb', 'dist/try'
end

desc "Run all tests (lint + unit + spec; native spec+compare if Spinel is present)"
task test: [:lint, :unit, :spec, :spec_spinel]
desc "Run all tests (lint + unit + spec + dist spec; native spec+compare if Spinel is present)"
task test: [:lint, :unit, :spec, :spec_dist, :spec_spinel]

task default: :test
12 changes: 8 additions & 4 deletions bin/try
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

$0 = File.expand_path('../try.rb', __dir__)
load $0
root = File.expand_path('..', __dir__)
native = File.join(root, 'dist', 'try')
script = File.join(root, 'dist', 'try.rb')
if File.file?(native) && File.executable?(native)
exec native, *ARGV
end
abort "try: missing #{script} (run: make dist)" unless File.file?(script)
load script
9 changes: 5 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,11 @@
nativeBuildInputs = [ pkgs.makeBinaryWrapper ];

installPhase = ''
mkdir -p $out/bin
cp try.rb $out/bin/try
cp -r lib $out/bin/
chmod +x $out/bin/try
make SHELL=bash dist
mkdir -p $out/bin $out/dist
cp bin/try $out/bin/try
cp dist/try.rb $out/dist/try.rb
chmod +x $out/bin/try $out/dist/try.rb

wrapProgram $out/bin/try \
--prefix PATH : ${ruby}/bin
Expand Down
Loading