file tree kinda working :D

This commit is contained in:
2026-02-07 21:04:37 +11:00
parent 16afb5e2d0
commit 92990e7d4b
4 changed files with 175 additions and 109 deletions

View File

@@ -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
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):
if type == "dir":
self.current_dir = path
@@ -164,6 +94,7 @@ 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,11 +193,16 @@ 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:
# get repo data via a request

72
util.py Normal file
View 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"

View File

@@ -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)
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)
class RepoDirectoryTree(Tree):
DEFAULT_CSS = """
RepoDirectoryTree {
dock: left;
width: 30;
background: $background;
}
"""
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
self.repo_tree = None
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()
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