├── .gitignore ├── screenshot.png ├── meow.py ├── get_all_repos.py ├── cache.py ├── kill.py ├── github.py ├── CHANGELOG.md ├── README.md └── projects.py /.gitignore: -------------------------------------------------------------------------------- 1 | cache_* 2 | !cache.py 3 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taylorzr/kitty-meow/HEAD/screenshot.png -------------------------------------------------------------------------------- /meow.py: -------------------------------------------------------------------------------- 1 | def binds_and_header(mapping, emoji="🐈"): 2 | bind_parts = [] 3 | header_parts = [] 4 | for keybind, (prompt, command) in mapping.items(): 5 | header_parts.append(f"{keybind}: {prompt}") 6 | bind = f"{keybind}:change-prompt({emoji} {prompt} > )+reload({command})" 7 | if "'" in bind: 8 | raise ValueError("can't have ' in bind") 9 | bind_parts.append(bind) 10 | binds = ','.join(bind_parts) 11 | header = ' | '.join(header_parts) 12 | return binds, header 13 | 14 | -------------------------------------------------------------------------------- /get_all_repos.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | 4 | import github 5 | 6 | parser = argparse.ArgumentParser(description="meow") 7 | 8 | parser.add_argument( 9 | "--org", 10 | dest="orgs", 11 | action="append", 12 | default=[], 13 | help="look for repos in these github orgs", 14 | ) 15 | 16 | parser.add_argument( 17 | "--user", 18 | dest="users", 19 | action="append", 20 | default=[], 21 | help="look for repos for these github users", 22 | ) 23 | 24 | 25 | def get_repos(login, type): 26 | cache = f"{os.path.expanduser('~')}/.config/kitty/meow/cache_{login}" 27 | try: 28 | with open(cache, "r") as file: 29 | print(file.read()) 30 | except FileNotFoundError: 31 | github.get_repos(login, type) 32 | 33 | 34 | if __name__ == "__main__": 35 | opts = parser.parse_args() 36 | for user in opts.users: 37 | get_repos(login=user, type="user") 38 | for org in opts.orgs: 39 | get_repos(login=org, type="organization") 40 | -------------------------------------------------------------------------------- /cache.py: -------------------------------------------------------------------------------- 1 | import github 2 | import argparse 3 | import os 4 | import sys 5 | from typing import List 6 | 7 | parser = argparse.ArgumentParser(description="meow") 8 | 9 | parser.add_argument( 10 | "--org", 11 | dest="orgs", 12 | action="append", 13 | default=[], 14 | help="look for repos in these github orgs", 15 | ) 16 | 17 | parser.add_argument( 18 | "--user", 19 | dest="users", 20 | action="append", 21 | default=[], 22 | help="look for repos for these github users", 23 | ) 24 | 25 | 26 | def main(args: List[str]) -> str: 27 | opts = parser.parse_args(args[1:]) 28 | 29 | for user in opts.users: 30 | cache = f"{os.path.expanduser('~')}/.config/kitty/meow/cache_{user}" 31 | repos = github.get_repos(user, type="user") 32 | with open(cache, "w") as file: 33 | file.write("\n".join(repos)) 34 | 35 | for org in opts.orgs: 36 | cache = f"{os.path.expanduser('~')}/.config/kitty/meow/cache_{org}" 37 | repos = github.get_repos(org, type="organization") 38 | with open(cache, "w") as file: 39 | file.write("\n".join(repos)) 40 | 41 | 42 | if __name__ == "__main__": 43 | main(sys.argv) 44 | -------------------------------------------------------------------------------- /kill.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | from datetime import datetime, timedelta 5 | from typing import List 6 | 7 | from kitty.boss import Boss 8 | 9 | import meow 10 | 11 | 12 | def main(args: List[str]) -> str: 13 | last_views = {} 14 | 15 | with open(f"{os.path.expanduser('~')}/.config/kitty/meow/history") as history: 16 | for line in history: 17 | parts = line.strip().split(" ") 18 | if len(parts) != 2: 19 | print( 20 | f"expected history to have 2 parts (project last-time), but found {len(parts)}" 21 | ) 22 | continue 23 | last_views[parts[0]] = parts[1] 24 | 25 | # TODO: make time configurable 26 | cutoff = datetime.now() - timedelta(days=3) 27 | 28 | stuff = subprocess.run( 29 | ["kitty", "@", "ls"], capture_output=True, text=True 30 | ).stdout.strip("\n") 31 | data = json.loads(stuff) 32 | 33 | all_tabs, old_tabs = [], [] 34 | 35 | for tab in data[0]["tabs"]: 36 | title = tab["title"] 37 | all_tabs.append(title) 38 | 39 | last_view = last_views.get(title, None) 40 | if not last_view or datetime.fromisoformat(last_view) < cutoff: 41 | old_tabs.append(title) 42 | 43 | bin_path = os.getenv("BIN_PATH", "") 44 | 45 | binds, header = meow.binds_and_header( 46 | { 47 | "ctrl-o": ( 48 | "old", 49 | 'printf "{0}"'.format("\n".join(old_tabs)), 50 | ), 51 | "ctrl-a": ( 52 | "any", 53 | 'printf "{0}"'.format("\n".join(all_tabs)), 54 | ), 55 | }, emoji="🐈💀" 56 | ) 57 | args = [ 58 | f"{bin_path}fzf", 59 | "--multi", 60 | "--reverse", 61 | f"--header={header}", 62 | f"--bind={binds}", 63 | f"--prompt=🐈💀 kill > ", 64 | ] 65 | p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 66 | out = p.communicate(input="\n".join(all_tabs).encode())[0] 67 | selections = out.decode().strip() 68 | 69 | if selections == "": 70 | return 71 | 72 | for tab in selections.split("\n"): 73 | subprocess.run(["kitty", "@", "close-tab", "--match", f"title:{tab}"]) 74 | 75 | 76 | def handle_result( 77 | args: List[str], answer: str, target_window_id: int, boss: Boss 78 | ) -> None: 79 | pass 80 | -------------------------------------------------------------------------------- /github.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from sys import stdout 4 | from urllib.request import Request, urlopen 5 | 6 | token = os.getenv("GITHUB_TOKEN") 7 | 8 | org_query = """ 9 | query($login: String!, $cursor: String) { 10 | organization(login: $login) { 11 | repositories(first: 100, after: $cursor) { 12 | nodes { 13 | name 14 | sshUrl 15 | } 16 | pageInfo { 17 | endCursor 18 | startCursor 19 | hasNextPage 20 | } 21 | } 22 | } 23 | } 24 | """ 25 | 26 | user_query = """ 27 | query($login: String!, $cursor: String) { 28 | user(login: $login) { 29 | repositories(first: 100, after: $cursor) { 30 | nodes { 31 | name 32 | sshUrl 33 | } 34 | pageInfo { 35 | endCursor 36 | startCursor 37 | hasNextPage 38 | } 39 | } 40 | } 41 | } 42 | """ 43 | 44 | 45 | def run_query(query, login, cursor=None): 46 | data = {"query": query, "variables": {"login": login, "cursor": cursor}} 47 | headers = {"Authorization": f"Bearer {token}"} 48 | request = Request( 49 | "https://api.github.com/graphql", json.dumps(data).encode("utf-8"), headers 50 | ) 51 | with urlopen(request) as response: 52 | code = response.code 53 | body = response.read() 54 | 55 | if code == 200: 56 | data = json.loads(body) 57 | if "errors" in data: 58 | raise Exception(f'Query returned errors:\n{data["errors"]}') 59 | return data 60 | else: 61 | raise Exception( 62 | "Query failed to run by returning code of {}. {}".format(code, query) 63 | ) 64 | 65 | 66 | def get_repos(login, type): 67 | if type == "organization": 68 | query = org_query 69 | elif type == "user": 70 | query = user_query 71 | else: 72 | raise RuntimeError( 73 | f"expected type to be `user` or `organization`, but got `{type}`" 74 | ) 75 | 76 | cursor = None 77 | repos = [] 78 | 79 | while True: 80 | result = run_query(query, login, cursor) 81 | 82 | for repo in result["data"][type]["repositories"]["nodes"]: 83 | output = f'{repo["name"]} {repo["sshUrl"]}' 84 | repos.append(output) 85 | print(output) 86 | stdout.flush() # so that results show in fzf sooner 87 | 88 | pageInfo = result["data"][type]["repositories"]["pageInfo"] 89 | if not pageInfo["hasNextPage"]: 90 | break 91 | cursor = pageInfo["endCursor"] 92 | 93 | return repos 94 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.5.1] - 2025-2-1 11 | 12 | ### Fixed 13 | 14 | - history path 15 | - tac path on mac 16 | 17 | ## [0.5.0] - 2025-2-1 18 | 19 | ### Changed 20 | 21 | - use ctrl- for all fzf bindings 22 | - cache_all_repos.py renamed to cache.py 23 | - instead of `map ctrl+shift+g kitten meow/cache_all_repos.py ...` 24 | - use `map ctrl+shift+g kitten meow/cache.py ...` 25 | - kill_old_projects.py renamed to kill.py 26 | - instead of `map ctrl+shift+x kitten meow/kill_old_projects.py` 27 | - use `map ctrl+shift+x kitten meow/kill.py` 28 | - kill now defaults to any project, ctrl-o to kill old projects 29 | 30 | ## [0.4.0] - 2024-4-20 31 | 32 | ### Changed 33 | 34 | - combined python scripts for load & new into single file 35 | - instead of 36 | - `ctrl+p kitten meow/load_project.py ...` 37 | - `ctrl+shift+n kitten meow/load_project.py ...` 38 | - use 39 | - `ctrl+p kitten meow/projects.py load ...` 40 | - `ctrl+shift+n kitten meow/projects.py new ...` 41 | 42 | ### Added 43 | 44 | - load & new projects support multiple selections 45 | - fix 2nd window path on mac 46 | 47 | ## [0.3.0] - 2023-11-11 48 | 49 | ### Changed 50 | 51 | - loads env EDITOR now, not always vim 52 | 53 | ### Added 54 | 55 | - keybinds listed in fzf header 56 | 57 | ## [0.2.0] - 2023-7-19 58 | 59 | ### Added 60 | 61 | - new project command. load projects outside your user/org using ctrl+shift+n 62 | 63 | ## [0.1.1] - 2023-3-31 64 | 65 | ### Fixed 66 | 67 | - can run cache_all_repos.py directly for testing 68 | 69 | ## [0.1.0] - 2023-3-11 70 | 71 | ### Added 72 | 73 | - repo caching 74 | - user repo support, org is now optional, and both can be repeated 75 | 76 | ## [0.0.4] - 2023-3-6 77 | 78 | ### Changed 79 | 80 | - default project list now combines tabs and local projects 81 | 82 | ## [0.0.3] - 2023-2-1 83 | 84 | ### Fixed 85 | 86 | - switching or loading a project now matches exact project name instead of a substring 87 | 88 | ## [0.0.2] - 2023-01-18 89 | 90 | ### Added 91 | 92 | - Added project history tracking. Switching project stores last view in history file. Ctrl-x lists 93 | and closes projects that are old. 94 | 95 | ## [0.0.1] - 2023-01-18 96 | 97 | ### Added - 2022-11-21 98 | 99 | - Initial release 100 | 101 | [unreleased]: https://github.com/taylorzr/meow/compare/v0.4.0...HEAD 102 | [0.4.0]: https://github.com/taylorzr/meow/compare/v0.3.0...v0.4.0 103 | [0.3.0]: https://github.com/taylorzr/meow/compare/v0.2.0...v0.3.0 104 | [0.2.0]: https://github.com/taylorzr/meow/compare/v0.1.1...v0.2.0 105 | [0.1.1]: https://github.com/taylorzr/meow/compare/v0.1.0...v0.1.1 106 | [0.1.0]: https://github.com/taylorzr/meow/compare/v0.0.4...v0.1.0 107 | [0.0.4]: https://github.com/taylorzr/meow/compare/v0.0.3...v0.0.4 108 | [0.0.3]: https://github.com/taylorzr/meow/releases/tag/v0.0.2..v0.0.3 109 | [0.0.2]: https://github.com/taylorzr/meow/releases/tag/v0.0.1..v0.0.2 110 | [0.0.1]: https://github.com/taylorzr/meow/releases/tag/v0.0.1 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kitty-Meow 2 | 3 | Meow is a kitty terminal extension for working with projects, where each kitty tab is a different 4 | project. It allows you to fuzzy switch between projects, and load them either from local directories or github. 5 | 6 | If you've used tmux, this is similar to switching between sessions, but allows you to 7 | create new sessions as well. 8 | 9 | ![Meow Screenshot](screenshot.png) 10 | 11 | ## Usage 12 | 13 | [1-minute demo](https://www.youtube.com/watch?v=Qm8Xl4GAylI) 14 | 15 | Call your project mapping, e.g. ctrl-space, and hit enter to select. Initially, tabs & local projects 16 | are listed, but you can show remote, local project only, or tabs only. 17 | 18 | On select 19 | 20 | - if the project is already in a tab, meow switches to that tab 21 | - if the project is a local dir, meow creates a new tab 22 | - if the project is github, meow clones to the first --dir, and creates a new tab 23 | 24 | ## Installation 25 | 26 | ```sh 27 | git clone git@github.com:taylorzr/kitty-meow.git ~/.config/kitty/meow 28 | ``` 29 | 30 | Requires [fzf](https://github.com/junegunn/fzf/) and tac (provided by coreutils). 31 | 32 | ## Getting Started 33 | 34 | You'll need to: 35 | 36 | - create mappings 37 | - set your github token as env 38 | 39 | For example: 40 | 41 | ```conf 42 | # ~/.config/kitty/kitty.conf 43 | 44 | env GITHUB_TOKEN= 45 | env BIN_PATH=/opt/homebrew/bin/ # probably only needed on macs 46 | 47 | map ctrl+space kitten meow/projects.py load --dir $HOME/code/ --dir $HOME --dir $HOME/.config/kitty/meow --user taylorzr 48 | map ctrl+- goto_tab -1 49 | map ctrl+shift+n kitten meow/projects.py new --dir $HOME/code/ 50 | map ctrl+shift+g kitten meow/cache.py --org AquaTeenHungerForce 51 | map ctrl+shift+x kitten meow/kill.py 52 | ``` 53 | 54 | ## Kitty Mappings 55 | 56 | #### Loading projects 57 | 58 | Create a mapping for loading projects. The pattern is: 59 | 60 | ```conf 61 | # ~/.config/kitty/kitty.conf 62 | 63 | map ctrl+p kitten meow/project.py load --dir $HOME/code/ --user --org 64 | ``` 65 | 66 | --dir can be provided multiple times. 67 | 68 | - when a dir ends in /, meow shows all it's subdirs 69 | - otherwise, meow only shows that specific dir 70 | - remote repos are cloned into the first --dir 71 | 72 | For example, I use: 73 | 74 | ```conf 75 | # ~/.config/kitty/kitty.conf 76 | 77 | map ctrl+p kitten meow/project.py load --dir $HOME/code/ --dir $HOME --dir $HOME/.config/kitty/meow --org my_cool_org 78 | ``` 79 | 80 | On mac, paths are goofy. You proabably need to set env BIN_PATH as well. This should be the dir 81 | containing and fzf. 82 | 83 | ```conf 84 | # ~/.config/kitty/kitty.conf 85 | 86 | env BIN_PATH=/opt/homebrew/bin/ 87 | ``` 88 | 89 | #### Caching github repositories 90 | 91 | If you work in an org with lots of repos, loading remote projects can be slow. You can create a 92 | binding that will cache all the repos for orgs. This is a manual process, just run it whenever you 93 | need to update the list of projects for an org. 94 | 95 | ```conf 96 | map ctrl+shift+g kitten meow/cache.py --org my_cool_org 97 | ``` 98 | 99 | Just like the projects.py load mapping, you can specify multiple users and orgs in your cache mapping. 100 | You might want these to be different than users and orgs in your projects.py load mapping, because an 101 | org might have lots of repos, but your user just a few. Any uncached users/orgs repos will be 102 | loaded from github on every call to projects.py load. And the cache never expires, you must call 103 | cache.py to refresh it. 104 | 105 | ## Github Auth 106 | 107 | You need to create a github token, and set it as env GITHUB_TOKEN. Because I commit kitty.conf to my 108 | dotfiles, I put any secrets in an extra conf file: 109 | 110 | ```conf 111 | # ~/.config/kitty/kitty.conf 112 | 113 | include ./dont_commit_me.conf 114 | ``` 115 | 116 | ```conf 117 | # ~/.config/kitty/dont_commit_me.conf 118 | 119 | env GITHUB_TOKEN= 120 | ``` 121 | 122 | You need to put env in your kitty config, not .zshrc. More about that [here](https://sw.kovidgoyal.net/kitty/faq/#things-behave-differently-when-running-kitty-from-system-launcher-vs-from-another-terminal) 123 | 124 | ## TODO 125 | 126 | - configurable fzf bindings 127 | - selectable dir to clone to? 128 | - some people might use 1 dir for work and one for personal? 129 | - maybe use flags like --login=user=taylorzr --login=org=my_cool_org 130 | - combine the scripts into one cli with subcommands 131 | - we could then have a fzf binding for loading new projects from the normal project selection 132 | - caching all repos should remove unknown files, e.g. i stop caching taylorzr, i need to delete cache_taylorzr 133 | -------------------------------------------------------------------------------- /projects.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | import re 5 | import subprocess 6 | from datetime import datetime 7 | from typing import List 8 | 9 | from kitty.boss import Boss 10 | 11 | import meow 12 | 13 | parser = argparse.ArgumentParser(description="meow") 14 | parser.add_argument("command", nargs="?", default="load") 15 | parser.add_argument( 16 | "--dir", 17 | dest="dirs", 18 | action="append", 19 | default=[], 20 | help="directories to find projects", 21 | ) 22 | parser.add_argument( 23 | "--org", 24 | dest="orgs", 25 | action="append", 26 | default=[], 27 | help="look for repos in these github orgs", 28 | ) 29 | parser.add_argument( 30 | "--user", 31 | dest="users", 32 | action="append", 33 | default=[], 34 | help="look for repos for these github users", 35 | ) 36 | 37 | 38 | def new_main(args, opts): 39 | try: 40 | url = input("🐈 new (name or github url): ") 41 | return url 42 | except KeyboardInterrupt: 43 | return "" 44 | 45 | 46 | def new_handler(args: List[str], answer: str, target_window_id: int, boss: Boss): 47 | opts = parser.parse_args(args[1:]) 48 | 49 | # This is the dir we clone repos into, for me it's not a big deal if they get cloned to the 50 | # first dir. But some people might want to pick which dir to clone to? How could that be 51 | # supported? 52 | projects_root = opts.dirs[0] 53 | 54 | if not answer: 55 | return 56 | elif "/" in answer: 57 | # Note: This is an attempt to see if the answer is a git url or not, e.g. 58 | # - git@github.com:taylorzr/kitty-meow.git 59 | # - https://github.com/taylorzr/kitty-meow.git 60 | github_url = answer 61 | dir = re.split("[/.]", github_url)[2] 62 | print(f"cloning into {dir}...") 63 | path = f"{projects_root}/{dir}" 64 | subprocess.run(["git", "clone", github_url, path]) 65 | else: 66 | new_local = answer 67 | dir = new_local 68 | path = f"{projects_root}/{dir}" 69 | os.makedirs(path, exist_ok=True) 70 | 71 | load_project(boss, path, dir) 72 | 73 | 74 | def load_main(args, opts): 75 | # FIXME: How to call boss in the main function? 76 | # data = boss.call_remote_control(None, ("ls",)) 77 | kitty_ls = json.loads( 78 | subprocess.run( 79 | ["kitty", "@", "ls"], capture_output=True, text=True 80 | ).stdout.strip("\n") 81 | ) 82 | 83 | tabs = [tab["title"] for tab in kitty_ls[0]["tabs"]] 84 | tabs_and_projects = [tab["title"] for tab in kitty_ls[0]["tabs"]] 85 | projects = [] 86 | 87 | for dir in opts.dirs: 88 | if dir.endswith("/"): 89 | for f in os.scandir(dir): 90 | if f.is_dir(): 91 | name = os.path.basename(f.path) 92 | pretty_path = f.path.replace(os.path.expanduser("~"), "~", 1) 93 | projects.append(pretty_path) 94 | if name not in tabs_and_projects: 95 | tabs_and_projects.append(pretty_path) 96 | else: 97 | name = os.path.basename(dir) 98 | projects.append(dir) 99 | if name not in tabs_and_projects: 100 | tabs_and_projects.append(dir) 101 | 102 | bin_path = os.getenv("BIN_PATH", "") 103 | 104 | flags = [] 105 | for org in opts.orgs: 106 | flags.append(f"--org {org}") 107 | for user in opts.users: 108 | flags.append(f"--user {user}") 109 | 110 | # NOTE: don't use 111 | # - ctrl-p -> fzf previous item in list 112 | # - ctrl-n -> fzf next item in list 113 | binds, header = meow.binds_and_header( 114 | { 115 | "ctrl-t": ( 116 | "tabs", 117 | 'printf "{0}"'.format("\n".join(tabs)), 118 | ), 119 | "ctrl-o": ( 120 | "local", 121 | 'printf "{0}"'.format("\n".join(projects)), 122 | ), 123 | "ctrl-r": ( 124 | "remote", 125 | f"{bin_path}python3 ~/.config/kitty/meow/get_all_repos.py {' '.join(flags)}", 126 | ), 127 | "ctrl-i": ( 128 | "history", 129 | # TODO: make a version that shows uniqueness? 130 | f"{bin_path}tac ~/.config/kitty/meow/history", 131 | ), 132 | "ctrl-a": ( 133 | "tabs&projects", 134 | 'printf "{0}"'.format("\n".join(tabs_and_projects)), 135 | ), 136 | } 137 | ) 138 | 139 | args = [ 140 | f"{bin_path}fzf", 141 | "--multi", 142 | "--reverse", 143 | f"--header={header}", 144 | f"--bind={binds}", 145 | "--prompt=🐈 meow > ", 146 | ] 147 | p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 148 | out = p.communicate(input="\n".join(tabs_and_projects).encode())[0] 149 | output = out.decode().strip() 150 | 151 | # from kittens.tui.loop import debug 152 | # debug(output) 153 | 154 | if output == "": 155 | return [] 156 | 157 | return output.split("\n") 158 | 159 | 160 | def load_project(boss, path, dir): 161 | with open(f"{os.path.expanduser('~')}/.config/kitty/meow/history", "a") as history: 162 | history.write(f"{dir} {datetime.now().isoformat()}\n") 163 | history.close() 164 | 165 | kitty_ls = json.loads(boss.call_remote_control(None, ("ls",))) 166 | for tab in kitty_ls[0]["tabs"]: 167 | if tab["title"] == dir: 168 | boss.call_remote_control(None, ("focus-tab", "--match", f"title:^{dir}$")) 169 | return 170 | 171 | window_id = boss.call_remote_control( 172 | None, 173 | ( 174 | "launch", 175 | "--type", 176 | "tab", 177 | "--tab-title", 178 | dir, 179 | "--cwd", 180 | path, 181 | ), 182 | ) 183 | 184 | parent_window = boss.window_id_map.get(int(window_id)) 185 | 186 | # start editor and another window 187 | boss.call_remote_control(parent_window, ("send-text", "${EDITOR:-vim}\n")) 188 | boss.call_remote_control( 189 | parent_window, 190 | ("launch", "--type", "window", "--dont-take-focus", "--cwd", path), 191 | ) 192 | 193 | 194 | def load_handler(args: List[str], answer: str, target_window_id: int, boss: Boss): 195 | opts = parser.parse_args(args[1:]) 196 | 197 | # This is the dir we clone repos into, for me it's not a big deal if they get cloned to the 198 | # first dir. But some people might want to pick which dir to clone to? How could that be 199 | # supported? 200 | projects_root = opts.dirs[0] 201 | 202 | if not answer: 203 | return 204 | 205 | for selection in answer: 206 | # NOTE: selection can be a variety of patterns 207 | # - meow 208 | # - ~/.config/kitty/meow/ 209 | # - kitty-meow git@github.com:taylorzr/kitty-meow.git 210 | # - meow 2023-03-23T20:52:21.841221 211 | path, *rest = selection.split() 212 | dir = os.path.basename(path) 213 | 214 | if len(rest) == 1: 215 | ssh_url = rest[0] 216 | print(f"cloning into {dir}...") 217 | path = f"{projects_root}/{dir}" 218 | subprocess.run(["git", "clone", ssh_url, path]) 219 | # TODO: handle error, like unset sso on ssh key and try this 220 | elif len(rest) != 0: 221 | print("something bad happenend :(") 222 | 223 | load_project(boss, path, dir) 224 | 225 | 226 | def main(args: List[str]) -> str: 227 | opts = parser.parse_args(args[1:]) 228 | 229 | if opts.command == "load": 230 | return load_main(args, opts) 231 | elif opts.command == "new": 232 | return new_main(args, opts) 233 | 234 | 235 | def handle_result( 236 | args: List[str], answer: str, target_window_id: int, boss: Boss 237 | ) -> None: 238 | opts = parser.parse_args(args[1:]) 239 | 240 | if opts.command == "load": 241 | return load_handler(args, answer, target_window_id, boss) 242 | elif opts.command == "new": 243 | return new_handler(args, answer, target_window_id, boss) 244 | --------------------------------------------------------------------------------