286 lines
7.9 KiB
Python
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() |