Files
Berry/main.py
SpookyDervish 1f81caf29f inbuilt terminal emulator is better on linux/macos now
i need to test this on mac actually
2025-10-26 19:39:14 +11:00

286 lines
7.9 KiB
Python

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 textual_terminal import Terminal
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)
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()
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)
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())
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
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()