continued adding support for other text editors, added a home screen

This commit is contained in:
2026-05-16 21:18:21 +10:00
parent 7df6ce10c7
commit fc07ffbfa8
6 changed files with 255 additions and 116 deletions

103
home_page.py Normal file
View File

@@ -0,0 +1,103 @@
from textual.containers import Center, Vertical, Horizontal
from textual.widgets import Rule, Static
from textual import on
from textualeffects.widgets import EffectLabel
from textualeffects.effects import effects
from terminaltexteffects.utils.graphics import Color
class HomePage(Vertical):
LOGO = r""" ______ ______ ______ ______ __ __
/\ == \ /\ ___\ /\ == \ /\ == \ /\ \_\ \
\ \ __< \ \ __\ \ \ __< \ \ __< \ \____ \
\ \_____\ \ \_____\ \ \_\ \_\ \ \_\ \_\ \/\_____\
\/_____/ \/_____/ \/_/ /_/ \/_/ /_/ \/_____/
"""
DEFAULT_CSS = """
HomePage {
content-align: center top;
Static {
text-align: center;
}
#banner {
height: 5; # probs will need to be updated
margin-bottom: 1;
Rule {
color: $surface-lighten-2;
height: 100%;
content-align: center middle;
margin: 0 3;
}
EffectLabel {
height: 100%;
}
}
#bindings {
Horizontal {
max-width: 50;
width: 50%;
margin-top: 1;
height: 1;
.bind-name {
width: 85%;
}
.bind-keys {
width: 15%;
}
Static {
text-align: left;
link-background: transparent;
link-color: $primary;
}
}
}
}
"""
@on(EffectLabel.EffectFinished)
def restart_effect(self):
self.banner_label.run_worker(self.banner_label.run_effect, exclusive=True)
def compose(self):
with Horizontal(id="banner"):
yield Rule()
self.banner_label = EffectLabel(self.LOGO, effect="Highlight", config={
"final_gradient_stops": [Color("#f263ff"), Color("#946ff2")],
"cycles": 0
})
yield self.banner_label
yield Rule()
yield Static("[dim]The text editor that no one asked for and didn't need.[/]")
# bindings
with Center(id="bindings"):
binds = self.app.bindings
with Horizontal():
yield Static("[b] New file[/]", classes="bind-name")
yield Static(f"[@click=app.new]{binds["new"]}[/]", classes="bind-keys")
with Horizontal():
yield Static("[b] Open file[/]", classes="bind-name")
yield Static(f"[@click=app.open]{binds["open"]}[/]", classes="bind-keys")
with Horizontal():
yield Static("[b] Settings[/]", classes="bind-name")
yield Static(f"[@click=app.settings]{binds["settings"]}[/]", classes="bind-keys")

64
main.py
View File

@@ -8,6 +8,8 @@ from textual_fspicker import FileOpen, FileSave
from pathlib import Path from pathlib import Path
from home_page import HomePage
from assets.theme_mappings import theme_mappings from assets.theme_mappings import theme_mappings
from plugin_loader import PluginLoader from plugin_loader import PluginLoader
from settings import SettingsScreen from settings import SettingsScreen
@@ -32,19 +34,32 @@ class Berry(App):
CSS_PATH = "assets/style.tcss" CSS_PATH = "assets/style.tcss"
SUB_TITLE = "New File" 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),
Binding("ctrl+f1", "settings", "Settings")
]
def __init__(self, path: str): def __init__(self, path: str):
self.config_handler = ConfigHandler(self)
self.bindings = eval(self.config_handler.get("editor", "bindings"))
super().__init__() super().__init__()
self.path = path self.path = path
self.config_handler = ConfigHandler(self)
self.BINDINGS = (
Binding(self.bindings["new"], "new", "New File"),
Binding(self.bindings["open"], "open", "Open File"),
Binding(self.bindings["save"], "save", "Save File"),
Binding(self.bindings["save-as"], "save_as", "Save File As..."),
Binding(self.bindings["find"], "find", "Find and Replace"),
Binding(self.bindings["settings"], "settings", "Settings"),
)
for binding in self.BINDINGS:
self._bindings._add_binding(binding)
self.refresh_bindings()
self.current_editor = None
self.file_tabs = {}
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header() yield Header()
@@ -59,14 +74,18 @@ class Berry(App):
yield CustomDirectoryTree(self.path, id="directory") yield CustomDirectoryTree(self.path, id="directory")
with Vertical(id="editor"): with Vertical(id="editor"):
first_tab = Tab("New File") first_tab = Tab("Home")
first_tab.file_path = None first_tab.file_path = "berry://home"
self.file_tabs["berry://home"] = first_tab
yield Tabs( yield Tabs(
first_tab, first_tab,
id="file-tabs" id="file-tabs"
) )
yield TextArea.code_editor(placeholder="This file is empty.", theme="css", id="code-editor", disabled=True, soft_wrap=True, show_line_numbers=bool(int(self.config_handler.get("editor", "line_numbers"))))
with Vertical() as editor:
yield HomePage()
self.current_editor = editor
#if os.name == "nt": #if os.name == "nt":
#with Vertical(id="console-container"): #with Vertical(id="console-container"):
@@ -201,7 +220,9 @@ class Berry(App):
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): def open_text_editor(self, file_content: bytes, file_extension: str):
code_editor: TextArea = self.query_one("#code-editor") code_editor = TextArea.code_editor(placeholder="This file is empty.", theme="css", disabled=True, soft_wrap=bool(int(self.config_handler.get("editor", "word_wrap"))), show_line_numbers=bool(int(self.config_handler.get("editor", "line_numbers"))))
self.current_editor.mount(code_editor)
code_editor.language = theme_mappings.get(file_extension, None) code_editor.language = theme_mappings.get(file_extension, None)
@@ -215,10 +236,12 @@ class Berry(App):
code_editor.focus() code_editor.focus()
def open_editor(self, file_extension: str, file_content: str): def open_editor(self, file_extension: str, file_content: bytes):
default_editors: dict = eval(self.config_handler.get("editor", "default_editors")) default_editors: dict = eval(self.config_handler.get("editor", "default_editors"))
editor_for_file = default_editors.get(file_extension, None) editor_for_file = default_editors.get(file_extension, None)
self.current_editor.remove_children()
if not editor_for_file: if not editor_for_file:
self.open_text_editor(file_content, file_extension) self.open_text_editor(file_content, file_extension)
@@ -230,8 +253,12 @@ class Berry(App):
else: else:
self.open_file = getattr(event.tab, "file_path") self.open_file = getattr(event.tab, "file_path")
if self.open_file and self.open_file.startswith(r"berry://"):
return
self.switching = True self.switching = True
content = b"" content = b""
file_extension = "" file_extension = ""
@@ -265,8 +292,6 @@ class Berry(App):
event.window.remove_window() event.window.remove_window()
def on_text_area_changed(self, event: TextArea.Changed): def on_text_area_changed(self, event: TextArea.Changed):
if event.text_area.id != "code-editor":
return
if self.switching: if self.switching:
self.switching = False self.switching = False
return return
@@ -301,10 +326,9 @@ class Berry(App):
self.open_file = None self.open_file = None
self.switching = True self.switching = True
code_editor: TextArea = self.query_one("#code-editor")
code_editor.disabled = False # open empty editor
code_editor.text = "" self.open_editor("", b"")
def done_saving(self, result): def done_saving(self, result):
if result is None: return if result is None: return
@@ -342,8 +366,6 @@ class Berry(App):
return super().action_quit() return super().action_quit()
def on_ready(self): def on_ready(self):
# src/main.py: Tab<>
self.file_tabs = {}
self.open_file = None self.open_file = None
self.unsaved_files = {} # list of paths self.unsaved_files = {} # list of paths
self.switching = False self.switching = False

View File

@@ -2,5 +2,6 @@
"name": "Markdown Reader", "name": "Markdown Reader",
"author": "SpookyDervish", "author": "SpookyDervish",
"version": "1.0.0", "version": "1.0.0",
"description": "",
"dependencies": [] "dependencies": []
} }

View File

@@ -1,5 +1,5 @@
from textual.screen import ModalScreen from textual.screen import ModalScreen
from textual.widgets import Label, Select, TabbedContent, TabPane, Switch, Input, Rule, Static from textual.widgets import Label, Select, TextArea, TabbedContent, TabPane, Switch, Input, Rule, Static
from textual.containers import Vertical, HorizontalGroup, VerticalGroup, VerticalScroll from textual.containers import Vertical, HorizontalGroup, VerticalGroup, VerticalScroll
from textual.binding import Binding from textual.binding import Binding
@@ -70,11 +70,15 @@ class SettingsScreen(ModalScreen):
self.dismiss() self.dismiss()
def on_switch_changed(self, event: Switch.Changed): def on_switch_changed(self, event: Switch.Changed):
text_area = self.app.current_editor.query_one_optional(TextArea)
if event.switch.id == "word-wrap": if event.switch.id == "word-wrap":
self.app.query_one("#code-editor").soft_wrap = event.value if text_area:
text_area.soft_wrap = event.value
self.app.config_handler.set("editor", "word_wrap", str(int(event.value))) self.app.config_handler.set("editor", "word_wrap", str(int(event.value)))
elif event.switch.id == "line-numbers": elif event.switch.id == "line-numbers":
self.app.query_one("#code-editor").show_line_numbers = event.value if text_area:
text_area.show_line_numbers = event.value
self.app.config_handler.set("editor", "line_numbers", str(int(event.value))) self.app.config_handler.set("editor", "line_numbers", str(int(event.value)))
elif event.switch.id == "plugins-enabled": elif event.switch.id == "plugins-enabled":
self.app.config_handler.set("plugins", "enabled", str(int(event.value))) self.app.config_handler.set("plugins", "enabled", str(int(event.value)))

View File

@@ -6,104 +6,114 @@ from typing import Any
class ConfigHandler: class ConfigHandler:
def __init__(self, app: App): def __init__(self, app: App):
self.app: App = app self.app: App = app
self.config: configparser.ConfigParser = configparser.ConfigParser() self.config: configparser.ConfigParser = configparser.ConfigParser()
self.plugin_defined_settings = {} self.plugin_defined_settings = {}
self.config_dir: str = self.ensure_hidden_config_dir() self.config_dir: str = self.ensure_hidden_config_dir()
self.load_settings() self.load_settings()
def define_plugin_setting(self, plugin_name: str, option_name: str, description: str, option_type: str, default_value: Any, on_changed_func = None): def define_plugin_setting(self, plugin_name: str, option_name: str, description: str, option_type: str, default_value: Any, on_changed_func = None):
actual_type = None actual_type = None
if option_type == "boolean": if option_type == "boolean":
actual_type = bool actual_type = bool
elif option_type == "string": elif option_type == "string":
actual_type = str actual_type = str
elif option_type == "float": elif option_type == "float":
actual_type = float actual_type = float
elif option_type == "integer": elif option_type == "integer":
actual_type = int actual_type = int
else: else:
raise Exception(f"Invalid type name \"{option_type}\".") raise Exception(f"Invalid type name \"{option_type}\".")
if plugin_name in self.plugin_defined_settings: if plugin_name in self.plugin_defined_settings:
self.plugin_defined_settings[plugin_name][option_name] = { self.plugin_defined_settings[plugin_name][option_name] = {
"option_name": option_name, "option_name": option_name,
"description": description, "description": description,
"type": actual_type, "type": actual_type,
"default_value": default_value, "default_value": default_value,
"on_changed_func": on_changed_func "on_changed_func": on_changed_func
} }
else: else:
self.plugin_defined_settings[plugin_name] = {option_name: { self.plugin_defined_settings[plugin_name] = {option_name: {
"option_name": option_name, "option_name": option_name,
"description": description, "description": description,
"type": actual_type, "type": actual_type,
"default_value": default_value, "default_value": default_value,
"on_changed_func": on_changed_func "on_changed_func": on_changed_func
}} }}
# user hasnt used this plugin before # user hasnt used this plugin before
if self.get("plugin_" + plugin_name, option_name) == None: if self.get("plugin_" + plugin_name, option_name) == None:
self.set("plugin_" + plugin_name, option_name, default_value) self.set("plugin_" + plugin_name, option_name, default_value)
def ensure_hidden_config_dir(self): def ensure_hidden_config_dir(self):
config_dir = Path.home() / ".berry" config_dir = Path.home() / ".berry"
config_dir.mkdir(parents=True, exist_ok=True) config_dir.mkdir(parents=True, exist_ok=True)
return config_dir return config_dir
def get(self, section: str, option: str): def get(self, section: str, option: str):
if not self.config.has_section(section): if not self.config.has_section(section):
self.config.add_section(section) self.config.add_section(section)
if not self.config.has_option(section, option): if not self.config.has_option(section, option):
self.set(section, option, "0") self.set(section, option, "0")
return "0" return "0"
return self.config.get(section, option, fallback=None) return self.config.get(section, option, fallback=None)
def set(self, section: str, option: str, new_value: str): def set(self, section: str, option: str, new_value: str):
if not self.config.has_section(section): if not self.config.has_section(section):
self.config.add_section(section) self.config.add_section(section)
self.config.set(section, option, new_value) self.config.set(section, option, new_value)
if section.startswith("plugin_"): if section.startswith("plugin_"):
section = section.removeprefix("plugin_") section = section.removeprefix("plugin_")
if self.plugin_defined_settings[section].get("on_changed_func", None) != None: if self.plugin_defined_settings[section].get("on_changed_func", None) != None:
self.plugin_defined_settings[section]["on_changed_func"](new_value) self.plugin_defined_settings[section]["on_changed_func"](new_value)
self.write_settings() self.write_settings()
def apply_settings(self): def apply_settings(self):
self.app.query_one("#code-editor").soft_wrap = bool(int(self.get("editor", "word_wrap"))) if self.app.current_editor and self.app.current_editor.id == "code-editor":
self.app.theme = self.get("appearance", "colour_theme") self.app.current_editor.soft_wrap = bool(int(self.get("editor", "word_wrap")))
def write_settings(self): self.app.theme = self.get("appearance", "colour_theme")
with open(self.config_dir / "config.ini", "w") as configfile:
self.config.write(configfile)
def load_settings(self): def write_settings(self):
if os.path.isfile(self.config_dir / "config.ini"): with open(self.config_dir / "config.ini", "w") as configfile:
self.config.read(self.config_dir / "config.ini") self.config.write(configfile)
return
self.config["config"] = { def load_settings(self):
"version": "1" if os.path.isfile(self.config_dir / "config.ini"):
} self.config.read(self.config_dir / "config.ini")
self.config["editor"] = { return
"word_wrap": "0",
"line_numbers": "1",
"default_editors": {}
}
self.config["plugins"] = {
"enabled": "1",
"log": "1",
"log_timeout": "10"
}
self.config["appearance"] = {
"colour_theme": "textual-dark"
}
with open(self.config_dir / "config.ini", "w") as configfile: self.config["config"] = {
self.config.write(configfile) "version": "1"
}
self.config["editor"] = {
"word_wrap": "0",
"line_numbers": "1",
"default_editors": {},
"bindings": {
"open": "ctrl+o",
"new": "ctrl+n",
"save": "ctrl+s",
"save-as": "ctrl+shift+s",
"find": "ctrl+f",
"settings": "ctrl+f1"
}
}
self.config["plugins"] = {
"enabled": "1",
"log": "1",
"log_timeout": "10"
}
self.config["appearance"] = {
"colour_theme": "textual-dark"
}
with open(self.config_dir / "config.ini", "w") as configfile:
self.config.write(configfile)

View File

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