Last active
June 30, 2025 17:59
-
-
Save kch/7e80148678fe42820b850fc856de0754 to your computer and use it in GitHub Desktop.
iTerm2 file opener for semantic history
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/opt/homebrew/opt/ruby/bin/ruby | |
# iTerm2 file opener for semantic history | |
# | |
# Setup: iTerm2 » Settings » Profiles » Advanced » Semantic History » Always run command… | |
# Command: /path/to/iterm2-edit \5 \1 | |
# | |
# Parses text selections and opens appropriate handler: | |
# - Text files: $EDITOR with line:col positioning | |
# - Binary files: system default app | |
# - URLs: browser | |
# | |
# Path resolution priority: | |
# 1. Absolute paths | |
# 2. Relative to working directory | |
# 3. Relative to git root | |
# 4. Git diff paths (strips a/b/ prefixes as fallback) | |
require 'open3' | |
require 'shellwords' | |
def shell(*args) = Open3.capture2(*args).then{ |out,status| out.chomp if status.success? } | |
def git_root(path) = shell("git -C #{path.shellescape} rev-parse --show-toplevel") | |
def text_file?(path) = shell("file", path).match?(/ASCII text|UTF-8|text/) | |
def file_exists?(path) = File.exist?(path) | |
def try_file(*parts, lc: nil) = File.join(*parts).then{ [text_file?(it) ? :text : :binary, it, *lc] if file_exists?(it) } | |
def detect(text, cwd) | |
text = text.sub(/\A"(.*)"\z/, '\1') # unwrap path in "..." | |
text = text.sub(/\A\[(.*)\]\z/, '\1') # unwrap path in [...] | |
_, path, *lc = *text.match(/\A(.*?)(?::(\d+))?(?::(\d+))?\z/) # path[:line][:col] | |
# absolute path | |
return try_file(path, lc: lc) if path[0] == ?/ | |
# relative: will look in cwd and git root for path or path with a/ b/ stripped | |
dirs = [cwd, git_root(cwd)].uniq.compact | |
paths = [path, path.sub(%r[^[ab]/], "")].uniq.compact | |
rel = dirs.product(paths).lazy.filter_map { try_file(*it, lc: lc) }.first | |
return rel if rel | |
# URLs | |
url = (protocol = text.match?(%r[^[a-z]+://])) || text.match?(/^[a-z0-9.-]+\.[a-z]{2,}/) | |
return [:url, protocol ? text : "http://#{text}"] if url | |
return nil | |
end | |
### Main | |
unless ARGV.empty? | |
working_dir, filename, = *ARGV | |
exit 1 unless filename | |
type, path, line, col = result = detect(filename, working_dir) | |
exit unless result | |
case type | |
when :text | |
# figure out editor from an interactive shell, or the fallbackest of the fallbacks | |
editor = shell(%[env TERM=dumb #{ENV["SHELL"]} -l -i -c 'eval "set -- $EDITOR" && command -v "$1"']) || "open -a TextEdit" | |
exec "#{editor} #{[path.shellescape, line, col].compact.join(":")}" | |
# OR JUST HARDCODE ONE OF | |
# exec("open", "subl://open?url=file://#{path}&line=#{line}&column=#{col}") | |
# exec("open", "txmt://open?url=file://#{path}&line=#{line}&column=#{col}") | |
# exec("open", "vscode://file/#{path}:#{line}:#{col}") | |
# exec("open", "zed://file/#{path}:#{line}:#{col}") | |
when :binary then exec("open", path) | |
when :url then exec("open", path) | |
end | |
exit 1 | |
end | |
############# | |
### Tests ### | |
############# | |
def shell(*args) = raise | |
def text_file?(path) = path.end_with?(".rb") | |
def git_root(dir) = ("/repo" if dir.include?("repo")) | |
def file_exists?(path) | |
%[ | |
/abs/path/file.rb | |
/no-git/file.rb | |
/repo/src/main.rb | |
/repo/image.png | |
/repo/working/file.rb | |
/repo/working/a/diff.rb | |
/repo/working/b/change.rb | |
/repo/working/stripped.rb | |
/repo/stripped.rb | |
/repo/a/prefixed.rb | |
/repo/b/prefixed.rb | |
/repo/working/spaces in name.rb | |
/repo/working/[brackets].rb | |
/repo/working/nested/deep/file.rb | |
].strip.split(/\s*\n\s*/).include?(path) | |
end | |
def test(a, b, msg) | |
return if a == b | |
puts "Failed: #{msg} #{[a,b].inspect} @ #{caller_locations(1, 1)[0].then{ it.path + ":" + it.lineno.to_s }}" | |
exit 1 | |
end | |
repo = "/repo/working" | |
# Path resolution - basic file finding | |
test detect("/abs/path/file.rb", repo), [:text, "/abs/path/file.rb", nil, nil], "absolute path" | |
test detect("file.rb", repo), [:text, "/repo/working/file.rb", nil, nil], "relative in working dir" | |
test detect("src/main.rb", repo), [:text, "/repo/src/main.rb", nil, nil], "relative from git root" | |
test detect("nested/deep/file.rb", repo), [:text, "/repo/working/nested/deep/file.rb", nil, nil], "nested relative path" | |
# Git diff prefix handling - a/ and b/ paths from git output | |
test detect("a/diff.rb", repo), [:text, "/repo/working/a/diff.rb", nil, nil], "a/ found as-is in working dir" | |
test detect("b/change.rb", repo), [:text, "/repo/working/b/change.rb", nil, nil], "b/ found as-is in working dir" | |
test detect("a/stripped.rb", repo), [:text, "/repo/working/stripped.rb", nil, nil], "a/ prefix stripped fallback" | |
test detect("b/stripped.rb", repo), [:text, "/repo/working/stripped.rb", nil, nil], "b/ prefix stripped fallback" | |
test detect("a/prefixed.rb", repo), [:text, "/repo/a/prefixed.rb", nil, nil], "a/ found as-is in git root" | |
test detect("b/prefixed.rb", repo), [:text, "/repo/b/prefixed.rb", nil, nil], "b/ found as-is in git root" | |
# File types | |
test detect("file.rb", repo), [:text, "/repo/working/file.rb", nil, nil], "text file" | |
test detect("image.png", repo), [:binary, "/repo/image.png", nil, nil], "binary file" | |
# Line/column parsing | |
test detect("file.rb:123", repo), [:text, "/repo/working/file.rb", "123", nil], "file with line number" | |
test detect("file.rb:123:45", repo), [:text, "/repo/working/file.rb", "123", "45"], "file with line and column" | |
test detect("/abs/path/file.rb:10:5", repo), [:text, "/abs/path/file.rb", "10", "5"], "absolute path with line/col" | |
# Quoted/bracketed paths | |
test detect('"file.rb"', repo), [:text, "/repo/working/file.rb", nil, nil], "double quoted path" | |
test detect('"spaces in name.rb"', repo), [:text, "/repo/working/spaces in name.rb", nil, nil], "double quoted with spaces" | |
test detect('"file.rb:123:45"', repo), [:text, "/repo/working/file.rb", "123", "45"], "double quoted with line/col" | |
test detect("[brackets].rb", repo), [:text, "/repo/working/[brackets].rb", nil, nil], "bracketed path" | |
test detect("[file.rb]", repo), [:text, "/repo/working/file.rb", nil, nil], "square bracketed input" | |
# URLs | |
test detect("example.com", repo), [:url, "http://example.com"], "URL domain only" | |
test detect("x.com/path/to/page", repo), [:url, "http://x.com/path/to/page"], "URL with path" | |
test detect("https://example.com/path", repo), [:url, "https://example.com/path"], "URL with protocol" | |
test detect("ftp://example.com/file", repo), [:url, "ftp://example.com/file"], "URL different protocol" | |
test detect("sub.example.com", repo), [:url, "http://sub.example.com"], "URL with subdomain" | |
test detect("example.com:8080", repo), [:url, "http://example.com:8080"], "URL with port" | |
test detect("x.co/p?q=1", repo), [:url, "http://x.co/p?q=1"], "URL with query params" | |
test detect("x.co/p#section", repo), [:url, "http://x.co/p#section"], "URL with fragment" | |
# Edge cases | |
test detect("file.rb", "/no-git"), [:text, "/no-git/file.rb", nil, nil], "no git root available" | |
test detect("truly-nonexistent-file", repo), nil, "no match for nonexistent file" | |
test detect("justtext", repo), nil, "not URL - single word" | |
test detect("", repo), nil, "empty string" | |
puts "ok" |
Author
kch
commented
Jun 30, 2025

Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment