Upload files to "/"
This commit is contained in:
247
main.py
Normal file
247
main.py
Normal file
@@ -0,0 +1,247 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Header, Footer, ContentSwitcher, DirectoryTree, Static, Button, TextArea, Tabs, Tab, RichLog, Input
|
||||
from textual.containers import HorizontalGroup, Vertical
|
||||
from textual.binding import Binding
|
||||
from textual_window import Window
|
||||
from textual import on
|
||||
from assets.theme_mappings import theme_mappings
|
||||
|
||||
from textual_fspicker import FileOpen, FileSave
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
|
||||
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)
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
with Vertical(id="sidebar"):
|
||||
with HorizontalGroup(id="sidebar-buttons"):
|
||||
yield Button("📂")
|
||||
yield Button("🔍")
|
||||
|
||||
with ContentSwitcher(initial="files"):
|
||||
with Vertical(id="files"):
|
||||
yield Static("EXPLORER")
|
||||
yield DirectoryTree("./", id="directory")
|
||||
|
||||
with Vertical(id="editor"):
|
||||
first_tab = Tab("New File")
|
||||
first_tab.file_path = None
|
||||
|
||||
yield Tabs(
|
||||
first_tab,
|
||||
id="file-tabs"
|
||||
)
|
||||
yield TextArea.code_editor(placeholder="This file is empty.", language="python", theme="css", id="code-editor", disabled=True)
|
||||
with Vertical(id="console-container"):
|
||||
yield RichLog(id="console")
|
||||
yield Input(placeholder="> ", id="console-input")
|
||||
|
||||
yield Footer()
|
||||
|
||||
def run_command(self, command: str):
|
||||
console = self.query_one("#console")
|
||||
|
||||
result = subprocess.check_output(command, shell=True, text=True)
|
||||
console.write(result)
|
||||
|
||||
def action_find(self):
|
||||
try:
|
||||
self.query_one("#find-window")
|
||||
return
|
||||
except:
|
||||
|
||||
find_window = Window(
|
||||
|
||||
Vertical(
|
||||
HorizontalGroup(
|
||||
Input(placeholder="Find"),
|
||||
Static("0 of 0", id="num-matches"),
|
||||
Button("↑", flat=True),
|
||||
Button("↓", flat=True),
|
||||
),
|
||||
HorizontalGroup(
|
||||
Input(placeholder="Replace"),
|
||||
),
|
||||
),
|
||||
|
||||
icon="🔍",
|
||||
start_open=True,
|
||||
allow_resize=False,
|
||||
allow_maximize=False,
|
||||
id="find-window",
|
||||
mode="temporary",
|
||||
name="Find & Replace"
|
||||
)
|
||||
|
||||
self.mount(find_window)
|
||||
|
||||
async def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected):
|
||||
if self.open_file == str(event.path):
|
||||
return
|
||||
|
||||
self.file_clicked = True
|
||||
self.switching = True
|
||||
|
||||
tabs: Tabs = self.query_one("#file-tabs")
|
||||
|
||||
if self.open_file not in self.unsaved_files:
|
||||
if self.open_file:
|
||||
self.file_tabs.pop(self.open_file)
|
||||
tabs.remove_tab(tabs.active_tab)
|
||||
|
||||
self.open_file = str(event.path)
|
||||
self.sub_title = os.path.basename(self.open_file)
|
||||
|
||||
if str(event.path) not in self.file_tabs:
|
||||
new_tab = Tab(os.path.basename(str(event.path)))
|
||||
new_tab.tooltip = str(new_tab.label)
|
||||
setattr(new_tab, "file_path", str(event.path))
|
||||
await tabs.add_tab(new_tab)
|
||||
self.file_tabs[str(event.path)] = new_tab
|
||||
tabs.active = new_tab.id
|
||||
else:
|
||||
tabs.active = self.file_tabs[str(event.path)].id
|
||||
|
||||
|
||||
|
||||
|
||||
@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
|
||||
|
||||
code_editor: TextArea = self.query_one("#code-editor")
|
||||
if self.open_file:
|
||||
try:
|
||||
f = open(self.open_file, "r", encoding="utf-8")
|
||||
except Exception as e:
|
||||
self.notify(f"Failed to open the file: {e}")
|
||||
return
|
||||
|
||||
|
||||
|
||||
|
||||
try:
|
||||
if self.open_file in self.unsaved_files:
|
||||
code_editor.text = self.unsaved_files[self.open_file]["current"]
|
||||
else:
|
||||
code_editor.text = f.read()
|
||||
|
||||
code_editor.language = theme_mappings.get(self.open_file.rsplit(".", 1)[1], None)
|
||||
code_editor.disabled = False
|
||||
except UnicodeDecodeError:
|
||||
code_editor.text = "This file is in binary, it can't be openned. Sorry."
|
||||
code_editor.language = None
|
||||
code_editor.disabled = True
|
||||
|
||||
f.close()
|
||||
else:
|
||||
code_editor.text = ""
|
||||
code_editor.language = None
|
||||
code_editor.disabled = False
|
||||
|
||||
|
||||
@on(Window.Minimized)
|
||||
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()}
|
||||
|
||||
|
||||
tabs.active_tab.tooltip = f"Unsaved changes in {tabs.active_tab.label}"
|
||||
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")
|
||||
new_tab = Tab("New File")
|
||||
setattr(new_tab, "file_path", None)
|
||||
tabs.add_tab(new_tab)
|
||||
tabs.active = new_tab.id
|
||||
|
||||
self.open_file = None
|
||||
self.switching = True
|
||||
code_editor: TextArea = self.query_one("#code-editor")
|
||||
|
||||
code_editor.disabled = False
|
||||
code_editor.text = ""
|
||||
|
||||
def done_saving(self, result):
|
||||
if result is None: return
|
||||
|
||||
with open(result, "wb") as f:
|
||||
f.write(self.query_one("#code-editor").text.encode())
|
||||
self.notify(f"Saved to {result} successfully.", title="Done!", markup=False)
|
||||
|
||||
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 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.query_one("#console").write("Run a command below.")
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = Berry()
|
||||
app.run()
|
||||
75
style.tcss
Normal file
75
style.tcss
Normal file
@@ -0,0 +1,75 @@
|
||||
#sidebar {
|
||||
background: $boost-darken-1;
|
||||
width: 15%;
|
||||
min-width: 20;
|
||||
dock: left;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
#sidebar-buttons {
|
||||
padding: 1;
|
||||
padding-bottom: 0;
|
||||
background: $boost-darken-1;
|
||||
border-bottom: tall $surface;
|
||||
|
||||
Button {
|
||||
max-width: 5;
|
||||
margin-right: 1;
|
||||
}
|
||||
}
|
||||
|
||||
ContentSwitcher Vertical {
|
||||
padding: 1;
|
||||
Static {
|
||||
margin-bottom: 1;
|
||||
text-style: dim;
|
||||
}
|
||||
}
|
||||
|
||||
#files {
|
||||
#directory {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
#editor {
|
||||
margin-top: 1;
|
||||
|
||||
#code-editor {
|
||||
min-height: 3;
|
||||
}
|
||||
}
|
||||
|
||||
#console-container {
|
||||
padding-top: 1;
|
||||
margin: 1;
|
||||
border-top: tall $boost;
|
||||
height: 35%;
|
||||
min-height: 12;
|
||||
|
||||
#console {
|
||||
border: tall $boost;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#find-window {
|
||||
width: 45;
|
||||
height: 11;
|
||||
|
||||
Input {
|
||||
width: 20;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#num-matches {
|
||||
width: 10;
|
||||
margin-left: 1;
|
||||
height: 3;
|
||||
content-align: left middle;
|
||||
}
|
||||
|
||||
Button {
|
||||
max-width: 5;
|
||||
}
|
||||
}
|
||||
16
theme_mappings.py
Normal file
16
theme_mappings.py
Normal file
@@ -0,0 +1,16 @@
|
||||
theme_mappings = {
|
||||
"py": "python",
|
||||
"json": "json",
|
||||
"tcss": "css",
|
||||
"css": "css",
|
||||
"html": "html",
|
||||
"htm": "html",
|
||||
"rs": "rust",
|
||||
"js": "javascript",
|
||||
"xml": "xml",
|
||||
"toml": "toml",
|
||||
"go": "go",
|
||||
"sh": "bash",
|
||||
"yaml": "yaml",
|
||||
"md": "markdown"
|
||||
}
|
||||
Reference in New Issue
Block a user