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 home_page import HomePage
from assets.theme_mappings import theme_mappings
from plugin_loader import PluginLoader
from settings import SettingsScreen
@@ -32,19 +34,32 @@ 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),
Binding("ctrl+f1", "settings", "Settings")
]
def __init__(self, path: str):
self.config_handler = ConfigHandler(self)
self.bindings = eval(self.config_handler.get("editor", "bindings"))
super().__init__()
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:
yield Header()
@@ -59,14 +74,18 @@ class Berry(App):
yield CustomDirectoryTree(self.path, id="directory")
with Vertical(id="editor"):
first_tab = Tab("New File")
first_tab.file_path = None
first_tab = Tab("Home")
first_tab.file_path = "berry://home"
self.file_tabs["berry://home"] = first_tab
yield Tabs(
first_tab,
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":
#with Vertical(id="console-container"):
@@ -201,7 +220,9 @@ class Berry(App):
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 = 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)
@@ -215,10 +236,12 @@ class Berry(App):
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"))
editor_for_file = default_editors.get(file_extension, None)
self.current_editor.remove_children()
if not editor_for_file:
self.open_text_editor(file_content, file_extension)
@@ -230,8 +253,12 @@ class Berry(App):
else:
self.open_file = getattr(event.tab, "file_path")
if self.open_file and self.open_file.startswith(r"berry://"):
return
self.switching = True
content = b""
file_extension = ""
@@ -265,8 +292,6 @@ class Berry(App):
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
@@ -301,10 +326,9 @@ class Berry(App):
self.open_file = None
self.switching = True
code_editor: TextArea = self.query_one("#code-editor")
code_editor.disabled = False
code_editor.text = ""
# open empty editor
self.open_editor("", b"")
def done_saving(self, result):
if result is None: return
@@ -342,8 +366,6 @@ class Berry(App):
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

View File

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

View File

@@ -1,5 +1,5 @@
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.binding import Binding
@@ -70,11 +70,15 @@ class SettingsScreen(ModalScreen):
self.dismiss()
def on_switch_changed(self, event: Switch.Changed):
text_area = self.app.current_editor.query_one_optional(TextArea)
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)))
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)))
elif event.switch.id == "plugins-enabled":
self.app.config_handler.set("plugins", "enabled", str(int(event.value)))

View File

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