continued adding support for other text editors, added a home screen
This commit is contained in:
103
home_page.py
Normal file
103
home_page.py
Normal 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
64
main.py
@@ -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
|
||||||
|
|||||||
@@ -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": []
|
||||||
}
|
}
|
||||||
10
settings.py
10
settings.py
@@ -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)))
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user