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))

585
main.py
View File

@@ -21,341 +21,344 @@ import os, sys
class Watcher(FileSystemEventHandler): class Watcher(FileSystemEventHandler):
def __init__(self, app: App): def __init__(self, app: App):
super().__init__() super().__init__()
self.app = app self.app = app
async def on_any_event(self, event): async def on_any_event(self, event):
await self.app.query_one(DirectoryTree).reload() await self.app.query_one(DirectoryTree).reload()
class Berry(App): class Berry(App):
CSS_PATH = "assets/style.tcss" CSS_PATH = "assets/style.tcss"
SUB_TITLE = "New File" SUB_TITLE = "New File"
BINDINGS = [ BINDINGS = [
Binding("ctrl+o", "open", "Open File"), Binding("ctrl+o", "open", "Open File"),
Binding("ctrl+n", "new", "New File"), Binding("ctrl+n", "new", "New File"),
Binding("ctrl+s", "save", "Save"), Binding("ctrl+s", "save", "Save"),
Binding("ctrl+shift+s", "save_as", "Save As...", priority=True), Binding("ctrl+shift+s", "save_as", "Save As...", priority=True),
Binding("ctrl+f", "find", "Find", priority=True), Binding("ctrl+f", "find", "Find", priority=True),
Binding("ctrl+f1", "settings", "Settings") Binding("ctrl+f1", "settings", "Settings")
] ]
def __init__(self, path: str): def __init__(self, path: str):
super().__init__() super().__init__()
self.path = path self.path = path
self.config_handler = ConfigHandler(self) self.config_handler = ConfigHandler(self)
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
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"):
yield Static("EXPLORER") yield Static("EXPLORER")
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("New File")
first_tab.file_path = None first_tab.file_path = None
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")))) 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"))))
#if os.name == "nt": #if os.name == "nt":
#with Vertical(id="console-container"): #with Vertical(id="console-container"):
# yield RichLog(id="console") # yield RichLog(id="console")
# yield Input(placeholder="> ", id="console-input") # yield Input(placeholder="> ", id="console-input")
#else: #else:
# yield Terminal(command="bash", id="terminal") # yield Terminal(command="bash", id="terminal")
yield Footer() yield Footer()
if bool(int(self.config_handler.get("plugins", "enabled"))) == True: if bool(int(self.config_handler.get("plugins", "enabled"))) == True:
yield PluginLoader() yield PluginLoader()
def action_settings(self): def action_settings(self):
self.push_screen(SettingsScreen()) self.push_screen(SettingsScreen())
def get_system_commands(self, screen): def get_system_commands(self, screen):
yield SystemCommand( yield SystemCommand(
"Quit the application", "Quit the application",
"Quit the application as soon as possible", "Quit the application as soon as possible",
self.action_quit, self.action_quit,
) )
if screen.query("HelpPanel"): if screen.query("HelpPanel"):
yield SystemCommand( yield SystemCommand(
"Hide keys and help panel", "Hide keys and help panel",
"Hide the keys and widget help panel", "Hide the keys and widget help panel",
self.action_hide_help_panel, self.action_hide_help_panel,
) )
else: else:
yield SystemCommand( yield SystemCommand(
"Show keys and help panel", "Show keys and help panel",
"Show help for the focused widget and a summary of available keys", "Show help for the focused widget and a summary of available keys",
self.action_show_help_panel, self.action_show_help_panel,
) )
yield SystemCommand("Settings", "Open the settings menu", self.action_settings) yield SystemCommand("Settings", "Open the settings menu", self.action_settings)
async def chose_file_to_open(self, result): async def chose_file_to_open(self, result):
if result == None: return if result == None: return
result = str(result) result = str(result)
if self.open_file == result: if self.open_file == result:
return return
def is_within_directory(file_path: str, directory: str) -> bool: def is_within_directory(file_path: str, directory: str) -> bool:
file_path = Path(file_path).resolve() file_path = Path(file_path).resolve()
directory = Path(directory).resolve() directory = Path(directory).resolve()
return directory in file_path.parents return directory in file_path.parents
self.switching = True self.switching = True
tabs: Tabs = self.query_one("#file-tabs") tabs: Tabs = self.query_one("#file-tabs")
if self.open_file not in self.unsaved_files: if self.open_file not in self.unsaved_files:
if self.open_file: if self.open_file:
self.file_tabs.pop(self.open_file) self.file_tabs.pop(self.open_file)
tabs.remove_tab(tabs.active_tab) tabs.remove_tab(tabs.active_tab)
self.open_file = result self.open_file = result
inside_dir = is_within_directory(result, self.path) inside_dir = is_within_directory(result, self.path)
self.sub_title = os.path.basename(self.open_file) if inside_dir else self.open_file self.sub_title = os.path.basename(self.open_file) if inside_dir else self.open_file
if result not in self.file_tabs: if result not in self.file_tabs:
new_tab = Tab(os.path.basename(result) if inside_dir else result) new_tab = Tab(os.path.basename(result) if inside_dir else result)
new_tab.tooltip = str(new_tab.label) new_tab.tooltip = str(new_tab.label)
setattr(new_tab, "file_path", result) setattr(new_tab, "file_path", result)
await tabs.add_tab(new_tab) await tabs.add_tab(new_tab)
self.file_tabs[result] = new_tab self.file_tabs[result] = new_tab
tabs.active = new_tab.id tabs.active = new_tab.id
else: else:
tabs.active = self.file_tabs[result].id tabs.active = self.file_tabs[result].id
def action_open(self): def action_open(self):
self.app.push_screen(FileOpen(), self.chose_file_to_open) self.app.push_screen(FileOpen(), self.chose_file_to_open)
def action_find(self): def action_find(self):
try: try:
self.query_one("#find-window") self.query_one("#find-window")
return return
except: except:
find_window = Window( find_window = Window(
Vertical( Vertical(
HorizontalGroup( HorizontalGroup(
Input(placeholder="Find"), Input(placeholder="Find"),
Static("0 of 0", id="num-matches"), Static("0 of 0", id="num-matches"),
Button("", flat=True), Button("", flat=True),
Button("", flat=True), Button("", flat=True),
), ),
HorizontalGroup( HorizontalGroup(
Input(placeholder="Replace"), Input(placeholder="Replace"),
), ),
), ),
icon="🔍", icon="🔍",
start_open=True, start_open=True,
allow_resize=False, allow_resize=False,
allow_maximize=False, allow_maximize=False,
id="find-window", id="find-window",
mode="temporary", mode="temporary",
name="Find & Replace" name="Find & Replace"
) )
self.mount(find_window) self.mount(find_window)
async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected): async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected):
if self.open_file == str(event.path): if self.open_file == str(event.path):
return return
self.file_clicked = True self.file_clicked = True
self.switching = True self.switching = True
tabs: Tabs = self.query_one("#file-tabs") tabs: Tabs = self.query_one("#file-tabs")
if self.open_file not in self.unsaved_files: if self.open_file not in self.unsaved_files:
if self.open_file: if self.open_file:
self.file_tabs.pop(self.open_file) self.file_tabs.pop(self.open_file)
tabs.remove_tab(tabs.active_tab) tabs.remove_tab(tabs.active_tab)
self.open_file = str(event.path) self.open_file = str(event.path)
self.sub_title = os.path.basename(self.open_file) self.sub_title = os.path.basename(self.open_file)
if str(event.path) not in self.file_tabs: if str(event.path) not in self.file_tabs:
new_tab = Tab(os.path.basename(str(event.path))) new_tab = Tab(os.path.basename(str(event.path)))
new_tab.tooltip = str(new_tab.label) new_tab.tooltip = str(new_tab.label)
setattr(new_tab, "file_path", str(event.path)) setattr(new_tab, "file_path", str(event.path))
await tabs.add_tab(new_tab) await tabs.add_tab(new_tab)
self.file_tabs[str(event.path)] = new_tab self.file_tabs[str(event.path)] = new_tab
tabs.active = new_tab.id tabs.active = new_tab.id
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)
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
content = b""
file_extension = ""
if self.open_file:
try:
f = open(self.open_file, "rb")
except Exception as e:
self.notify(f"Failed to open the file: {e}")
return
# if the file is unsaved, use the content we wrote into it
# otherwise, read the CURRENT state of the file
if self.open_file in self.unsaved_files:
content = self.unsaved_files[self.open_file]["current"]
else:
content = f.read()
# get the file extension
file_extension = self.open_file.split(".")[-1] if "." in self.open_file else ""
# close the file
f.close()
self.open_editor(file_extension, content)
@on(Tabs.TabActivated) @on(Window.Minimized)
def on_tab_shown(self, event: Tabs.TabActivated): 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()}
if self.file_clicked:
self.file_clicked = False tabs.active_tab.tooltip = f"Unsaved changes in {tabs.active_tab.label}"
else: tabs.active_tab.label = "[d orange]●[/] " + str(tabs.active_tab.label)
self.open_file = getattr(event.tab, "file_path")
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)
self.switching = True 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
code_editor: TextArea = self.query_one("#code-editor") self.open_file = None
if self.open_file: self.switching = True
try: code_editor: TextArea = self.query_one("#code-editor")
f = open(self.open_file, "r", encoding="utf-8")
except Exception as e:
self.notify(f"Failed to open the file: {e}")
return
code_editor.disabled = False
code_editor.text = ""
def done_saving(self, result):
try: if result is None: return
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) with open(result, "w", encoding="utf-8") as f:
code_editor.disabled = False f.write(self.query_one("#code-editor").text)
except UnicodeDecodeError:
code_editor.text = "This file is in binary, it can't be opened. Sorry."
code_editor.language = None
code_editor.disabled = True
f.close() tabs: Tabs = self.query_one("#file-tabs")
else: tabs.active_tab.label = os.path.basename(result)
code_editor.text = "" self.notify(f"Saved to {result} successfully.", title="Done!", markup=False)
code_editor.language = None self.query_one(DirectoryTree).reload()
code_editor.disabled = False
code_editor.focus() 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()
@on(Window.Minimized) # dont bother saving if there are no new changes
def window_minimized(self, event: Window.Minimized): if not self.open_file in self.unsaved_files: return
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()}
with open(self.open_file, "w") as f:
tabs.active_tab.tooltip = f"Unsaved changes in {tabs.active_tab.label}" f.write(self.unsaved_files[self.open_file]["current"])
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")
tabs: Tabs = self.query_one("#file-tabs") tabs.active_tab.label = os.path.basename(self.open_file)
new_tab = Tab("New File") tabs.active_tab.tooltip = str(tabs.active_tab.label)
setattr(new_tab, "file_path", None) self.unsaved_files.pop(self.open_file)
tabs.add_tab(new_tab) self.notify("Saved.")
tabs.active = new_tab.id
self.open_file = None def action_quit(self):
self.switching = True self.observer.stop()
code_editor: TextArea = self.query_one("#code-editor") self.observer.join()
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
self.file_clicked = False
code_editor.disabled = False self.observer = Observer()
code_editor.text = "" self.observer.schedule(Watcher(self), path=self.path)
self.observer.start()
def done_saving(self, result): self.config_handler.apply_settings()
if result is None: return
with open(result, "w", encoding="utf-8") as f:
f.write(self.query_one("#code-editor").text)
tabs: Tabs = self.query_one("#file-tabs")
tabs.active_tab.label = os.path.basename(result)
self.notify(f"Saved to {result} successfully.", title="Done!", markup=False)
self.query_one(DirectoryTree).reload()
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 action_quit(self):
self.observer.stop()
self.observer.join()
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
self.file_clicked = False
self.observer = Observer()
self.observer.schedule(Watcher(self), path=self.path)
self.observer.start()
self.config_handler.apply_settings()
if __name__ == "__main__": if __name__ == "__main__":
working_path = os.getcwd() if len(sys.argv) == 1 else sys.argv[1] working_path = os.getcwd() if len(sys.argv) == 1 else sys.argv[1]
app = Berry(working_path) app = Berry(working_path)
app.run() app.run()

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": []

127
prompt.py
View File

@@ -1,80 +1,85 @@
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
class Prompt(ModalScreen): class Prompt(ModalScreen):
DEFAULT_CSS = """ DEFAULT_CSS = """
Prompt { Prompt {
align: center middle; align: center middle;
#window { #window {
max-width: 50; max-width: 50;
height: auto; height: auto;
border: panel $accent; border: panel $accent;
#question { #question {
width: 100%; width: 100%;
margin: 2; margin: 2;
margin-top: 1; margin-top: 1;
} }
#bottom { #bottom {
dock: bottom; dock: bottom;
margin-bottom: 1; margin-bottom: 1;
margin-left: 1; margin-left: 1;
#yes { #yes {
margin-left: 1; margin-left: 1;
margin-right: 2; margin-right: 2;
} }
#confirm-string { #confirm-string {
margin-left: 1; margin-left: 1;
} }
Input { Input {
max-width: 25; max-width: 25;
} }
} }
} }
} }
""" """
BINDINGS = [ BINDINGS = [
Binding("escape", "close", "Close") Binding("escape", "close", "Close")
] ]
def action_close(self): def action_close(self):
self.dismiss(None) self.dismiss(None)
def on_button_pressed(self, event: Button.Pressed): def on_button_pressed(self, event: Button.Pressed):
if event.button.id == "yes": if event.button.id == "yes":
self.dismiss(True) self.dismiss(True)
elif event.button.id == "no": elif event.button.id == "no":
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!")