adasdasdasdasd Ctrl+O action

This commit is contained in:
2025-10-29 12:40:28 +11:00
parent 63614988f2
commit f8f18e6431
3 changed files with 353 additions and 325 deletions

667
main.py
View File

@@ -1,315 +1,354 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, ContentSwitcher, DirectoryTree, Static, Button, TextArea, Tabs, Tab, RichLog, Input from textual.widgets import Header, Footer, ContentSwitcher, DirectoryTree, Static, Button, TextArea, Tabs, Tab, RichLog, Input
from textual.containers import HorizontalGroup, Vertical from textual.containers import HorizontalGroup, Vertical
from textual.binding import Binding from textual.binding import Binding
from textual_window import Window from textual_window import Window
from textual import on from textual import on
from assets.theme_mappings import theme_mappings from textual_fspicker import FileOpen, FileSave
from textual_fspicker import FileOpen, FileSave from pathlib import Path
from plugin_loader import PluginLoader from assets.theme_mappings import theme_mappings
from plugin_loader import PluginLoader
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import subprocess
import os import subprocess
import os
class Watcher(FileSystemEventHandler):
def __init__(self, app: App): class Watcher(FileSystemEventHandler):
super().__init__() def __init__(self, app: App):
self.app = app super().__init__()
self.app = app
async def on_any_event(self, event):
await self.app.query_one(DirectoryTree).reload() async def on_any_event(self, event):
await self.app.query_one(DirectoryTree).reload()
class Berry(App):
CSS_PATH = "assets/style.tcss" class Berry(App):
SUB_TITLE = "New File" CSS_PATH = "assets/style.tcss"
SUB_TITLE = "New File"
BINDINGS = [
Binding("ctrl+o", "open", "Open File"), BINDINGS = [
Binding("ctrl+n", "new", "New File"), Binding("ctrl+o", "open", "Open File"),
Binding("ctrl+s", "save", "Save"), Binding("ctrl+n", "new", "New File"),
Binding("ctrl+shift+s", "save_as", "Save As...", priority=True), Binding("ctrl+s", "save", "Save"),
Binding("ctrl+f", "find", "Find", priority=True) Binding("ctrl+shift+s", "save_as", "Save As...", priority=True),
] Binding("ctrl+f", "find", "Find", priority=True)
]
def __init__(self, path: str):
super().__init__() def __init__(self, path: str):
self.path = path super().__init__()
self.path = path
def compose(self) -> ComposeResult:
yield Header() def compose(self) -> ComposeResult:
with Vertical(id="sidebar"): yield Header()
with HorizontalGroup(id="sidebar-buttons"): with Vertical(id="sidebar"):
yield Button("📂") with HorizontalGroup(id="sidebar-buttons"):
yield Button("🔍") yield Button("📂")
yield Button("🔍")
with ContentSwitcher(initial="files"):
with Vertical(id="files"): with ContentSwitcher(initial="files"):
yield Static("EXPLORER") with Vertical(id="files"):
yield DirectoryTree(self.path, id="directory") yield Static("EXPLORER")
yield DirectoryTree(self.path, id="directory")
with Vertical(id="editor"):
first_tab = Tab("New File") with Vertical(id="editor"):
first_tab.file_path = None first_tab = Tab("New File")
first_tab.file_path = None
yield Tabs(
first_tab, yield Tabs(
id="file-tabs" first_tab,
) id="file-tabs"
yield TextArea.code_editor(placeholder="This file is empty.", theme="css", id="code-editor", disabled=True) )
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"): #if os.name == "nt":
yield RichLog(id="console") with Vertical(id="console-container"):
yield Input(placeholder="> ", id="console-input") yield RichLog(id="console")
#else: yield Input(placeholder="> ", id="console-input")
# yield Terminal(command="bash", id="terminal") #else:
# yield Terminal(command="bash", id="terminal")
yield Footer()
yield PluginLoader() yield Footer()
yield PluginLoader()
def on_input_submitted(self, event: Input.Submitted):
if event.input.id != "console-input": def on_input_submitted(self, event: Input.Submitted):
return if event.input.id != "console-input":
return
self.run_command(event.input.value)
event.input.clear() self.run_command(event.input.value)
event.input.clear()
def run_command(self, command: str):
console = self.query_one("#console") def run_command(self, command: str):
console = self.query_one("#console")
console.write(f"> {command}")
console.write(f"> {command}")
if command == "clear" or command == "cls":
console.clear() if command == "clear" or command == "cls":
else: console.clear()
try: else:
result = subprocess.check_output(command, shell=True, text=True, stderr=subprocess.STDOUT) try:
console.write(result) result = subprocess.check_output(command, shell=True, text=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e: console.write(result)
console.write(e.stdout) except subprocess.CalledProcessError as e:
console.write(e.stdout)
self.query_one("#console-input").focus()
self.query_one("#console-input").focus()
def action_find(self):
try: async def chose_file_to_open(self, result):
self.query_one("#find-window") if result == None: return
return result = str(result)
except: if self.open_file == result:
return
find_window = Window(
def is_within_directory(file_path: str, directory: str) -> bool:
Vertical( file_path = Path(file_path).resolve()
HorizontalGroup( directory = Path(directory).resolve()
Input(placeholder="Find"), return directory in file_path.parents
Static("0 of 0", id="num-matches"),
Button("", flat=True), self.switching = True
Button("", flat=True),
), tabs: Tabs = self.query_one("#file-tabs")
HorizontalGroup(
Input(placeholder="Replace"), 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)
icon="🔍",
start_open=True, self.open_file = result
allow_resize=False, inside_dir = is_within_directory(result, self.path)
allow_maximize=False,
id="find-window", self.sub_title = os.path.basename(self.open_file) if inside_dir else self.open_file
mode="temporary",
name="Find & Replace" 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)
self.mount(find_window) setattr(new_tab, "file_path", result)
await tabs.add_tab(new_tab)
async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected): self.file_tabs[result] = new_tab
if self.open_file == str(event.path): tabs.active = new_tab.id
return else:
tabs.active = self.file_tabs[result].id
self.file_clicked = True
self.switching = True def action_open(self):
self.app.push_screen(FileOpen(), self.chose_file_to_open)
tabs: Tabs = self.query_one("#file-tabs")
def action_find(self):
if self.open_file not in self.unsaved_files: try:
if self.open_file: self.query_one("#find-window")
self.file_tabs.pop(self.open_file) return
tabs.remove_tab(tabs.active_tab) except:
self.open_file = str(event.path) find_window = Window(
self.sub_title = os.path.basename(self.open_file)
Vertical(
if str(event.path) not in self.file_tabs: HorizontalGroup(
new_tab = Tab(os.path.basename(str(event.path))) Input(placeholder="Find"),
new_tab.tooltip = str(new_tab.label) Static("0 of 0", id="num-matches"),
setattr(new_tab, "file_path", str(event.path)) Button("", flat=True),
await tabs.add_tab(new_tab) Button("", flat=True),
self.file_tabs[str(event.path)] = new_tab ),
tabs.active = new_tab.id HorizontalGroup(
else: Input(placeholder="Replace"),
tabs.active = self.file_tabs[str(event.path)].id ),
),
icon="🔍",
start_open=True,
@on(Tabs.TabActivated) allow_resize=False,
def on_tab_shown(self, event: Tabs.TabActivated): allow_maximize=False,
id="find-window",
if self.file_clicked: mode="temporary",
self.file_clicked = False name="Find & Replace"
else: )
self.open_file = getattr(event.tab, "file_path")
self.mount(find_window)
self.switching = True
async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected):
code_editor: TextArea = self.query_one("#code-editor") if self.open_file == str(event.path):
if self.open_file: return
try:
f = open(self.open_file, "r", encoding="utf-8") self.file_clicked = True
except Exception as e: self.switching = True
self.notify(f"Failed to open the file: {e}")
return 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)
try: tabs.remove_tab(tabs.active_tab)
if self.open_file in self.unsaved_files:
code_editor.text = self.unsaved_files[self.open_file]["current"] self.open_file = str(event.path)
else: self.sub_title = os.path.basename(self.open_file)
code_editor.text = f.read()
if str(event.path) not in self.file_tabs:
file_extension = self.open_file new_tab = Tab(os.path.basename(str(event.path)))
dot_count = file_extension.count(".") new_tab.tooltip = str(new_tab.label)
setattr(new_tab, "file_path", str(event.path))
if dot_count == 1: await tabs.add_tab(new_tab)
if file_extension.startswith("."): self.file_tabs[str(event.path)] = new_tab
file_extension = file_extension.removeprefix(".") tabs.active = new_tab.id
else: else:
file_extension = file_extension.rsplit(".", 1)[1] tabs.active = self.file_tabs[str(event.path)].id
elif dot_count > 1:
file_extension = file_extension.rsplit(".", 1)[1]
code_editor.language = theme_mappings.get(file_extension, None)
code_editor.disabled = False @on(Tabs.TabActivated)
except UnicodeDecodeError: def on_tab_shown(self, event: Tabs.TabActivated):
code_editor.text = "This file is in binary, it can't be openned. Sorry."
code_editor.language = None if self.file_clicked:
code_editor.disabled = True self.file_clicked = False
else:
f.close() self.open_file = getattr(event.tab, "file_path")
else:
code_editor.text = "" self.switching = True
code_editor.language = None
code_editor.disabled = False code_editor: TextArea = self.query_one("#code-editor")
if self.open_file:
code_editor.focus() try:
f = open(self.open_file, "r", encoding="utf-8")
except Exception as e:
@on(Window.Minimized) self.notify(f"Failed to open the file: {e}")
def window_minimized(self, event: Window.Minimized): return
event.window.remove_window()
def on_text_area_changed(self, event: TextArea.Changed):
if event.text_area.id != "code-editor":
return try:
if self.switching: if self.open_file in self.unsaved_files:
self.switching = False code_editor.text = self.unsaved_files[self.open_file]["current"]
return else:
code_editor.text = f.read()
file_extension = self.open_file
tabs: Tabs = self.query_one("#file-tabs") dot_count = file_extension.count(".")
if self.open_file:
if not self.open_file in self.unsaved_files: if dot_count == 1:
with open(self.open_file, "r", encoding="utf-8") as f: if file_extension.startswith("."):
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 file_extension = file_extension.removeprefix(".")
return else:
self.unsaved_files[self.open_file] = {"current": event.text_area.text, "original": f.read()} file_extension = file_extension.rsplit(".", 1)[1]
elif dot_count > 1:
file_extension = file_extension.rsplit(".", 1)[1]
tabs.active_tab.tooltip = f"Unsaved changes in {tabs.active_tab.label}"
tabs.active_tab.label = "[d orange]●[/] " + str(tabs.active_tab.label) code_editor.language = theme_mappings.get(file_extension, None)
code_editor.disabled = False
else: except UnicodeDecodeError:
self.unsaved_files[self.open_file]["current"] = event.text_area.text code_editor.text = "This file is in binary, it can't be openned. Sorry."
if self.unsaved_files[self.open_file]["original"] == self.unsaved_files[self.open_file]["current"]: code_editor.language = None
tabs.active_tab.label = os.path.basename(self.open_file) code_editor.disabled = True
tabs.active_tab.tooltip = str(tabs.active_tab.label)
self.unsaved_files.pop(self.open_file) f.close()
else:
def action_new(self): code_editor.text = ""
tabs: Tabs = self.query_one("#file-tabs") code_editor.language = None
new_tab = Tab("New File") code_editor.disabled = False
setattr(new_tab, "file_path", None)
tabs.add_tab(new_tab) code_editor.focus()
tabs.active = new_tab.id
self.open_file = None @on(Window.Minimized)
self.switching = True def window_minimized(self, event: Window.Minimized):
code_editor: TextArea = self.query_one("#code-editor") event.window.remove_window()
code_editor.disabled = False def on_text_area_changed(self, event: TextArea.Changed):
code_editor.text = "" if event.text_area.id != "code-editor":
return
def done_saving(self, result): if self.switching:
if result is None: return self.switching = False
return
with open(result, "wb") as f:
f.write(self.query_one("#code-editor").text.encode())
tabs: Tabs = self.query_one("#file-tabs") tabs: Tabs = self.query_one("#file-tabs")
tabs.active_tab.label = os.path.basename(result) if self.open_file:
self.notify(f"Saved to {result} successfully.", title="Done!", markup=False) if not self.open_file in self.unsaved_files:
self.query_one(DirectoryTree).reload() 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
def action_save_as(self): return
self.push_screen(FileSave(), callback=self.done_saving) self.unsaved_files[self.open_file] = {"current": event.text_area.text, "original": f.read()}
def action_save(self):
if self.open_file == None and self.query_one("#code-editor").disabled == False: tabs.active_tab.tooltip = f"Unsaved changes in {tabs.active_tab.label}"
self.action_save_as() tabs.active_tab.label = "[d orange]●[/] " + str(tabs.active_tab.label)
# dont bother saving if there are no new changes else:
if not self.open_file in self.unsaved_files: return 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"]:
with open(self.open_file, "w") as f: tabs.active_tab.label = os.path.basename(self.open_file)
f.write(self.unsaved_files[self.open_file]["current"]) tabs.active_tab.tooltip = str(tabs.active_tab.label)
self.unsaved_files.pop(self.open_file)
tabs: Tabs = self.query_one("#file-tabs")
tabs.active_tab.label = os.path.basename(self.open_file) def action_new(self):
tabs.active_tab.tooltip = str(tabs.active_tab.label) tabs: Tabs = self.query_one("#file-tabs")
self.unsaved_files.pop(self.open_file) new_tab = Tab("New File")
self.notify("Saved.") setattr(new_tab, "file_path", None)
tabs.add_tab(new_tab)
def action_quit(self): tabs.active = new_tab.id
self.observer.stop()
self.observer.join() self.open_file = None
return super().action_quit() self.switching = True
code_editor: TextArea = self.query_one("#code-editor")
def on_ready(self):
# src/main.py: Tab<> code_editor.disabled = False
self.file_tabs = {} code_editor.text = ""
self.open_file = None
self.unsaved_files = {} # list of paths def done_saving(self, result):
self.switching = False if result is None: return
self.file_clicked = False
with open(result, "wb") as f:
self.observer = Observer() f.write(self.query_one("#code-editor").text.encode())
self.observer.schedule(Watcher(self), path=self.path)
self.observer.start() tabs: Tabs = self.query_one("#file-tabs")
tabs.active_tab.label = os.path.basename(result)
#if os.name == "nt": self.notify(f"Saved to {result} successfully.", title="Done!", markup=False)
self.query_one("#console").write("Run a command below.") self.query_one(DirectoryTree).reload()
#else:
# self.query_one("#terminal").start() def action_save_as(self):
self.push_screen(FileSave(), callback=self.done_saving)
if __name__ == "__main__":
app = Berry("./") 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() app.run()

View File

@@ -1,14 +1,3 @@
local plugin = {} 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 return plugin

0
test.py Normal file
View File