├── .github └── workflows │ └── flakehub-publish-rolling.yml ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── default.nix ├── flake.lock ├── flake.nix ├── h ├── renovate.json └── up /.github/workflows/flakehub-publish-rolling.yml: -------------------------------------------------------------------------------- 1 | name: "Publish every Git push to master to FlakeHub" 2 | on: 3 | push: 4 | branches: 5 | - "master" 6 | jobs: 7 | flakehub-publish: 8 | runs-on: "ubuntu-latest" 9 | permissions: 10 | id-token: "write" 11 | contents: "read" 12 | steps: 13 | - uses: "actions/checkout@v4" 14 | - uses: "DeterminateSystems/nix-installer-action@main" 15 | - uses: "DeterminateSystems/flakehub-push@main" 16 | with: 17 | name: "zimbatm/h" 18 | rolling: true 19 | visibility: "public" 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /result 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: nix 2 | script: nix-build 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 zimbatm and 4 | [contributors](https://github.com/zimbatm/h/graphs/contributors) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the 'Software'), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `h` - faster shell navigation of projects 2 | 3 | [![Built with Nix](https://builtwithnix.org/badge.svg)](https://builtwithnix.org) 4 | [![Build Status](https://travis-ci.com/zimbatm/h.svg?branch=master)](https://travis-ci.com/zimbatm/h) 5 | 6 | `h` is a small shell utility that I use every day to jump between projects quickly. It is complimentary to the amazing j 7 | ([autojump](https://github.com/joelthelion/autojump)) project and both help me 8 | improve my workflow. 9 | 10 | `h` is designed to work with a secific filesystem structure where all code is 11 | checked-out in `~/code//`. Eg: `~/code/github.com/zimbatm/h` for 12 | this project. This allows to not have to think about project locality. 13 | 14 | The goal is that `h h` would find and cd into `~/code/github.com/zimbatm/h` if 15 | it exists. When using the `h zimbatm/h` form it would look for the specific 16 | folder or clone the repo from github. In both cases you will end-up changing 17 | directory in the repo that you want to access. This allows to quickly access 18 | existing and new project. 19 | 20 | If projects don't live on github then their full git url can be provided to 21 | clone into `~/code//`. 22 | 23 | ## Usage 24 | 25 | `h ` searches for a project called `` where `` matches 26 | `^\w\.\-$`. The search is done up to 3 levels deep and the longest match is 27 | returned. If a result is found the path is printed on stdout. The 28 | current directory is printed on stdout. 29 | 30 | `h /` looks for a `~/code/github.com//` folder or 31 | clones it from github. The path is output on stdout if the repo exists or has 32 | been cloned successfully. The current directory is printed on stdout 33 | otherwise. 34 | 35 | `h ` looks for a `~/code//` folder or clones it with git. 36 | The path is output on stdout if the repo exists or has been cloned 37 | successfully. The current directory is printed on stdout otherwise. 38 | 39 | ## Installation 40 | 41 | Copy the `h` ruby script to somewhere in the PATH. 42 | 43 | In your `~/.zshrc | ~/.bashrc | ..` add the following line: 44 | 45 | ```bash 46 | eval "$(h --setup ~/code)" 47 | ``` 48 | 49 | This installs something really similar to `alias h='cd "$(h ~/code "$@")"` 50 | where ~/code is the root of all your repositories. Once the shell is reloaded 51 | you can use the above commands. 52 | 53 | ### With nix-darwin 54 | 55 | Add `h` to your flake inputs and include the module: 56 | 57 | ```nix 58 | { 59 | inputs.h.url = "https://flakehub.com/f/zimbatm/h/0.1.35.tar.gz"; 60 | # [ ...snip... ] 61 | 62 | outputs = { nix-darwin, ... } @ inputs: { 63 | darwinConfigurations.default = nix-darwin.lib.darwinSystem { 64 | system = "x86_64-darwin"; 65 | 66 | modules = [ 67 | h.darwinModules.default 68 | ({ pkgs, ... }: { 69 | # ... your configuration ... 70 | }) 71 | ]; 72 | }; 73 | } 74 | } 75 | ``` 76 | 77 | ### With Home Manager 78 | 79 | Add `h` to your flake inputs and include the module: 80 | 81 | ```nix 82 | { 83 | inputs.h.url = "https://flakehub.com/f/zimbatm/h/0.1.35.tar.gz"; 84 | # [ ...snip... ] 85 | 86 | outputs = { home-manager, ... } @ inputs: { 87 | homeConfigurations.default = home-manager.lib.homeManagerConfiguration { 88 | pkgs = nixpkgs.legacyPackages.x86_64-linux; 89 | 90 | modules = [ 91 | h.homeModules.default 92 | ./home.nix 93 | ]; 94 | }; 95 | }; 96 | } 97 | ``` 98 | 99 | **Note:** enable `zsh` or `bash` as well, via `programs.zsh.enable = true;` or `programs.bash.enable = true;`. 100 | 101 | ## See also 102 | 103 | * [autojump](https://github.com/joelthelion/autojump) 104 | * [hub](https://hub.github.com/) 105 | * [direnv](http://direnv.net/) 106 | 107 | ## License 108 | 109 | MIT - (c) 2015 zimbatm and contributors 110 | 111 | 112 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | with pkgs; 3 | 4 | stdenv.mkDerivation { 5 | name = "h"; 6 | 7 | src = ./.; 8 | 9 | buildInputs = [ ruby ]; 10 | 11 | installPhase = '' 12 | mkdir -p $out/bin 13 | cp h $out/bin 14 | cp up $out/bin 15 | ''; 16 | } 17 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1710451336, 6 | "narHash": "sha256-pP86Pcfu3BrAvRO7R64x7hs+GaQrjFes+mEPowCfkxY=", 7 | "rev": "d691274a972b3165335d261cc4671335f5c67de9", 8 | "revCount": 596954, 9 | "type": "tarball", 10 | "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.596954%2Brev-d691274a972b3165335d261cc4671335f5c67de9/018e4219-154b-7a41-b00a-47c7351cce9c/source.tar.gz" 11 | }, 12 | "original": { 13 | "type": "tarball", 14 | "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.0" 15 | } 16 | }, 17 | "root": { 18 | "inputs": { 19 | "nixpkgs": "nixpkgs" 20 | } 21 | } 22 | }, 23 | "root": "root", 24 | "version": 7 25 | } 26 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "faster shell navigation of projects"; 3 | 4 | inputs = { 5 | nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.0"; 6 | }; 7 | 8 | outputs = { self, nixpkgs }: 9 | let 10 | supportedSystems = [ "x86_64-linux" "aarch64-darwin" "x86_64-darwin" "aarch64-linux" ]; 11 | forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { 12 | inherit system; 13 | pkgs = nixpkgs.legacyPackages.${system}; 14 | }); 15 | in 16 | { 17 | packages = forEachSupportedSystem ({ pkgs, ... }: { 18 | default = import ./default.nix { inherit pkgs; }; 19 | }); 20 | 21 | darwinModules.default = { lib, config, pkgs, ... }: 22 | let 23 | h = self.packages.${pkgs.stdenv.system}.default; 24 | in 25 | { 26 | options = { 27 | programs.h.codeRoot = lib.mkOption { 28 | type = lib.types.str; 29 | default = "~/src"; 30 | description = lib.mdDoc '' 31 | Root location for checking out your code. 32 | ''; 33 | }; 34 | }; 35 | config = { 36 | environment.systemPackages = [ h ]; 37 | 38 | environment.extraInit = '' 39 | eval "$(${h}/bin/h --setup ${lib.escapeShellArg config.programs.h.codeRoot})" 40 | ''; 41 | }; 42 | }; 43 | 44 | homeModules.default = { lib, config, pkgs, ... }: 45 | let 46 | h = self.packages.${pkgs.stdenv.system}.default; 47 | in 48 | { 49 | options = { 50 | programs.h.codeRoot = lib.mkOption { 51 | type = lib.types.str; 52 | default = "~/src"; 53 | description = lib.mdDoc '' 54 | Root location for checking out your code. 55 | ''; 56 | }; 57 | }; 58 | config = let 59 | hook = '' 60 | eval "$(${h}/bin/h --setup ${lib.escapeShellArg config.programs.h.codeRoot})" 61 | ''; 62 | in { 63 | home.packages = [ h ]; 64 | 65 | programs.bash.initExtra = hook; 66 | programs.zsh.initExtra = hook; 67 | programs.fish.functions.h = { 68 | body = '' 69 | set _h_dir $(${h}/bin/h --resolve $(path resolve ${config.programs.h.codeRoot}) $argv) 70 | set _h_ret $status 71 | if test "$_h_dir" != "$PWD" 72 | cd "$_h_dir" 73 | end 74 | return $_h_ret 75 | ''; 76 | wraps = "h"; 77 | }; 78 | }; 79 | }; 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /h: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Usage: h 4 | # 5 | # term can be of form [\w-]+ for search 6 | # / for github repos 7 | # url for cloning 8 | 9 | require 'find' 10 | require 'json' 11 | require 'open-uri' 12 | require 'pathname' 13 | require 'uri' 14 | 15 | DEFAULT_CODE_ROOT = ENV['H_CODE_ROOT'] || '~/src' 16 | 17 | def abort(*) 18 | puts Dir.pwd 19 | super 20 | end 21 | 22 | term = ARGV.shift 23 | path = nil 24 | url = nil 25 | 26 | case term 27 | when "--setup" 28 | code_root = Pathname.new(ARGV[0] || DEFAULT_CODE_ROOT).expand_path 29 | puts <<-SH 30 | h() { 31 | _h_dir=$(command h --resolve "#{code_root}" "$@") 32 | _h_ret=$? 33 | [ "$_h_dir" != "$PWD" ] && cd "$_h_dir" 34 | return $_h_ret 35 | } 36 | SH 37 | exit 38 | when "--resolve" 39 | CODE_ROOT = Pathname.new(ARGV.shift) 40 | else 41 | puts "h is not installed" 42 | puts 43 | puts "h needs to be hooked into the shell before it can be used." 44 | puts 45 | 46 | abort "Usage: eval \"$(h --setup [code-root])\"" 47 | end 48 | 49 | term = ARGV.shift 50 | case term 51 | when nil, "-h", "--help" 52 | abort "Usage: h ( | / | ) [git opts]" 53 | when %r[\A([\w\.\-]+)/([\w\.\-]+)\z] # github user/repo 54 | # query the github API to find out the right file case 55 | begin 56 | api_info = JSON.load(URI.open("https://api.github.com/repos/#{$1}/#{$2}").read) 57 | owner = api_info["owner"]["login"] 58 | repo = api_info["name"] 59 | rescue OpenURI::HTTPError 60 | owner = $1 61 | repo = $2 62 | end 63 | 64 | url = "git@github.com:#{owner}/#{repo}.git" 65 | path = CODE_ROOT.join('github.com', owner, repo) 66 | when %r[://] # URL 67 | url = URI.parse(term) 68 | path = CODE_ROOT.join(url.host.downcase, url.path[1..-1]) 69 | abort "Missing url scheme" unless url.scheme 70 | when %r[\Agit(?:ea)?@([^:]+):(.*)] # git url 71 | url = term 72 | path = CODE_ROOT.join($1, $2) 73 | when %r[\A[\w\.\-]+\z] # just search for repo 74 | path_depth = 0 75 | 76 | if term == term.downcase then 77 | # case insentitive search 78 | compare = ->(basename) { basename.downcase == term } 79 | else 80 | compare = ->(basename) { basename == term } 81 | end 82 | 83 | # Find all matches 84 | CODE_ROOT.find do |curpath| 85 | next unless curpath.directory? 86 | 87 | depth = curpath.to_s.sub(CODE_ROOT.to_s, '').split('/').size 88 | 89 | # Select deepest result 90 | if compare.(curpath.basename.to_s) && depth > path_depth 91 | path = curpath 92 | path_depth = depth 93 | end 94 | 95 | # Don't search below 4 96 | Find.prune if depth > 3 97 | end 98 | else 99 | abort "Unknown pattern for #{term}" 100 | end 101 | 102 | abort "#{term} not found" unless path 103 | 104 | # Remove .git to path 105 | path = path.sub_ext('') if path.extname == '.git' 106 | 107 | unless path.directory? 108 | # Keep note of the existing path 109 | parent = path.parent 110 | dir = parent 111 | dir = dir.parent until dir.directory? || dir.root? 112 | # Make sure the parent directory exists 113 | parent.mkpath 114 | unless system( 115 | 'git', 116 | 'clone', 117 | *(ARGV.any? ? ARGV : ['--recursive']), 118 | '--', 119 | url.to_s, 120 | path.to_s, 121 | out: :err, 122 | close_others: true, 123 | ) 124 | # Cleanup the parent directory if created 125 | until parent == dir 126 | parent.rmdir 127 | parent = parent.parent 128 | end 129 | 130 | exit $?.exitstatus 131 | end 132 | end 133 | 134 | puts path 135 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /up: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Usage: up 4 | # 5 | # Move back up to the project root. 6 | # 7 | # Up has a heuristic to detect the project root. 8 | # 9 | # If no project root is found it stays in the current directory 10 | # If the current directory is already a project root it will try to find 11 | # a parent project 12 | 13 | require 'find' 14 | require 'pathname' 15 | require 'uri' 16 | 17 | def abort(*) 18 | puts Dir.pwd 19 | super 20 | end 21 | 22 | case ARGV.shift 23 | when "-h", "--help" 24 | puts "up is not installed" 25 | puts 26 | 27 | abort "Usage: eval \"$(up --setup)\"" 28 | when "--setup" 29 | puts <<-SH 30 | up() { 31 | _up_dir=$(command up "$@") 32 | if [ $? = 0 ]; then 33 | [ "$_up_dir" != "$PWD" ] && cd "$_up_dir" 34 | fi 35 | } 36 | SH 37 | exit 38 | end 39 | 40 | HOME = Pathname.new(ENV['HOME']) if ENV['HOME'] 41 | PWD = ENV['PWD'] ? Pathname.new(ENV['PWD']) : Pathname.pwd 42 | 43 | def project_root?(dir) 44 | (dir + '.git').directory? || 45 | (dir + '.hg').directory? || 46 | (dir + '.envrc').file? || 47 | (dir + 'Gemfile').file? || 48 | (dir.to_s == ENV['DIRENV_DIR'].to_s[1..-1]) || 49 | false 50 | end 51 | 52 | def should_stop?(dir) 53 | dir.root? || dir == HOME 54 | end 55 | 56 | def find_project_root 57 | dir = PWD 58 | 59 | # Search parent projects if already in a 60 | # sub-project 61 | dir = dir.parent if project_root?(dir) 62 | 63 | while !should_stop?(dir) 64 | return dir if project_root?(dir) 65 | dir = dir.parent 66 | end 67 | 68 | PWD 69 | end 70 | 71 | puts find_project_root 72 | --------------------------------------------------------------------------------