file tree kinda working :D
This commit is contained in:
2
main.py
2
main.py
@@ -15,4 +15,4 @@ class TuiGithub(App):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = TuiGithub()
|
app = TuiGithub()
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.app import ComposeResult
|
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.containers import VerticalGroup, Vertical, HorizontalGroup, Right
|
||||||
from textual import work
|
from textual import work
|
||||||
|
|
||||||
@@ -9,6 +9,8 @@ from widgets import Navbar, RepoDirectoryTree
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from human_readable import time_delta
|
from human_readable import time_delta
|
||||||
|
|
||||||
|
from util import get_icon_from_name_and_type
|
||||||
|
|
||||||
import requests, asyncio, base64
|
import requests, asyncio, base64
|
||||||
|
|
||||||
|
|
||||||
@@ -21,81 +23,9 @@ class RepoViewScreen(Screen):
|
|||||||
self.repo_name = repo_name
|
self.repo_name = repo_name
|
||||||
self.current_dir = "."
|
self.current_dir = "."
|
||||||
self.readme_loaded = False
|
self.readme_loaded = False
|
||||||
|
self.most_recent_commit = None
|
||||||
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"
|
|
||||||
|
|
||||||
@work(thread=False, exclusive=True)
|
#@work(thread=False, exclusive=True)
|
||||||
async def action_view_file(self, path: str, type: str):
|
async def action_view_file(self, path: str, type: str):
|
||||||
if type == "dir":
|
if type == "dir":
|
||||||
self.current_dir = path
|
self.current_dir = path
|
||||||
@@ -163,7 +93,8 @@ class RepoViewScreen(Screen):
|
|||||||
|
|
||||||
loading.display = False
|
loading.display = False
|
||||||
file_screen.display = True
|
file_screen.display = True
|
||||||
|
|
||||||
|
@work(thread=True, exclusive=True)
|
||||||
def show_directory(self, path: str):
|
def show_directory(self, path: str):
|
||||||
files: DataTable = self.query_one("#files")
|
files: DataTable = self.query_one("#files")
|
||||||
self.query_one("#file-screen").display = False
|
self.query_one("#file-screen").display = False
|
||||||
@@ -178,18 +109,6 @@ class RepoViewScreen(Screen):
|
|||||||
readme: Markdown = self.query_one("#readme")
|
readme: Markdown = self.query_one("#readme")
|
||||||
readme.display = path == "."
|
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...")
|
print("Getting most recent commit...")
|
||||||
|
|
||||||
# get 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")
|
self.notify(commits_response.text, title="Failed to get most recent commit:", severity="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
most_recent_commit = commits_response.json()[0]
|
self.most_recent_commit = commits_response.json()[0]
|
||||||
files.add_columns(
|
files.add_columns(
|
||||||
f"[b]{most_recent_commit["commit"]["author"]["name"]}[/]",
|
f"[b]{self.most_recent_commit["commit"]["author"]["name"]}[/]",
|
||||||
f"[r]{most_recent_commit["sha"][:10]}[/]",
|
f"[r]{self.most_recent_commit["sha"][:10]}[/]",
|
||||||
f"[d]{most_recent_commit["commit"]["message"]}"
|
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...")
|
print("Getting root commits...")
|
||||||
|
|
||||||
@@ -223,7 +157,7 @@ class RepoViewScreen(Screen):
|
|||||||
if path != ".":
|
if path != ".":
|
||||||
previous_dir = self.current_dir[:self.current_dir.rfind("/")]
|
previous_dir = self.current_dir[:self.current_dir.rfind("/")]
|
||||||
rows.append((
|
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)
|
commit_created_at = datetime.fromisoformat(file["last_committer_date"]).replace(tzinfo=None)
|
||||||
|
|
||||||
rows.append((
|
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]{file["last_commit_message"]}[/]",
|
||||||
f"[d]Updated {time_delta(datetime.now() - commit_created_at)} ago[/]"
|
f"[d]Updated {time_delta(datetime.now() - commit_created_at)} ago[/]"
|
||||||
))
|
))
|
||||||
@@ -259,10 +193,15 @@ class RepoViewScreen(Screen):
|
|||||||
files.display = True
|
files.display = True
|
||||||
loading.display = False
|
loading.display = False
|
||||||
|
|
||||||
@work(thread=True, exclusive=True)
|
|
||||||
def on_mount(self):
|
def on_mount(self):
|
||||||
print("Getting files...")
|
print("Getting files...")
|
||||||
self.show_directory(self.current_dir)
|
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:
|
def compose(self) -> ComposeResult:
|
||||||
|
|
||||||
|
|||||||
72
util.py
Normal file
72
util.py
Normal file
@@ -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"
|
||||||
@@ -1,22 +1,77 @@
|
|||||||
from textual.widgets import Tree, DirectoryTree
|
from textual.widgets import Tree
|
||||||
from textual.widgets.directory_tree import DirEntry
|
from textual.widgets.tree import TreeNode
|
||||||
from textual import work
|
from textual import work
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from util import get_icon_from_name_and_type
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
class RepoDirectoryTree(DirectoryTree):
|
def tree():
|
||||||
ICON_NODE_EXPANDED = "\uf07c "
|
return defaultdict(tree)
|
||||||
ICON_NODE = "\ue5ff "
|
|
||||||
ICON_FILE = "\uf15b "
|
|
||||||
|
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):
|
ICON_NODE_EXPANDED = ""
|
||||||
super().__init__(path=path, name=name, id=id, classes=classes, disabled=disabled)
|
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_owner = repo_owner
|
||||||
self.repo_name = repo_name
|
self.repo_name = repo_name
|
||||||
|
self.repo_tree = None
|
||||||
def _directory_content(self, location: Path, worker: Worker) -> Iterator[Path]:
|
|
||||||
contents = requests.get(
|
def load_repo_tree(self):
|
||||||
self.app.GITEA_HOST + f"api/v1/repos/{self.repo_owner}/{self.repo_name}/contents/{location}"
|
root_branch_sha = self.screen.most_recent_commit["commit"]["tree"]["sha"]
|
||||||
).json()
|
|
||||||
|
# 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
|
||||||
Reference in New Issue
Block a user