diff --git a/main.py b/main.py index 047ffe9..2e5fcff 100644 --- a/main.py +++ b/main.py @@ -1,315 +1,354 @@ -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 - -from plugin_loader import PluginLoader - -from watchdog.observers import Observer -from watchdog.events import FileSystemEventHandler - -import subprocess -import os - - -class Watcher(FileSystemEventHandler): - def __init__(self, app: App): - super().__init__() - self.app = app - - async def on_any_event(self, event): - await self.app.query_one(DirectoryTree).reload() - -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 __init__(self, path: str): - super().__init__() - self.path = path - - 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(self.path, 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.", theme="css", id="code-editor", disabled=True) - - #if os.name == "nt": - with Vertical(id="console-container"): - yield RichLog(id="console") - yield Input(placeholder="> ", id="console-input") - #else: - # yield Terminal(command="bash", id="terminal") - - yield Footer() - yield PluginLoader() - - def on_input_submitted(self, event: Input.Submitted): - if event.input.id != "console-input": - return - - self.run_command(event.input.value) - event.input.clear() - - def run_command(self, command: str): - console = self.query_one("#console") - - console.write(f"> {command}") - - if command == "clear" or command == "cls": - console.clear() - else: - try: - result = subprocess.check_output(command, shell=True, text=True, stderr=subprocess.STDOUT) - console.write(result) - except subprocess.CalledProcessError as e: - console.write(e.stdout) - - - self.query_one("#console-input").focus() - - 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() - - file_extension = self.open_file - dot_count = file_extension.count(".") - - if dot_count == 1: - if file_extension.startswith("."): - file_extension = file_extension.removeprefix(".") - else: - file_extension = file_extension.rsplit(".", 1)[1] - elif dot_count > 1: - file_extension = file_extension.rsplit(".", 1)[1] - - code_editor.language = theme_mappings.get(file_extension, 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 - - code_editor.focus() - - - @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()) - - tabs: Tabs = self.query_one("#file-tabs") - tabs.active_tab.label = os.path.basename(result) - self.notify(f"Saved to {result} successfully.", title="Done!", markup=False) - self.query_one(DirectoryTree).reload() - - 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 action_quit(self): - self.observer.stop() - self.observer.join() - return super().action_quit() - - 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.observer = Observer() - self.observer.schedule(Watcher(self), path=self.path) - self.observer.start() - - #if os.name == "nt": - self.query_one("#console").write("Run a command below.") - #else: - # self.query_one("#terminal").start() - -if __name__ == "__main__": - app = Berry("./") +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 textual_fspicker import FileOpen, FileSave + +from pathlib import Path + +from assets.theme_mappings import theme_mappings +from plugin_loader import PluginLoader + +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +import subprocess +import os + + +class Watcher(FileSystemEventHandler): + def __init__(self, app: App): + super().__init__() + self.app = app + + async def on_any_event(self, event): + await self.app.query_one(DirectoryTree).reload() + +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 __init__(self, path: str): + super().__init__() + self.path = path + + 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(self.path, 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.", theme="css", id="code-editor", disabled=True) + + #if os.name == "nt": + with Vertical(id="console-container"): + yield RichLog(id="console") + yield Input(placeholder="> ", id="console-input") + #else: + # yield Terminal(command="bash", id="terminal") + + yield Footer() + yield PluginLoader() + + def on_input_submitted(self, event: Input.Submitted): + if event.input.id != "console-input": + return + + self.run_command(event.input.value) + event.input.clear() + + def run_command(self, command: str): + console = self.query_one("#console") + + console.write(f"> {command}") + + if command == "clear" or command == "cls": + console.clear() + else: + try: + result = subprocess.check_output(command, shell=True, text=True, stderr=subprocess.STDOUT) + console.write(result) + except subprocess.CalledProcessError as e: + console.write(e.stdout) + + + self.query_one("#console-input").focus() + + async def chose_file_to_open(self, result): + if result == None: return + result = str(result) + if self.open_file == result: + return + + def is_within_directory(file_path: str, directory: str) -> bool: + file_path = Path(file_path).resolve() + directory = Path(directory).resolve() + return directory in file_path.parents + + 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 = result + inside_dir = is_within_directory(result, self.path) + + self.sub_title = os.path.basename(self.open_file) if inside_dir else self.open_file + + if result not in self.file_tabs: + new_tab = Tab(os.path.basename(result) if inside_dir else result) + new_tab.tooltip = str(new_tab.label) + setattr(new_tab, "file_path", result) + await tabs.add_tab(new_tab) + self.file_tabs[result] = new_tab + tabs.active = new_tab.id + else: + tabs.active = self.file_tabs[result].id + + def action_open(self): + self.app.push_screen(FileOpen(), self.chose_file_to_open) + + 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() + + file_extension = self.open_file + dot_count = file_extension.count(".") + + if dot_count == 1: + if file_extension.startswith("."): + file_extension = file_extension.removeprefix(".") + else: + file_extension = file_extension.rsplit(".", 1)[1] + elif dot_count > 1: + file_extension = file_extension.rsplit(".", 1)[1] + + code_editor.language = theme_mappings.get(file_extension, 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 + + code_editor.focus() + + + @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()) + + tabs: Tabs = self.query_one("#file-tabs") + tabs.active_tab.label = os.path.basename(result) + self.notify(f"Saved to {result} successfully.", title="Done!", markup=False) + self.query_one(DirectoryTree).reload() + + 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 action_quit(self): + self.observer.stop() + self.observer.join() + return super().action_quit() + + 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.observer = Observer() + self.observer.schedule(Watcher(self), path=self.path) + self.observer.start() + + #if os.name == "nt": + self.query_one("#console").write("Run a command below.") + #else: + # self.query_one("#terminal").start() + +if __name__ == "__main__": + app = Berry("./") app.run() \ No newline at end of file diff --git a/plugins/test-plugin/lua/main.lua b/plugins/test-plugin/lua/main.lua index fed5872..553c6e0 100644 --- a/plugins/test-plugin/lua/main.lua +++ b/plugins/test-plugin/lua/main.lua @@ -1,14 +1,3 @@ local plugin = {} -function plugin.run() - local window = berry.ui.createWindow("test window") - berry.ui.app.mount(window) - berry.ui.runOnEvent(window.Initialized, function() - local test = berry.ui.createWidget("Static", "the absolute minimal experience") - window.mount_in_window(test) - end) - - -end - return plugin \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..e69de29