├── .envrc ├── .gitignore ├── Cargo.toml ├── flake.nix ├── LICENSE ├── flake.lock ├── README.md ├── src └── main.rs └── Cargo.lock /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /result 3 | .direnv 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "open-project" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | clap = { version = "4.0", features = ["derive"] } 8 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A flake for the open-project utility"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let 10 | pkgs = import nixpkgs { inherit system; }; 11 | in { 12 | packages.default = pkgs.rustPlatform.buildRustPackage { 13 | pname = "open-project"; 14 | version = "0.1.0"; 15 | src = ./.; 16 | 17 | cargoHash = "sha256-cxy+UZvJNOpfIEZ59d+pT6jbHEpAeOOUjkCaVuT7NBQ="; 18 | }; 19 | 20 | devShell = pkgs.mkShell { 21 | nativeBuildInputs = [ 22 | pkgs.cargo 23 | pkgs.rustc 24 | ]; 25 | 26 | shellHook = '' 27 | echo "Welcome to the open-project development shell!" 28 | ''; 29 | }; 30 | }); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Cor Pruijs 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. 22 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1735914541, 24 | "narHash": "sha256-bwzRjjf+owmf9xTYSKFv4JXmUbWFlqWt7jeU95SmfVw=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "561b254314bd4b7e18b255f50470cd8c1a901f45", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "repo": "nixpkgs", 33 | "type": "github" 34 | } 35 | }, 36 | "root": { 37 | "inputs": { 38 | "flake-utils": "flake-utils", 39 | "nixpkgs": "nixpkgs" 40 | } 41 | }, 42 | "systems": { 43 | "locked": { 44 | "lastModified": 1681028828, 45 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 46 | "owner": "nix-systems", 47 | "repo": "default", 48 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "nix-systems", 53 | "repo": "default", 54 | "type": "github" 55 | } 56 | } 57 | }, 58 | "root": "root", 59 | "version": 7 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # open-project 2 | 3 | `open-project` is a command-line utility written in Rust to streamline management of local clones of GitHub repos and opening them in your preferred terminal multiplexer (tmux or Zellij). 4 | 5 | Just type `open-project user repo` to automatically clone the repo if necessary and open it in tmux (default) or Zellij. 6 | 7 | ## Install 8 | 9 | Make sure you have either **tmux** or **Zellij** installed: 10 | - [Install tmux](https://github.com/tmux/tmux/wiki/Installing) 11 | - [Install Zellij](https://zellij.dev/documentation/installation.html) 12 | 13 | Build `open-project` with either `nix` or `cargo` and add the binary to your $PATH. 14 | 15 | #### Nix 16 | 17 | ```bash 18 | nix build github:cor/open-project 19 | ``` 20 | 21 | #### Cargo 22 | 23 | ```bash 24 | git clone https://github.com/cor/open-project 25 | cd open-project 26 | cargo build --release 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```bash 32 | open-project [OPTIONS] 33 | ``` 34 | 35 | ### Options 36 | 37 | - `-m, --multiplexer `: Choose terminal multiplexer (tmux or zellij) [default: tmux] 38 | 39 | ### Environment Variables 40 | 41 | - `TERMINAL_MULTIPLEXER`: Set default multiplexer (overrides CLI default, but CLI flag takes precedence) 42 | 43 | ### Examples 44 | 45 | ```bash 46 | # Use default (tmux) 47 | open-project alice my-repo 48 | 49 | # Use zellij 50 | open-project -m zellij alice my-repo 51 | 52 | # Use environment variable 53 | TERMINAL_MULTIPLEXER=zellij open-project alice my-repo 54 | ``` 55 | 56 | This will: 57 | 58 | 1. Check for a directory `$HOME/dev/alice` and prompt to create it if missing. 59 | 2. Check for the repository `my-repo` under the user's GitHub and clone it if not present. 60 | 3. Attach to or create a session in your chosen multiplexer. 61 | 62 | ## Contributing 63 | 64 | Contributions are welcome! Feel free to open issues or submit pull requests to improve this tool. 65 | 66 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::{self, Write}; 3 | use std::path::PathBuf; 4 | use std::process::Command; 5 | use clap::Parser; 6 | 7 | #[derive(Parser)] 8 | struct Cli { 9 | username: String, 10 | projectname: String, 11 | /// Terminal multiplexer to use (tmux or zellij) 12 | #[clap(short, long, default_value = "tmux")] 13 | multiplexer: String, 14 | } 15 | 16 | fn main() { 17 | let args = Cli::parse(); 18 | let username = args.username; 19 | let projectname = args.projectname; 20 | 21 | // Get multiplexer from CLI arg or environment variable 22 | let multiplexer = if args.multiplexer != "tmux" { 23 | args.multiplexer 24 | } else { 25 | std::env::var("TERMINAL_MULTIPLEXER").unwrap_or_else(|_| args.multiplexer) 26 | }; 27 | 28 | let home_dir = std::env::var("HOME").expect("Could not get HOME environment variable"); 29 | let base_dir = PathBuf::from(format!("{}/dev", home_dir)); 30 | let user_dir = base_dir.join(&username); 31 | let project_dir = user_dir.join(&projectname); 32 | 33 | // Step 2: Check if the username directory exists 34 | if !user_dir.exists() { 35 | print!("Directory for user '{}' does not exist. Create it? (y/n): ", username); 36 | io::stdout().flush().unwrap(); 37 | let mut response = String::new(); 38 | io::stdin().read_line(&mut response).unwrap(); 39 | if response.trim().eq_ignore_ascii_case("y") { 40 | fs::create_dir_all(&user_dir).expect("Failed to create user directory"); 41 | } else { 42 | println!("Exiting."); 43 | return; 44 | } 45 | } 46 | 47 | // Step 3: Change into username directory 48 | std::env::set_current_dir(&user_dir).expect("Failed to change directory to user"); 49 | 50 | // Step 4: Check if the project directory exists 51 | if !project_dir.exists() { 52 | print!("Project '{}' does not exist. Clone it from GitHub? (y/n): ", projectname); 53 | io::stdout().flush().unwrap(); 54 | let mut response = String::new(); 55 | io::stdin().read_line(&mut response).unwrap(); 56 | if response.trim().eq_ignore_ascii_case("y") { 57 | let repo_url = format!("{}/{}", username, projectname); 58 | let output = Command::new("gh") 59 | .arg("repo") 60 | .arg("clone") 61 | .arg(&repo_url) 62 | .status() 63 | .expect("Failed to execute 'gh repo clone'"); 64 | 65 | if !output.success() { 66 | println!("Failed to clone the repository."); 67 | return; 68 | } 69 | } else { 70 | println!("Exiting."); 71 | return; 72 | } 73 | } 74 | 75 | // Step 5: Change into the project directory 76 | std::env::set_current_dir(&project_dir).expect("Failed to change directory to project"); 77 | 78 | // Step 6: Attach to a (new) session based on multiplexer choice 79 | match multiplexer.as_str() { 80 | "tmux" => { 81 | let session_name = format!("{}-{}", username, projectname); 82 | 83 | // Check if session exists 84 | let check_session = Command::new("tmux") 85 | .arg("has-session") 86 | .arg("-t") 87 | .arg(&session_name) 88 | .status(); 89 | 90 | match check_session { 91 | Ok(status) if status.success() => { 92 | // Session exists, attach to it 93 | Command::new("tmux") 94 | .arg("attach-session") 95 | .arg("-t") 96 | .arg(&session_name) 97 | .status() 98 | .expect("Failed to attach to tmux session"); 99 | } 100 | _ => { 101 | // Session doesn't exist, create new one 102 | Command::new("tmux") 103 | .arg("new-session") 104 | .arg("-s") 105 | .arg(&session_name) 106 | .status() 107 | .expect("Failed to create new tmux session"); 108 | } 109 | } 110 | } 111 | "zellij" => { 112 | let session_name = format!("{} {}", username, projectname); 113 | Command::new("zellij") 114 | .arg("attach") 115 | .arg("-c") 116 | .arg(&session_name) 117 | .status() 118 | .expect("Failed to attach to Zellij session"); 119 | } 120 | _ => { 121 | println!("Unsupported multiplexer: {}. Use 'tmux' or 'zellij'.", multiplexer); 122 | } 123 | } 124 | } 125 | 126 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.6" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys", 52 | ] 53 | 54 | [[package]] 55 | name = "clap" 56 | version = "4.5.23" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" 59 | dependencies = [ 60 | "clap_builder", 61 | "clap_derive", 62 | ] 63 | 64 | [[package]] 65 | name = "clap_builder" 66 | version = "4.5.23" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" 69 | dependencies = [ 70 | "anstream", 71 | "anstyle", 72 | "clap_lex", 73 | "strsim", 74 | ] 75 | 76 | [[package]] 77 | name = "clap_derive" 78 | version = "4.5.18" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 81 | dependencies = [ 82 | "heck", 83 | "proc-macro2", 84 | "quote", 85 | "syn", 86 | ] 87 | 88 | [[package]] 89 | name = "clap_lex" 90 | version = "0.7.4" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 93 | 94 | [[package]] 95 | name = "colorchoice" 96 | version = "1.0.3" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 99 | 100 | [[package]] 101 | name = "heck" 102 | version = "0.5.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 105 | 106 | [[package]] 107 | name = "is_terminal_polyfill" 108 | version = "1.70.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 111 | 112 | [[package]] 113 | name = "open-project" 114 | version = "0.1.0" 115 | dependencies = [ 116 | "clap", 117 | ] 118 | 119 | [[package]] 120 | name = "proc-macro2" 121 | version = "1.0.92" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 124 | dependencies = [ 125 | "unicode-ident", 126 | ] 127 | 128 | [[package]] 129 | name = "quote" 130 | version = "1.0.38" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 133 | dependencies = [ 134 | "proc-macro2", 135 | ] 136 | 137 | [[package]] 138 | name = "strsim" 139 | version = "0.11.1" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 142 | 143 | [[package]] 144 | name = "syn" 145 | version = "2.0.94" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" 148 | dependencies = [ 149 | "proc-macro2", 150 | "quote", 151 | "unicode-ident", 152 | ] 153 | 154 | [[package]] 155 | name = "unicode-ident" 156 | version = "1.0.14" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 159 | 160 | [[package]] 161 | name = "utf8parse" 162 | version = "0.2.2" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 165 | 166 | [[package]] 167 | name = "windows-sys" 168 | version = "0.59.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 171 | dependencies = [ 172 | "windows-targets", 173 | ] 174 | 175 | [[package]] 176 | name = "windows-targets" 177 | version = "0.52.6" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 180 | dependencies = [ 181 | "windows_aarch64_gnullvm", 182 | "windows_aarch64_msvc", 183 | "windows_i686_gnu", 184 | "windows_i686_gnullvm", 185 | "windows_i686_msvc", 186 | "windows_x86_64_gnu", 187 | "windows_x86_64_gnullvm", 188 | "windows_x86_64_msvc", 189 | ] 190 | 191 | [[package]] 192 | name = "windows_aarch64_gnullvm" 193 | version = "0.52.6" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 196 | 197 | [[package]] 198 | name = "windows_aarch64_msvc" 199 | version = "0.52.6" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 202 | 203 | [[package]] 204 | name = "windows_i686_gnu" 205 | version = "0.52.6" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 208 | 209 | [[package]] 210 | name = "windows_i686_gnullvm" 211 | version = "0.52.6" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 214 | 215 | [[package]] 216 | name = "windows_i686_msvc" 217 | version = "0.52.6" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 220 | 221 | [[package]] 222 | name = "windows_x86_64_gnu" 223 | version = "0.52.6" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 226 | 227 | [[package]] 228 | name = "windows_x86_64_gnullvm" 229 | version = "0.52.6" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 232 | 233 | [[package]] 234 | name = "windows_x86_64_msvc" 235 | version = "0.52.6" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 238 | --------------------------------------------------------------------------------