From fb0a8aa60bcf472f24edf1c60a82abadf34a81f5 Mon Sep 17 00:00:00 2001 From: SpookyDervish Date: Sun, 26 Oct 2025 13:07:44 +1100 Subject: [PATCH] Upload files to "/" --- main.py | 247 ++++++++++++++++++++++++++++++++++++++++++++++ style.tcss | 75 ++++++++++++++ theme_mappings.py | 16 +++ 3 files changed, 338 insertions(+) create mode 100644 main.py create mode 100644 style.tcss create mode 100644 theme_mappings.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..9910f22 --- /dev/null +++ b/main.py @@ -0,0 +1,247 @@ +from textual.app import App, ComposeResult +from textual.widgets import Header, Footer, ContentSwitcher, DirectoryTree, Static, Button, TextArea, Tabs, Tab, RichLog, Input +from textual.containers import HorizontalGroup, Vertical +from textual.binding import Binding +from textual_window import Window +from textual import on +from assets.theme_mappings import theme_mappings + +from textual_fspicker import FileOpen, FileSave + +import subprocess +import os + + +class Berry(App): + CSS_PATH = "assets/style.tcss" + SUB_TITLE = "New File" + + BINDINGS = [ + Binding("ctrl+o", "open", "Open File"), + Binding("ctrl+n", "new", "New File"), + Binding("ctrl+s", "save", "Save"), + Binding("ctrl+shift+s", "save_as", "Save As...", priority=True), + Binding("ctrl+f", "find", "Find", priority=True) + ] + + def compose(self) -> ComposeResult: + yield Header() + with Vertical(id="sidebar"): + with HorizontalGroup(id="sidebar-buttons"): + yield Button("📂") + yield Button("🔍") + + with ContentSwitcher(initial="files"): + with Vertical(id="files"): + yield Static("EXPLORER") + yield DirectoryTree("./", id="directory") + + with Vertical(id="editor"): + first_tab = Tab("New File") + first_tab.file_path = None + + yield Tabs( + first_tab, + id="file-tabs" + ) + yield TextArea.code_editor(placeholder="This file is empty.", language="python", theme="css", id="code-editor", disabled=True) + with Vertical(id="console-container"): + yield RichLog(id="console") + yield Input(placeholder="> ", id="console-input") + + yield Footer() + + def run_command(self, command: str): + console = self.query_one("#console") + + result = subprocess.check_output(command, shell=True, text=True) + console.write(result) + + def action_find(self): + try: + self.query_one("#find-window") + return + except: + + find_window = Window( + + Vertical( + HorizontalGroup( + Input(placeholder="Find"), + Static("0 of 0", id="num-matches"), + Button("↑", flat=True), + Button("↓", flat=True), + ), + HorizontalGroup( + Input(placeholder="Replace"), + ), + ), + + icon="🔍", + start_open=True, + allow_resize=False, + allow_maximize=False, + id="find-window", + mode="temporary", + name="Find & Replace" + ) + + self.mount(find_window) + + async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected): + if self.open_file == str(event.path): + return + + self.file_clicked = True + self.switching = True + + tabs: Tabs = self.query_one("#file-tabs") + + if self.open_file not in self.unsaved_files: + if self.open_file: + self.file_tabs.pop(self.open_file) + tabs.remove_tab(tabs.active_tab) + + self.open_file = str(event.path) + self.sub_title = os.path.basename(self.open_file) + + if str(event.path) not in self.file_tabs: + new_tab = Tab(os.path.basename(str(event.path))) + new_tab.tooltip = str(new_tab.label) + setattr(new_tab, "file_path", str(event.path)) + await tabs.add_tab(new_tab) + self.file_tabs[str(event.path)] = new_tab + tabs.active = new_tab.id + else: + tabs.active = self.file_tabs[str(event.path)].id + + + + + @on(Tabs.TabActivated) + def on_tab_shown(self, event: Tabs.TabActivated): + + if self.file_clicked: + self.file_clicked = False + else: + self.open_file = getattr(event.tab, "file_path") + + self.switching = True + + code_editor: TextArea = self.query_one("#code-editor") + if self.open_file: + try: + f = open(self.open_file, "r", encoding="utf-8") + except Exception as e: + self.notify(f"Failed to open the file: {e}") + return + + + + + try: + if self.open_file in self.unsaved_files: + code_editor.text = self.unsaved_files[self.open_file]["current"] + else: + code_editor.text = f.read() + + code_editor.language = theme_mappings.get(self.open_file.rsplit(".", 1)[1], None) + code_editor.disabled = False + except UnicodeDecodeError: + code_editor.text = "This file is in binary, it can't be openned. Sorry." + code_editor.language = None + code_editor.disabled = True + + f.close() + else: + code_editor.text = "" + code_editor.language = None + code_editor.disabled = False + + + @on(Window.Minimized) + def window_minimized(self, event: Window.Minimized): + event.window.remove_window() + + def on_text_area_changed(self, event: TextArea.Changed): + if event.text_area.id != "code-editor": + return + if self.switching: + self.switching = False + return + + + + tabs: Tabs = self.query_one("#file-tabs") + if self.open_file: + if not self.open_file in self.unsaved_files: + with open(self.open_file, "r", encoding="utf-8") as f: + if f.read() == event.text_area.text: # TODO: figure out why im guetting what seems like a race conidition which is making this if statement needed + return + self.unsaved_files[self.open_file] = {"current": event.text_area.text, "original": f.read()} + + + tabs.active_tab.tooltip = f"Unsaved changes in {tabs.active_tab.label}" + tabs.active_tab.label = "[d orange]●[/] " + str(tabs.active_tab.label) + + else: + self.unsaved_files[self.open_file]["current"] = event.text_area.text + if self.unsaved_files[self.open_file]["original"] == self.unsaved_files[self.open_file]["current"]: + tabs.active_tab.label = os.path.basename(self.open_file) + tabs.active_tab.tooltip = str(tabs.active_tab.label) + self.unsaved_files.pop(self.open_file) + + def action_new(self): + tabs: Tabs = self.query_one("#file-tabs") + new_tab = Tab("New File") + setattr(new_tab, "file_path", None) + tabs.add_tab(new_tab) + tabs.active = new_tab.id + + self.open_file = None + self.switching = True + code_editor: TextArea = self.query_one("#code-editor") + + code_editor.disabled = False + code_editor.text = "" + + def done_saving(self, result): + if result is None: return + + with open(result, "wb") as f: + f.write(self.query_one("#code-editor").text.encode()) + self.notify(f"Saved to {result} successfully.", title="Done!", markup=False) + + def action_save_as(self): + self.push_screen(FileSave(), callback=self.done_saving) + + def action_save(self): + if self.open_file == None and self.query_one("#code-editor").disabled == False: + self.action_save_as() + + # dont bother saving if there are no new changes + if not self.open_file in self.unsaved_files: return + + with open(self.open_file, "w") as f: + f.write(self.unsaved_files[self.open_file]["current"]) + + tabs: Tabs = self.query_one("#file-tabs") + tabs.active_tab.label = os.path.basename(self.open_file) + tabs.active_tab.tooltip = str(tabs.active_tab.label) + self.unsaved_files.pop(self.open_file) + self.notify("Saved.") + + def on_ready(self): + # src/main.py: Tab<> + self.file_tabs = {} + self.open_file = None + self.unsaved_files = {} # list of paths + self.switching = False + self.file_clicked = False + self.query_one("#console").write("Run a command below.") + + + +if __name__ == "__main__": + app = Berry() + app.run() \ No newline at end of file diff --git a/style.tcss b/style.tcss new file mode 100644 index 0000000..2a830e5 --- /dev/null +++ b/style.tcss @@ -0,0 +1,75 @@ +#sidebar { + background: $boost-darken-1; + width: 15%; + min-width: 20; + dock: left; + margin-top: 1; +} + +#sidebar-buttons { + padding: 1; + padding-bottom: 0; + background: $boost-darken-1; + border-bottom: tall $surface; + + Button { + max-width: 5; + margin-right: 1; + } +} + +ContentSwitcher Vertical { + padding: 1; + Static { + margin-bottom: 1; + text-style: dim; + } +} + +#files { + #directory { + background: transparent; + } +} + +#editor { + margin-top: 1; + + #code-editor { + min-height: 3; + } +} + +#console-container { + padding-top: 1; + margin: 1; + border-top: tall $boost; + height: 35%; + min-height: 12; + + #console { + border: tall $boost; + margin-bottom: 1; + } +} + +#find-window { + width: 45; + height: 11; + + Input { + width: 20; + margin-bottom: 1; + } + + #num-matches { + width: 10; + margin-left: 1; + height: 3; + content-align: left middle; + } + + Button { + max-width: 5; + } +} \ No newline at end of file diff --git a/theme_mappings.py b/theme_mappings.py new file mode 100644 index 0000000..ab244d6 --- /dev/null +++ b/theme_mappings.py @@ -0,0 +1,16 @@ +theme_mappings = { + "py": "python", + "json": "json", + "tcss": "css", + "css": "css", + "html": "html", + "htm": "html", + "rs": "rust", + "js": "javascript", + "xml": "xml", + "toml": "toml", + "go": "go", + "sh": "bash", + "yaml": "yaml", + "md": "markdown" +} \ No newline at end of file