From de101e8dfb050bac829b33428dc8bce9309c83d1 Mon Sep 17 00:00:00 2001 From: SpookyDervish Date: Wed, 4 Feb 2026 19:30:41 +1100 Subject: [PATCH] we have a very basic search system working --- banner.txt | 6 ++ main.py | 15 +++++ screens/search_screen.py | 123 ++++++++++++++++++++++++++++++++++++++ screens/welcome_screen.py | 47 +++++++++++++++ widgets/__init__.py | 1 + widgets/navbar.py | 21 +++++++ 6 files changed, 213 insertions(+) create mode 100644 banner.txt create mode 100644 main.py create mode 100644 screens/search_screen.py create mode 100644 screens/welcome_screen.py create mode 100644 widgets/__init__.py create mode 100644 widgets/navbar.py diff --git a/banner.txt b/banner.txt new file mode 100644 index 0000000..d256f4c --- /dev/null +++ b/banner.txt @@ -0,0 +1,6 @@ + + ______ _ _______ __ __ __ + /_ __/_ __(_) ____(_) /_/ /_ __ __/ /_ + / / / / / / / / __/ / __/ __ \/ / / / __ \ + / / / /_/ / / /_/ / / /_/ / / / /_/ / /_/ / +/_/ \__,_/_/\____/_/\__/_/ /_/\__,_/_.___/ \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..614d05e --- /dev/null +++ b/main.py @@ -0,0 +1,15 @@ +from textual.app import App, ComposeResult +from screens.welcome_screen import WelcomeScreen +from screens.search_screen import SearchScreen + + +class TuiGithub(App): + GITEA_HOST = "https://chookspace.com/" + + def on_compose(self): + self.push_screen(SearchScreen()) + + +if __name__ == "__main__": + app = TuiGithub() + app.run() \ No newline at end of file diff --git a/screens/search_screen.py b/screens/search_screen.py new file mode 100644 index 0000000..df75eb5 --- /dev/null +++ b/screens/search_screen.py @@ -0,0 +1,123 @@ +from widgets import Navbar +from textual.screen import Screen +from textual.widgets import Input, Select, LoadingIndicator, Static +from textual.containers import HorizontalGroup, ScrollableContainer +from textual.app import ComposeResult + +from human_readable import time_delta +from datetime import datetime, timedelta + +import requests + + +class SearchResult(HorizontalGroup): + DEFAULT_CSS = """ + SearchResult { + padding: 0 1; + margin: 0 1; + margin-top: 1; + border: tall $surface; + + Static { + width: auto; + } + + + } + """ + + def __init__( + self, + author: str, + name: str, + description: str, + is_fork: bool, + updated_at: datetime + ): + super().__init__() + self.author = author + self.repo_name = name + self.description = description + self.is_fork = is_fork + self.updated_at = updated_at.replace(tzinfo=None) + + def compose(self) -> ComposeResult: + updated_string = time_delta(datetime.now() - self.updated_at) + yield Static(f"[b]{self.author}[/] / [b]{self.repo_name}[/]{' [d]\[[blue]fork[/]]' if self.is_fork else ''}\n{self.description}\n[d]Updated {updated_string} ago") + +class SearchScreen(Screen): + DEFAULT_CSS = """ + #search-box { + padding: 1; + padding-bottom: 0; + border-bottom: hkey $surface; + + #query { + width: 1fr; + } + + #search-select { + width: 25; + } + } + """ + + def action_search_query(self): + query_input = self.query_one("#query") + loading = self.query_one(LoadingIndicator) + results = self.query_one(ScrollableContainer) + + loading.display = True + + # send off a request + response = requests.get( + url=self.app.GITEA_HOST + "api/v1/repos/search", + params={ + "q": query_input.value, + "limit": 20 + } + ) + + loading.display = False + + # error handling + if not response.ok: + self.notify(response.text, title="Error while getting search results:", severity="error") + return + + # display results + results.remove_children(SearchResult) + to_mount = [] + print(response.json()) + for result in response.json()["data"]: + to_mount.append(SearchResult( + result["owner"]["login"], + result["name"], + result["description"], + result["fork"], + datetime.fromisoformat(result["updated_at"]) + )) + results.mount_all(to_mount) + + # self explanitory + query_input.clear() + + def on_input_submitted(self, event: Input.Submitted): + if event.input.id == "query": + self.action_search_query() + + def on_mount(self): + self.action_search_query() + #self.query_one(LoadingIndicator).display = False + + def compose(self) -> ComposeResult: + yield Navbar() + + with HorizontalGroup(id="search-box"): + yield Input(placeholder="Search repos...", id="query") + yield Select.from_values([ + "Repositories", + ], id="search-select", allow_blank=False) + + with ScrollableContainer(id="results"): + yield LoadingIndicator() \ No newline at end of file diff --git a/screens/welcome_screen.py b/screens/welcome_screen.py new file mode 100644 index 0000000..72bf369 --- /dev/null +++ b/screens/welcome_screen.py @@ -0,0 +1,47 @@ +from textual.containers import Vertical, HorizontalGroup, Center +from textual.screen import Screen +from textual.app import ComposeResult +from textual.widgets import Static, Button +from textualeffects.widgets import EffectLabel, EffectType, effects + +from widgets import Navbar + +from random import choice + + +class WelcomeScreen(Screen): + DEFAULT_CSS = """ + Center { + padding: 2; + width: 100%; + + Static { + text-align: center; + } + + EffectLabel { + text-style: bold; + min-width: 100%; + } + + #buttons { + margin-top: 1; + align: center middle; + Button { + margin: 0 2; + } + } + } + """ + + def compose(self) -> ComposeResult: + yield Navbar() + with Center(): + with open("banner.txt", "r") as f: + yield EffectLabel(text=f.read(), effect=choice(list(effects.keys()))) + + yield Static("[d]Gitea, in your terminal.[/]") + + with HorizontalGroup(id="buttons"): + yield Button("Explore", variant="primary", flat=True) + yield Button("another button lol", flat=True) \ No newline at end of file diff --git a/widgets/__init__.py b/widgets/__init__.py new file mode 100644 index 0000000..0316a46 --- /dev/null +++ b/widgets/__init__.py @@ -0,0 +1 @@ +from .navbar import Navbar \ No newline at end of file diff --git a/widgets/navbar.py b/widgets/navbar.py new file mode 100644 index 0000000..23d4426 --- /dev/null +++ b/widgets/navbar.py @@ -0,0 +1,21 @@ +from textual.widgets import Static, Button +from textual.containers import HorizontalGroup + + +class Navbar(HorizontalGroup): + DEFAULT_CSS = """ + Navbar { + background: $surface-darken-1; + border-bottom: hkey $surface; + height: 3; + padding: 1; + padding-bottom: 0; + + #hamburger-menu { + max-width: 1; + } + } + """ + + def compose(self): + yield Button("≡", compact=True, id="hamburger-menu") \ No newline at end of file