diff --git a/main.py b/main.py index 7d311ed..e8f2677 100644 --- a/main.py +++ b/main.py @@ -15,4 +15,4 @@ class TuiGithub(App): if __name__ == "__main__": app = TuiGithub() - app.run() \ No newline at end of file + app.run() diff --git a/screens/repo_view_screen.py b/screens/repo_view_screen.py index 8fae660..390cfc7 100644 --- a/screens/repo_view_screen.py +++ b/screens/repo_view_screen.py @@ -1,6 +1,6 @@ from textual.screen import Screen from textual.app import ComposeResult -from textual.widgets import Input, Select, TextArea, LoadingIndicator, Static, DataTable, ContentSwitcher, Tabs, Tab, Button, Markdown +from textual.widgets import Input, Tree, Select, TextArea, LoadingIndicator, Static, DataTable, ContentSwitcher, Tabs, Tab, Button, Markdown from textual.containers import VerticalGroup, Vertical, HorizontalGroup, Right from textual import work @@ -9,6 +9,8 @@ from widgets import Navbar, RepoDirectoryTree from datetime import datetime from human_readable import time_delta +from util import get_icon_from_name_and_type + import requests, asyncio, base64 @@ -21,81 +23,9 @@ class RepoViewScreen(Screen): self.repo_name = repo_name self.current_dir = "." self.readme_loaded = False - - def get_icon_from_name_and_type(self, file_name: str, is_folder: bool): - - # nerd font icons my beloved - if not is_folder: - match file_name: - case "Makefile": - return "\ue673" - case "Dockerfile" | "Containerfile": - return "\ue7b0" - case "requirements.txt": - return "\ue73c" - case "LICENSE": - return "\uf1f9" - case "Cargo.lock" | "Cargo.toml": - return "\ue7a8" - - if "." in file_name: - extension = file_name[file_name.find(".")+1:] - - match extension: - case 'c' | 'h': - return "\ue61e" - case 'cpp': - return "\ue61d" - case 'py': - return "\ue73c" - case 'js': - return "\ue781" - case 'json': - return "\ueb0f" - case 'gitignore' | 'gitmodules': - return "\ue702" - case 'html' | 'htm': - return "\ue736" - case 'css' | 'tcss': - return "\ue749" - case 'svg': - return "\ue698" - case 'ico': - return "\ue623" - case 'go': - return "\ue65e" - case 'rs': - return "\ue7a8" - case 'grnd' | 'sols': - return "\uf44f" - case 'md': - return "\ueb1d" - case 'fish': - return "\uee41" - case 'sh': - return "\ue760" - case 'bat': - return "\ue70f" - case 'png' | 'jpg' | 'jpeg' | 'avif': - return "\uf03e" - case 'lua': - return "\ue620" - case 'zip' | 'tar' | 'gz' | "7z": - return "\ue6aa" - case "rb": - return "\ue605" - case "kt": - return "\ue634" - case "java": - return "\ue738" - case _: # unrecognized file type - return "\uf15b" - else: # has no dot in the name - return "\uf15b" - else: # is a folder - return "\ue5ff" + self.most_recent_commit = None - @work(thread=False, exclusive=True) + #@work(thread=False, exclusive=True) async def action_view_file(self, path: str, type: str): if type == "dir": self.current_dir = path @@ -163,7 +93,8 @@ class RepoViewScreen(Screen): loading.display = False file_screen.display = True - + + @work(thread=True, exclusive=True) def show_directory(self, path: str): files: DataTable = self.query_one("#files") self.query_one("#file-screen").display = False @@ -178,18 +109,6 @@ class RepoViewScreen(Screen): readme: Markdown = self.query_one("#readme") readme.display = path == "." - # get files in dir - files_response = requests.get( - self.app.GITEA_HOST + f"api/v1/repos/{self.owner_name}/{self.repo_name}/contents-ext/{path}", - params={ - "includes": "commit_metadata,commit_message" - } - ) - - if not files_response.ok: - self.notify(files_response.text, title="Failed to get files:", severity="error") - return - print("Getting most recent commit...") # get most recent commit @@ -205,12 +124,27 @@ class RepoViewScreen(Screen): self.notify(commits_response.text, title="Failed to get most recent commit:", severity="error") return - most_recent_commit = commits_response.json()[0] + self.most_recent_commit = commits_response.json()[0] files.add_columns( - f"[b]{most_recent_commit["commit"]["author"]["name"]}[/]", - f"[r]{most_recent_commit["sha"][:10]}[/]", - f"[d]{most_recent_commit["commit"]["message"]}" + f"[b]{self.most_recent_commit["commit"]["author"]["name"]}[/]", + f"[r]{self.most_recent_commit["sha"][:10]}[/]", + f"[d]{self.most_recent_commit["commit"]["message"]}" ) + self.query_one(RepoDirectoryTree).load_repo_tree() + + # get files in dir + files_response = requests.get( + self.app.GITEA_HOST + f"api/v1/repos/{self.owner_name}/{self.repo_name}/contents-ext/{path}", + params={ + "includes": "commit_metadata,commit_message" + } + ) + + if not files_response.ok: + self.notify(files_response.text, title="Failed to get files:", severity="error") + return + + print("Getting root commits...") @@ -223,7 +157,7 @@ class RepoViewScreen(Screen): if path != ".": previous_dir = self.current_dir[:self.current_dir.rfind("/")] rows.append(( - f"[cyan]{self.get_icon_from_name_and_type("..", True)}[/] [@click=screen.view_file('{previous_dir}','dir')]..[/]", + f"[cyan]{get_icon_from_name_and_type("..", True)}[/] [@click=screen.view_file('{previous_dir}','dir')]..[/]", "", "" )) @@ -232,7 +166,7 @@ class RepoViewScreen(Screen): commit_created_at = datetime.fromisoformat(file["last_committer_date"]).replace(tzinfo=None) rows.append(( - f"[cyan]{self.get_icon_from_name_and_type(file["name"], file["type"] != "file")}[/] [@click=screen.view_file('{self.current_dir}/{file["name"]}','{file["type"]}')]{file["name"]}[/]", + f"[cyan]{get_icon_from_name_and_type(file["name"], file["type"] != "file")}[/] [@click=screen.view_file('{self.current_dir}/{file["name"]}','{file["type"]}')]{file["name"]}[/]", f"[d]{file["last_commit_message"]}[/]", f"[d]Updated {time_delta(datetime.now() - commit_created_at)} ago[/]" )) @@ -259,10 +193,15 @@ class RepoViewScreen(Screen): files.display = True loading.display = False - @work(thread=True, exclusive=True) def on_mount(self): print("Getting files...") self.show_directory(self.current_dir) + + async def on_tree_node_selected(self, event: Tree.NodeSelected): + path = event.node.data["path"] + + if not path.endswith("/"): # ignore when we click on a directory + await self.action_view_file(path, "file") def compose(self) -> ComposeResult: diff --git a/util.py b/util.py new file mode 100644 index 0000000..24c6735 --- /dev/null +++ b/util.py @@ -0,0 +1,72 @@ +def get_icon_from_name_and_type(file_name: str, is_folder: bool): + + # nerd font icons my beloved + if not is_folder: + match file_name: + case "Makefile": + return "\ue673" + case "Dockerfile" | "Containerfile": + return "\ue7b0" + case "requirements.txt": + return "\ue73c" + case "LICENSE": + return "\uf1f9" + case "Cargo.lock" | "Cargo.toml": + return "\ue7a8" + + if "." in file_name: + extension = file_name[file_name.find(".")+1:] + + match extension: + case 'c' | 'h': + return "\ue61e" + case 'cpp': + return "\ue61d" + case 'py': + return "\ue73c" + case 'js': + return "\ue781" + case 'json': + return "\ueb0f" + case 'gitignore' | 'gitmodules': + return "\ue702" + case 'html' | 'htm': + return "\ue736" + case 'css' | 'tcss': + return "\ue749" + case 'svg': + return "\ue698" + case 'ico': + return "\ue623" + case 'go': + return "\ue65e" + case 'rs': + return "\ue7a8" + case 'grnd' | 'sols': + return "\uf44f" + case 'md': + return "\ueb1d" + case 'fish': + return "\uee41" + case 'sh': + return "\ue760" + case 'bat': + return "\ue70f" + case 'png' | 'jpg' | 'jpeg' | 'avif': + return "\uf03e" + case 'lua': + return "\ue620" + case 'zip' | 'tar' | 'gz' | "7z": + return "\ue6aa" + case "rb": + return "\ue605" + case "kt": + return "\ue634" + case "java": + return "\ue738" + case _: # unrecognized file type + return "\uf15b" + else: # has no dot in the name + return "\uf15b" + else: # is a folder + return "\ue5ff" \ No newline at end of file diff --git a/widgets/repo_dir_tree.py b/widgets/repo_dir_tree.py index 9a15fb3..4afc63b 100644 --- a/widgets/repo_dir_tree.py +++ b/widgets/repo_dir_tree.py @@ -1,22 +1,77 @@ -from textual.widgets import Tree, DirectoryTree -from textual.widgets.directory_tree import DirEntry +from textual.widgets import Tree +from textual.widgets.tree import TreeNode from textual import work +from collections import defaultdict + +from util import get_icon_from_name_and_type + import requests -class RepoDirectoryTree(DirectoryTree): - ICON_NODE_EXPANDED = "\uf07c " - ICON_NODE = "\ue5ff " - ICON_FILE = "\uf15b " +def tree(): + return defaultdict(tree) + + +class RepoDirectoryTree(Tree): + DEFAULT_CSS = """ + RepoDirectoryTree { + dock: left; + width: 30; + background: $background; + } + """ - def __init__(self, repo_owner: str, repo_name: str, path: str, name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False): - super().__init__(path=path, name=name, id=id, classes=classes, disabled=disabled) + ICON_NODE_EXPANDED = "" + ICON_NODE = "" + + def __init__(self, repo_owner: str, repo_name: str, root_sha: str, name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False): + super().__init__(label="\ue5ff Files", id=id, classes=classes, disabled=disabled) self.repo_owner = repo_owner self.repo_name = repo_name - - def _directory_content(self, location: Path, worker: Worker) -> Iterator[Path]: - contents = requests.get( - self.app.GITEA_HOST + f"api/v1/repos/{self.repo_owner}/{self.repo_name}/contents/{location}" - ).json() \ No newline at end of file + self.repo_tree = None + + def load_repo_tree(self): + root_branch_sha = self.screen.most_recent_commit["commit"]["tree"]["sha"] + + # get list of paths in repo tree + root = tree() + page_number = 1 + while True: + contents = requests.get( + self.app.GITEA_HOST + f"api/v1/repos/{self.repo_owner}/{self.repo_name}/git/trees/{root_branch_sha}", + params={ + "recursive": True, + "page": page_number, + "page_size": 25 + } + ).json() + + for file in contents["tree"]: + node = root + for part in file["path"].split("/"): + node = node[part] + + if not contents["truncated"]: + break + + page_number += 1 + + # walk the tree we generated and add all the leaves + def walk(tree, prefix="", tree_node: TreeNode = None): + if tree_node == None: + tree_node = self.root + + for name, node in tree.items(): + if len(node) > 0: # directory + new_tree_node = tree_node.add(f"[cyan]\ue5ff[/cyan] {name}", data={"path": prefix + name + "/"}) + + # walk new found directory + walk(node, prefix + name + "/", new_tree_node) + else: # file + tree_node.add_leaf(f"[cyan]{get_icon_from_name_and_type(name, False)}[/cyan] {name}", data={"path": prefix + name}) + + walk(root) + self.root.expand_all() + self.repo_tree = root \ No newline at end of file