working on multiple editor types

This commit is contained in:
2026-05-16 17:50:51 +10:00
parent 44c3ebe887
commit 7df6ce10c7
10 changed files with 411 additions and 375 deletions

View File

@@ -14,4 +14,5 @@ theme_mappings = {
"yaml": "yaml", "yaml": "yaml",
"md": "markdown", "md": "markdown",
"gitignore": "markdown", "gitignore": "markdown",
"lua": "lua"
} }

View File

@@ -9,13 +9,18 @@ import os, shutil
class CustomDirectoryTree(DirectoryTree): class CustomDirectoryTree(DirectoryTree):
ICON_NODE = ""
ICON_NODE_EXPANDED = ""
ICON_FILE = ""
def __init__(self, path, *, name = None, id = None, classes = None, disabled = False): def __init__(self, path, *, name = None, id = None, classes = None, disabled = False):
super().__init__(path, name=name, id=id, classes=classes, disabled=disabled) super().__init__(path, name=name, id=id, classes=classes, disabled=disabled)
self.right_clicked_node: TreeNode | None = None self.right_clicked_node: TreeNode | None = None
def context_menu_chosen(self, result): def context_menu_chosen(self, file_name: str, result: str):
if result == "Delete": if result == "Delete":
def delete_confirm(will_delete: bool | None): def delete_confirm(will_delete: bool | None):
if will_delete == True: if will_delete == True:
@@ -44,7 +49,7 @@ class CustomDirectoryTree(DirectoryTree):
self.right_clicked_node = None self.right_clicked_node = None
self.app.push_screen(Prompt(f"Enter the new name for \"{self.right_clicked_node.label}\".", "string", "Rename"), rename_confirm) self.app.push_screen(Prompt(f"Enter the new name for \"{self.right_clicked_node.label}\".", "string", "Rename"), rename_confirm)
elif result == "New Folder": elif result == "New folder":
def new_folder(folder_name: str | None): def new_folder(folder_name: str | None):
if folder_name == None: return if folder_name == None: return
if folder_name.strip() == "": if folder_name.strip() == "":
@@ -60,7 +65,7 @@ class CustomDirectoryTree(DirectoryTree):
return return
self.app.push_screen(Prompt("Enter the name of the new folder.", "string", "Create New Folder"), new_folder) self.app.push_screen(Prompt("Enter the name of the new folder.", "string", "Create New Folder"), new_folder)
elif result == "New File": elif result == "New file":
def new_file(file_name: str | None): def new_file(file_name: str | None):
if file_name == None: return if file_name == None: return
if file_name.strip() == "": if file_name.strip() == "":
@@ -82,6 +87,24 @@ class CustomDirectoryTree(DirectoryTree):
return return
self.app.push_screen(Prompt("Enter the name of the new file", "string", "Create New File"), new_file) self.app.push_screen(Prompt("Enter the name of the new file", "string", "Create New File"), new_file)
elif result == "Open with...":
file_extension = file_name.rsplit(".")[1]
default_editors = eval(self.app.config_handler.get("editor", "default_editors"))
default_editor = default_editors.get(file_extension, "Text Editor")
def open_with(chosen_editor):
self.notify(str(chosen_editor))
self.app.push_screen(Prompt(
"Editor for file:",
"list",
"Choose editor for file",
values=[
default_editor + " (Default)",
"Text Editor"
],
allow_blank=False
), open_with)
def on_mouse_down(self, event: MouseDown): def on_mouse_down(self, event: MouseDown):
if event.button != 3 or not "line" in event.style.meta: if event.button != 3 or not "line" in event.style.meta:
@@ -89,16 +112,18 @@ class CustomDirectoryTree(DirectoryTree):
selected_node = self.get_node_at_line(event.style.meta["line"]) selected_node = self.get_node_at_line(event.style.meta["line"])
self.right_clicked_node = selected_node self.right_clicked_node = selected_node
spacer = NoSelectStatic(f'[d]{"-" * 17}[/]')
options = None options = None
if self._safe_is_dir(self.right_clicked_node.data.path): if self._safe_is_dir(self.right_clicked_node.data.path):
options = ["New Folder", "New File", NoSelectStatic(f'[d]{"-" * 17}[/]'), "Delete", "Rename"] options = ["New folder", "New file", spacer, "Delete", "Rename"]
else: else:
options = ["Delete", "Rename"] options = ["Delete", "Rename", spacer, "Open with..."]
file_name = str(self.right_clicked_node.label) if len(self.right_clicked_node.label) <= 17 else self.right_clicked_node.label[:14] + "..." file_name = str(self.right_clicked_node.label) if len(self.right_clicked_node.label) <= 17 else self.right_clicked_node.label[:14] + "..."
self.app.push_screen(ContextMenu( self.app.push_screen(ContextMenu(
[NoSelectStatic(f"[b]{file_name}[/]"), NoSelectStatic(f'[d]{"-" * 17}[/]')] + options, [NoSelectStatic(f"[b]{file_name}[/]"), NoSelectStatic(f'[d]{"-" * 17}[/]')] + options,
event.screen_offset event.screen_offset
), self.context_menu_chosen) ), lambda result: self.context_menu_chosen(file_name, result))

67
main.py
View File

@@ -50,8 +50,8 @@ class Berry(App):
yield Header() yield Header()
with Vertical(id="sidebar"): with Vertical(id="sidebar"):
with HorizontalGroup(id="sidebar-buttons"): with HorizontalGroup(id="sidebar-buttons"):
yield Button("📂") yield Button("")
yield Button("🔍") yield Button("")
with ContentSwitcher(initial="files", id="sidebar-switcher"): with ContentSwitcher(initial="files", id="sidebar-switcher"):
with Vertical(id="files"): with Vertical(id="files"):
@@ -200,8 +200,27 @@ class Berry(App):
else: else:
tabs.active = self.file_tabs[str(event.path)].id tabs.active = self.file_tabs[str(event.path)].id
def open_text_editor(self, file_content: bytes, file_extension: str):
code_editor: TextArea = self.query_one("#code-editor")
code_editor.language = theme_mappings.get(file_extension, None)
try:
code_editor.text = file_content.decode("utf-8")
code_editor.disabled = False
except UnicodeDecodeError:
code_editor.text = "This file is in binary, it can't be opened. Sorry."
code_editor.language = None
code_editor.disabled = True
code_editor.focus()
def open_editor(self, file_extension: str, file_content: str):
default_editors: dict = eval(self.config_handler.get("editor", "default_editors"))
editor_for_file = default_editors.get(file_extension, None)
if not editor_for_file:
self.open_text_editor(file_content, file_extension)
@on(Tabs.TabActivated) @on(Tabs.TabActivated)
def on_tab_shown(self, event: Tabs.TabActivated): def on_tab_shown(self, event: Tabs.TabActivated):
@@ -213,48 +232,32 @@ class Berry(App):
self.switching = True self.switching = True
code_editor: TextArea = self.query_one("#code-editor") content = b""
file_extension = ""
if self.open_file: if self.open_file:
try: try:
f = open(self.open_file, "r", encoding="utf-8") f = open(self.open_file, "rb")
except Exception as e: except Exception as e:
self.notify(f"Failed to open the file: {e}") self.notify(f"Failed to open the file: {e}")
return return
# if the file is unsaved, use the content we wrote into it
# otherwise, read the CURRENT state of the file
try:
if self.open_file in self.unsaved_files: if self.open_file in self.unsaved_files:
code_editor.text = self.unsaved_files[self.open_file]["current"] content = self.unsaved_files[self.open_file]["current"]
else: else:
code_editor.text = f.read() content = f.read()
file_extension = self.open_file # get the file extension
dot_count = file_extension.count(".") file_extension = self.open_file.split(".")[-1] if "." in self.open_file else ""
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 opened. Sorry."
code_editor.language = None
code_editor.disabled = True
# close the file
f.close() f.close()
else:
code_editor.text = ""
code_editor.language = None
code_editor.disabled = False
code_editor.focus() self.open_editor(file_extension, content)
@on(Window.Minimized) @on(Window.Minimized)

View File

@@ -71,7 +71,14 @@ class PluginLoader(Window):
setattr(self.app, f"action_{action_name}", wrapper) setattr(self.app, f"action_{action_name}", wrapper)
def create_widget(self, widget_type: str, *args): def create_widget(self, widget_type: str, *args):
return getattr(textual.widgets, widget_type)(*args) new_widget = getattr(textual.widgets, widget_type)(*args)
def on_func(event_name: str, function):
self.event_handler.subscribe(new_widget, event_name, function)
new_widget.message_signal.subscribe(self, lambda e: self.event_handler.message_handler(new_widget, e))
setattr(new_widget, "on", on_func)
return new_widget
def run_on_message(self, widget, event, function): def run_on_message(self, widget, event, function):
self.event_handler.subscribe(widget, event, function) self.event_handler.subscribe(widget, event, function)

View File

@@ -1,14 +0,0 @@
local plugin = {}
function plugin.run()
--berry.config.defineSetting("my plugin", "test setting", "description", "boolean", "1")
button = berry.ui.button("Click me!", "success")
berry.ui.mount(button)
berry.ui.onMessage(button, "Pressed", function()
print("hi")
end)
end
return plugin

View File

@@ -0,0 +1,7 @@
local plugin = {}
function plugin.run()
berry.ui.notify("hi lmao")
end
return plugin

View File

@@ -1,5 +1,5 @@
{ {
"name": "Git", "name": "Markdown Reader",
"author": "SpookyDervish", "author": "SpookyDervish",
"version": "1.0.0", "version": "1.0.0",
"dependencies": [] "dependencies": []

View File

@@ -1,6 +1,6 @@
from textual.screen import ModalScreen from textual.screen import ModalScreen
from textual.containers import Vertical from textual.containers import Vertical
from textual.widgets import Static, Button, Input from textual.widgets import Static, Button, Input, Select
from textual.containers import HorizontalGroup, Center from textual.containers import HorizontalGroup, Center
from textual.binding import Binding from textual.binding import Binding
from typing import Literal from typing import Literal
@@ -58,23 +58,28 @@ class Prompt(ModalScreen):
self.dismiss(False) self.dismiss(False)
elif event.button.id == "confirm-string": elif event.button.id == "confirm-string":
self.dismiss(self.query_one("#input").value) self.dismiss(self.query_one("#input").value)
elif event.button.id == "confirm-list":
self.dismiss(self.query_one("#select").value)
def __init__(self, question: str, prompt_type: Literal["confirm", "string"], title: str = "Confirm"): def __init__(self, question: str, prompt_type: Literal["confirm", "string"], title: str = "Confirm", **kwargs):
super().__init__() super().__init__()
self.question = question self.question = question
self.window_title = title self.window_title = title
self.prompt_type = prompt_type self.prompt_type = prompt_type
self.kwargs = kwargs
def compose(self): def compose(self):
with Vertical(id="window") as window: with Vertical(id="window") as window:
window.border_title = self.window_title window.border_title = self.window_title
yield Static(self.question, id="question") yield Static(self.question, id="question")
if self.prompt_type == "confirm":
with HorizontalGroup(id="bottom"): with HorizontalGroup(id="bottom"):
if self.prompt_type == "confirm":
yield Button("Yes", variant="success", id="yes") yield Button("Yes", variant="success", id="yes")
yield Button("No", variant="error", id="no") yield Button("No", variant="error", id="no")
elif self.prompt_type == "string": elif self.prompt_type == "string":
with HorizontalGroup(id="bottom"): yield Input(id="input", **self.kwargs)
yield Input(id="input", max_length=30)
yield Button("Confirm", variant="success", id="confirm-string") yield Button("Confirm", variant="success", id="confirm-string")
elif self.prompt_type == "list":
yield Select.from_values(**self.kwargs, id="select")
yield Button("Confirm", variant="success", id="confirm-list")

View File

@@ -93,7 +93,8 @@ class ConfigHandler:
} }
self.config["editor"] = { self.config["editor"] = {
"word_wrap": "0", "word_wrap": "0",
"line_numbers": "1" "line_numbers": "1",
"default_editors": {}
} }
self.config["plugins"] = { self.config["plugins"] = {
"enabled": "1", "enabled": "1",

1
test.py Normal file
View File

@@ -0,0 +1 @@
print("Hello, World!")