Something went wrong. Try again.
a simple git-manager ui
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265#!/usr/bin/env python3import cursesimport subprocessclass GitUI: def __init__(self, stdscr): self.s = stdscr self.s.keypad(True) curses.curs_set(0) curses.start_color() curses.use_default_colors() # Colors: 1:Border, 2:Select, 3:Green, 4:Red, 5:Input, 6:Help, 7:Default curses.init_pair(1, curses.COLOR_CYAN, -1) curses.init_pair(2, curses.COLOR_BLUE, -1) curses.init_pair(3, curses.COLOR_GREEN, -1) curses.init_pair(4, curses.COLOR_RED, -1) curses.init_pair(5, curses.COLOR_YELLOW, -1) curses.init_pair(6, curses.COLOR_MAGENTA, -1) curses.init_pair(7, curses.COLOR_WHITE, -1) self.menu = [ "Fetch", "Stage All", "Stage Files", "Commit", "Push", "Switch Branch", "New Branch", "Stash", "Quit", ] self.sel = 0 self.msg = "" self.input = "" self.input_mode = False self.stage_mode = False self.branch_mode = False # New mode for switching branches self.prompt = "" self.cb = None self.running = True def git(self, args, timeout=5): try: r = subprocess.run( ["git"] + args, capture_output=True, text=True, timeout=timeout ) if r.returncode == 0: return r.stdout.strip() return f"Error: {r.stderr.strip()}" except subprocess.TimeoutExpired: return "Error: Command timed out" except Exception as e: return f"Error: {str(e)}" def get_local_branches(self): out = self.git(["branch", "--format=%(refname:short)"]) if "Error" in out or not out: return ["main"] return out.split("\n") def get_modified_files(self): out = self.git(["status", "--porcelain"]) if "Error" in out or not out: return ["No changes"] return [line[3:] for line in out.split("\n") if line.strip()] def draw(self): h, w = self.s.getmaxyx() self.s.erase() # UI Borders self.s.attron(curses.color_pair(1)) self.s.border() self.s.vline(1, 20, curses.ACS_VLINE, h - 5) self.s.hline(h // 2, 21, curses.ACS_HLINE, w - 22) self.s.attroff(curses.color_pair(1)) # Header curr_br = self.git(["branch", "--show-current"]) self.s.attron(curses.color_pair(2) | curses.A_BOLD) self.s.addstr(0, 2, f" GITUI | {curr_br} ") self.s.attroff(curses.color_pair(2) | curses.A_BOLD) # Determine which list to show on the left if self.stage_mode: current_list = self.get_modified_files() elif self.branch_mode: current_list = self.get_local_branches() else: current_list = self.menu for i, item in enumerate(current_list): if i >= (h - 6): break if i == self.sel: self.s.attron(curses.color_pair(2) | curses.A_REVERSE) self.s.addstr(2 + i, 2, f" {item[:16].ljust(16)} ") self.s.attroff(curses.color_pair(2) | curses.A_REVERSE) else: self.s.addstr(2 + i, 2, f" {item[:16]}") # Top Right: Status status = self.git(["status", "-s"]) self.s.attron(curses.color_pair(3)) for i, line in enumerate(status.split("\n")[: (h // 2) - 2]): if line: self.s.addstr(2 + i, 22, line[: w - 24]) self.s.attroff(curses.color_pair(3)) # Bottom Right: Contextual Preview preview_y = (h // 2) + 1 if self.stage_mode and current_list != ["No changes"]: target = current_list[self.sel] diff = self.git(["diff", target]).split("\n") self.s.addstr( preview_y - 1, 22, f"[ Diff: {target[:20]} ]", curses.color_pair(1) ) lines = diff[4:] if len(diff) > 4 else ["(New file)"] for i, line in enumerate(lines[: (h - 4) - preview_y]): color = ( curses.color_pair(3) if line.startswith("+") else ( curses.color_pair(4) if line.startswith("-") else curses.color_pair(7) ) ) self.s.addstr(preview_y + i, 22, line[: w - 24], color) elif self.branch_mode: self.s.addstr( preview_y - 1, 22, "[ Select Branch to Switch ]", curses.color_pair(1) ) self.s.addstr( preview_y + 1, 22, f"Target: {current_list[self.sel]}", curses.color_pair(5), ) # Messages/Help if self.msg: color = ( curses.color_pair(4) if "Error" in self.msg else curses.color_pair(3) ) self.s.addstr(h - 3, 2, f"● {self.msg[:w-6]}", color) if self.input_mode: self.s.addstr( h - 2, 2, f"{self.prompt}: {self.input}█", curses.color_pair(5) ) else: self.s.addstr( h - 2, 2, "Esc:Back | Enter:Select | j/k:Nav | q:Quit", curses.color_pair(6), ) self.s.refresh() def handle_key(self, k): if self.input_mode: if k in (10, 13): self.cb() elif k == 27: self.input_mode = False elif k in (127, 8, curses.KEY_BACKSPACE): self.input = self.input[:-1] elif 32 <= k < 127: self.input += chr(k) return if k == ord("q"): self.running = False elif k == 27: self.stage_mode = self.branch_mode = False self.sel = 0 elif k in (ord("j"), curses.KEY_DOWN): limit = ( len(self.get_modified_files()) if self.stage_mode else ( len(self.get_local_branches()) if self.branch_mode else len(self.menu) ) ) self.sel = min(self.sel + 1, limit - 1) elif k in (ord("k"), curses.KEY_UP): self.sel = max(self.sel - 1, 0) elif k in (10, 13, ord("l")): if self.stage_mode: files = self.get_modified_files() if files != ["No changes"]: self.git(["add", files[self.sel]]) if len(self.get_modified_files()) == 0: self.stage_mode = False elif self.branch_mode: target = self.get_local_branches()[self.sel] res = self.git(["checkout", target]) self.msg = f"Switched to {target}" if "Error" not in res else res[:60] self.branch_mode = False self.sel = 0 else: action = self.menu[self.sel] if action == "Fetch": self.msg = "Fetching..." self.draw() self.git(["fetch"]) self.msg = "Fetched" elif action == "Stage All": self.git(["add", "."]) self.msg = "Staged all" elif action == "Stage Files": self.stage_mode, self.sel = True, 0 elif action == "Switch Branch": self.branch_mode, self.sel = True, 0 elif action == "Push": self.msg = "Pushing..." self.draw() branch = self.git(["branch", "--show-current"]) res = self.git(["push", "-u", "origin", branch], timeout=30) self.msg = "Pushed!" if "Error" not in res else res[:60] elif action == "Commit": self.input_mode, self.prompt, self.input, self.cb = ( True, "Msg", "", self._commit, ) elif action == "New Branch": self.input_mode, self.prompt, self.input, self.cb = ( True, "Name", "", self._new_branch, ) elif action == "Stash": self.git(["stash"]) self.msg = "Stashed" elif action == "Quit": self.running = False def _commit(self): r = self.git(["commit", "-m", self.input]) self.msg = "Committed!" if "Error" not in r else r[:60] self.input_mode = False def _new_branch(self): r = self.git(["checkout", "-b", self.input]) self.msg = f"Created {self.input}" if "Error" not in r else r[:60] self.input_mode = False def run(self): while self.running: self.draw() self.handle_key(self.s.getch())if __name__ == "__main__": curses.wrapper(lambda s: GitUI(s).run())